Developers

Build with OptiTrust APIs.

Find the API reference, webhook contract, upload flow, and tenant setup entry points used by client integrations.

Developer links

API requests use tenant API keys in the XApiKey header. Generate keys in the tenant dashboard, then use Swagger and the bundle guide to wire the integration.

Integration operating contract

Plan testing and traffic before going live.

Contact Us to discuss non-production testing and access options.

The application currently has an in-process global limiter of 600 requests per minute, partitioned by the remote IP observed by the application. Because it is global, it also applies to authenticated API calls. Requests rejected by the limiter receive HTTP 429.

Statement bundle upload flow

Create a bundle, request a signed upload target, send PDF bytes to blob storage, complete the file attachment, and receive lifecycle callbacks.

Read the bundle guide

Webhook signatures

Validate signed bundle events with HMAC-SHA256, handle retries idempotently, and inspect delivery history on bundle responses.

Verify webhook signatures

API keys and tenant setup

Sign in to your tenant dashboard to create, update, or revoke API keys before calling authenticated endpoints.

Sign in to manage keys

LLM integration skill

Give this Markdown file to an AI coding assistant so it can implement the OptiTrust API flow with the correct upload, webhook, signing, and authentication rules.

Open the skill file

Errors

Every API error is problem+json with a stable code.

Errors from the public API use application/problem+json with status, title, detail, a machine-readable code, and a traceId to quote to support. Branch on code, not on the message text. The error member equals detail when present, otherwise title, for clients written against the earlier { "error": "..." } bodies. Validation failures can also include errors per field.

Codes: validation_failed, invalid_upload, file_too_large, unsupported_file_type, unauthorized, forbidden, signing_not_configured, subscription_required, not_found, method_not_allowed, insufficient_credits, idempotency_conflict, conflict, nothing_to_retry, payload_too_large, unsupported_media_type, rate_limited, recheck_unavailable, transactions_unavailable, transactions_timeout, not_acceptable, and internal_error. Any other HTTP error status uses the fallback code error.

Credits

Check the balance before uploading.

Completing a bundle file upload without enough credits returns HTTP 402 with code insufficient_credits (this was HTTP 400 before). GET /api/credits returns the tenant's balance, costPerStatement, costPerAuthentication, costPerSeal, and the active subscription, if any.

POST /api/sign costs costPerSeal credits (1 by default) for each successful seal; a seal that fails is not charged. A prepaid account that cannot pay gets HTTP 402 insufficient_credits and no PDF. The PDF is returned only after its charge is confirmed. If confirmation fails and the charge remains unconfirmed, the call returns an error with no PDF and the charge is refunded automatically; an unknown confirmation outcome is investigated and reconciled separately. A retry after a lost response is a new, separately charged seal.

Prepaid balances (accounts with no active subscription) are enforced atomically when a charge is saved: a charge that would take a prepaid balance below zero is rejected with HTTP 402 insufficient_credits, even when uploads run concurrently. The balance check before a document is processed is still best-effort, so a concurrent upload can pass it and then be rejected with 402 after the document has been checked. GET /api/credits is informational and does not reserve credits. Subscription (contract) accounts may go negative by design.

Bundle references and paging

Find bundles by your own reference.

Send an optional clientReference (for example your loan application id) when you create or update a bundle. It is trimmed, must then be 1-200 characters without control characters, and is not unique. On update, omit it to keep it, or send an empty value to clear it.

GET /api/bundles filters by clientReference (exact match within your tenant; case sensitivity follows the database collation), createdFrom (inclusive) and createdTo (exclusive). Dates must use exactly yyyy-MM-dd'T'HH:mm:ssK or yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK, with K an uppercase Z or a +HH:mm/-HH:mm offset.

The body stays a JSON array. Paging metadata is in the X-Total-Count, X-Page and X-Per-Page headers, and an RFC 8288 Link header carries relative rel="next" and rel="prev" URLs.

Applicant names

Send the names the statements should match.

name, surname, maidenName and nameOnStatement are optional on create and update. The statement engine checks the account holder on each uploaded statement against them. Each is trimmed and must then be 1-100 characters without control characters. On update, omit a name to keep it, or send an empty value to clear it.

