Swagger API reference
Explore the authenticated REST endpoints, request bodies, response models, and status codes for bundle, employment confirmation, and signing API operations.
Open Swagger (opens in a new tab)Developers
Find the API reference, webhook contract, upload flow, and tenant setup entry points used by client integrations.
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
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.
Explore the authenticated REST endpoints, request bodies, response models, and status codes for bundle, employment confirmation, and signing API operations.
Open Swagger (opens in a new tab)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 guideValidate signed bundle events with HMAC-SHA256, handle retries idempotently, and inspect delivery history on bundle responses.
Verify webhook signaturesSign in to your tenant dashboard to create, update, or revoke API keys before calling authenticated endpoints.
Sign in to manage keysGive 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
Current client integrations are exposed under /api/bundles,
/api/employment-confirmations, /api/sign,
/api/signing-requests, and /api/credits.
Errors
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
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
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
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
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
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
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.
Applicant upload links
POST /api/bundles/{identifier}/upload-link issues a link for a bundle that is awaiting
documents and returns 201 with url, issuedAtUtc and
expiresAtUtc. Send an optional expiresAtUtc (in the future, at most
90 days ahead) or omit it for no expiry. Issuing again replaces the link and the old one stops
working at once. A bundle that is not awaiting documents returns 409 conflict.
The url is a bearer secret returned only in that response: send it to the applicant and do
not log it or store it in plain text. GET /api/bundles/{identifier} reports only
uploadLink.active, uploadLink.issuedAtUtc and
uploadLink.expiresAtUtc, never the URL.
DELETE /api/bundles/{identifier}/upload-link revokes the link and returns 204
whether or not one was active. A link also stops working when it expires, or when the bundle leaves
awaiting documents, is deleted or is erased. Issuing and revoking send bundle.updated.
Older links built from the bundle identifier (/Upload?id=) no longer work.