idnumber is optional too. The statement engine uses it as a candidate password for password-protected statements (many South African banks use the account holder's ID number) and to check the account holder's identity, so without it those statements cannot be opened. On create, omit it (or send an empty value) for a bundle without one; on update, omit it to keep it, or send an empty value to clear it. You can add it later with an update.

Changing the ID number or a name marks an existing assessment as stale and, once background rechecks are enabled, reads every document in the bundle again in the background when it has documents to recheck (see background rechecks). If another change of the ID number or names was saved first, nothing is saved and the update returns 409 conflict. Names are never returned in responses and are cleared when the bundle is erased.

Only a document the statement engine could not read (unavailable, or an incomplete read) is attached uncharged; it is charged once at the normal upload price when a later recheck reads it. Every completed result, including a genuine "Error parsing bank document", is priced normally. If the ID number or names change while a document is being read, the upload completion saves nothing and returns 409 conflict: complete it again.

Background rechecks

Retry locked and failed documents without re-uploading.

POST /api/bundles/{identifier}/files/retry reads the bundle's password-locked documents (with its current ID number) and the documents the statement engine could not process (processingFailed: true on the file) again, in the background. It returns 202 with the recheck. A PUT that changes the ID number or a name schedules a recheck of every document the same way, but only when the bundle has documents to recheck. Each background recheck makes up to 5 engine attempts per document (immediately, then after 1m, 5m, 15m and 1h); a document still unreadable after that keeps its previous result.

The bundle shows recheckPending (whether a recheck is pending or running at the moment the response was read) and a recheck summary (kind, status, counts); when it finishes, bundle.recheck_completed is sent with the final counts, even when no document changed. Only a document that failed to process (at the normal upload price), or a password-protected document read for the first time (at its arrival channel's price: the upload price, or the email verify cost for one that arrived by email), is charged, once, when it is read; everything else is free. recovered counts the documents whose engine-failure marker the recheck cleared, including free duplicate-account recoveries; unlocked counts the password-protected documents it opened for the first time and charged; charged is the credit total. A password-protected document is attached free and stays free while it is locked.

At most one retry per bundle per minute: sooner returns 429 rate_limited with Retry-After. A bundle with nothing to retry returns 409 nothing_to_retry; a deleted or erased bundle returns 409 conflict. Until background rechecks are enabled for your account the endpoint returns 503 recheck_unavailable with Retry-After.

Transactions

Download the extracted transactions.

GET /api/bundles/{identifier}/accounts lists the bank accounts found in the bundle's statements, each with an opaque accountId. GET /api/bundles/{identifier}/accounts/{accountId}/transactions returns that account's transactions, de-duplicated across overlapping statements and ordered by date, with optional inclusive fromDate/toDate (yyyy-MM-dd). GET /api/bundles/{identifier}/files/{fileId}/transactions returns one document's rows in extraction order. Each row has the same seven fields as the workspace's "Export transactions" CSV: date, text, transactionType, debit, credit, balance and period. Downloads are free and work with every API key.

JSON is paged (numberPerPage 1-1000, default 500) with X-Total-Count and Link; format=csv or Accept: text/csv returns the whole list as CSV (header Date,Text,TransactionType,Debit,Credit,Balance,Period), with formula-like text cells prefixed by ' and plain numbers. A response holds at most 50,000 rows and 4 MiB of stored text, otherwise 413 payload_too_large.

An Authentication-only bundle returns 409 transactions_unavailable; a deleted bundle or document 404; an erased bundle 409 conflict. Too many downloads at once return 429 rate_limited without Retry-After (retry with backoff), and a download that cannot be prepared in time returns 503 transactions_timeout. bundle.transactions_ready tells you when a document's transactions arrive, change or are made available by an approval.

Safe retries

Retry bundle creation with an Idempotency-Key.

POST /api/bundles accepts an optional Idempotency-Key header: one value of 1-128 visible ASCII characters, no spaces or commas, scoped per tenant. Repeating the same request returns HTTP 200 with the original bundle and Idempotent-Replayed: true; the first create returns 201. A different request with the same key returns 409 idempotency_conflict, and a key whose bundle was erased returns 409 conflict.

Use an opaque value such as a UUIDv4, never a loan number or ID number. OptiTrust stores only a hash of the key and keeps it after erasure so a reused key is refused.

bundle.created is recorded in the webhook outbox in the same transaction as the first committed create, and is never sent for a replay. Delivery is at least once, so process webhooks idempotently by X-OptiTrust-Event-Id. The 201, its 200 replay, and a PUT that first sets a webhookUrl (and so generates the secret) are the only responses that return the bundle's webhookSecret.