Developer guide

Statement-bundle webhooks

Configure an HTTPS callback URL on a bundle, or one for your whole account, and OptiTrust will post signed lifecycle events when the bundle or its files change.

Testing access

Discuss non-production testing before integration.

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

Contact Us

Application limits

Plan for the current global limiter.

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; it is not a dedicated API quota. Requests rejected by the limiter receive HTTP 429.

Proxying and scale-out can affect the effective boundary, so this is not a throughput guarantee. The separate 30-per-minute public policy applies to selected anonymous endpoints and is not the authenticated API limit.

Delivery contract

At-least-once delivery. Deduplicate by event ID.

Every event is recorded in a durable outbox in the same database transaction as the change that caused it, so a committed change always has its event. A background dispatcher then delivers it. A delivery succeeds when your receiver answers with any 2xx status within 15 seconds; anything else (a non-2xx status, a timeout, a connection error) fails that attempt.

  • At least once. The same event can arrive more than once, for example when your 2xx response is lost. Use X-OptiTrust-Event-Id (also eventId in the body) as the idempotency key.
  • No ordering guarantee. Events can arrive out of order, especially around retries. occurredAtUtc orders the changes that triggered events; it is not a version of the payload. Fetch the bundle from the API when you need its current state.
  • Current state, historical trigger. The bundle and file objects describe the state when the event is sent, not when it happened; event, changeKind and occurredAtUtc describe what happened and when.
  • Unsafe or unsigned destinations fail at once. A URL that is not public HTTPS, or a destination without a signing secret, fails the event without retrying. Events with no destination configured are not sent.
Retry schedule after a failed attempt (10 attempts, about 94 hours in total)
After attemptNext attempt in
11 minute
25 minutes
315 minutes
41 hour
53 hours
66 hours
712 hours
824 hours
948 hours
10No further attempts: the event is marked failed.

Times are approximate: the dispatcher runs every minute. After the tenth failed attempt the event is not retried; fetch the bundle from the API to reconcile.

Bundle responses expose up to the 10 most recent webhook delivery entries, with dateUtc, status (delivered or failed), and detail. The detail names the event, its ID, the attempt number, the failure code (for example http_503 or timeout) and which secret signed it (bundle or account-v2), never a secret value.

Configuration

Set webhookUrl when creating or updating a bundle.

Send an HTTPS URL in the bundle API request. A blank value disables the bundle's own callbacks. On update, omit webhookUrl (or send null) to keep the existing URL and secret; changing the URL keeps the same secret.

The bundle's webhookSecret is returned only by the response that generated it: the 201 Created create response (and its 200 replay with the same Idempotency-Key), or the update response that first set a webhookUrl. Every other response, including GET, the bundle list and file completion, returns webhookSecret: null. Store the secret when you receive it.

{
  "idnumber": "8001015009087",
  "type": "TransactionExtraction",
  "dateRange": "Last90Days",
  "proofOfAccount": "NotNeeded",
  "webhookUrl": "https://client.example.com/optitrust/webhooks"
}

Account webhook

One endpoint for every bundle in your account.

Tenant administrators can set an account-level bundle webhook in the OptiTrust workspace, on the company details page under Bundle webhooks. It receives events for every bundle in the account, including bundles created in the workspace or by email. A webhookUrl set on an individual bundle overrides it, and that bundle's events are signed with the bundle's own webhookSecret.

The account signing secret is shown once, on the page that generated it, and never again. Rotate it in two steps so no delivery fails verification:

  1. Generate a new secret. It is pending: deliveries are still signed with the active secret. Configure your receiver to accept both.
  2. Activate the pending secret. Deliveries from then on are signed with it. Remove the old secret from your receiver once in-flight retries have drained.

A pending secret can be replaced before activation; the one shown earlier then cannot be activated. The URL can only be set once a secret is active. Each payload carries account.id, a stable public identifier for your account, so one receiver can serve several OptiTrust accounts.

File upload

Upload files through a server-issued blob target.

Clients do not need shared Azure Blob Storage access. The bundle file endpoint is a completion call, not a multipart upload. Request an upload target first, upload the PDF bytes to the returned signed URL, then post the file metadata and uploadToken back to OptiTrust.

1. Request a one-file upload target.

POST /api/bundles/{identifier}/files/upload-target
XApiKey: <api key>
{
  "fileId": "84a60d05-7546-4731-a23a-e41777d891e4",
  "uploadUri": "https://storage.example/optitrust-upload/84a60d05-7546-4731-a23a-e41777d891e4?...",
  "uploadToken": "<signed token>"
}

2. Upload the PDF bytes directly to the signed URL.

PUT {uploadUri}
x-ms-blob-type: BlockBlob
Content-Type: application/pdf

<PDF bytes>

3. Complete the file attachment in OptiTrust.

POST /api/bundles/{identifier}/files
XApiKey: <api key>
Content-Type: application/json

{
  "fileName": "bank-statement.pdf",
  "fileId": "84a60d05-7546-4731-a23a-e41777d891e4",
  "size": 1024,
  "mimeType": "application/pdf",
  "uploadToken": "<uploadToken from upload-target>"
}
  • POST /api/bundles/{identifier}/files does not accept raw PDF bytes.
  • The uploadUri is HTTPS-only, create-only, scoped to one server-chosen blob name, and expires after 20 minutes.
  • The uploadToken proves the fileId was issued by OptiTrust and expires after 30 minutes.
  • Only application/pdf files up to 10 MB are accepted.
  • A successful completion returns the updated bundle (with webhookSecret: null) and sends bundle.file_added when callbacks are configured.
Statement-bundle webhook events
Event When it is sent
bundle.created A bundle is created: through the API, in the workspace, or by email.
bundle.updated The bundle is updated through the API (PUT) or its details are saved in the workspace, or its applicant upload link is issued, replaced or revoked.
bundle.deleted The bundle is soft-deleted.
bundle.status_changed The bundle status actually changes during review or processing.
bundle.file_added A file is attached to the bundle (API, workspace, applicant upload link or email).
bundle.file_deleted A bundle file is deleted.
bundle.file_updated A file's API-visible result or review state changes. changeKind says how (below).
bundle.erased The bundle was erased under the retention policy or on request (POPIA). Sent after the erasure, with identifiers only. Delete your copy of the bundle's personal data.
bundle.recheck_completed A background recheck (POST /api/bundles/{identifier}/files/retry, or a PUT that changed the ID number or a name) finished, even when no document changed. Its recheck object carries the job's jobId, kind, status, counts and completedAtUtc as they were when it finished.
bundle.transactions_ready A document's transactions were stored, a recheck changed what the transaction endpoints return for that document (a change to data the API does not return does not trigger it), or approving a statement with issues made them available (one event per document). Its transactions object carries the accountId and the stored transactionCount only, never the transactions or the account number: fetch them with GET /api/bundles/{identifier}/files/{fileId}/transactions or the account endpoints. accountId is null while account downloads are not configured for the platform; then use the event's file.id with the per-file endpoint. Like every event it can arrive out of order (for example before bundle.file_added), and it is skipped when the document or bundle has since been deleted or no longer has transactions.
changeKind values for bundle.file_updated
changeKind Meaning
result_changedA re-check changed the file's result (code or issues).
approvedOptiTrust manually approved the file's issues.
unapprovedA manual approval was withdrawn.
flaggedThe file was flagged for review.
unflaggedThe review flag was cleared.
restoredA deleted file was restored.

changeKind is null for every other event. Treat unknown events and unknown changeKind values as a signal to fetch the bundle; new ones may be added.

Signature

Verify every delivery before processing it.

  • X-OptiTrust-Event-Id contains the idempotency key for this delivery.
  • X-OptiTrust-Event contains the event name.
  • X-OptiTrust-Timestamp contains the Unix timestamp in seconds.
  • X-OptiTrust-Signature contains sha256=<hex>.

Compute HMAC-SHA256 over this UTF-8 string, using the bundle webhookSecret when the bundle has its own webhookUrl, or else your account signing secret, then compare it with the signature header using a constant-time comparison:

{timestamp}.{eventId}.{rawJsonBody}

X-OptiTrust-Timestamp is the time of this attempt, so a retried delivery carries a fresh timestamp. Reject unsigned requests and stale timestamps, and treat a repeated event ID as already processed. Return any 2xx response only after the payload has been accepted.

Payload

Use eventId for idempotency and inspect the bundle or file object.

Use file.result as the machine-readable screening outcome. The bundle status describes workflow state only and should not be used as the anti-fraud verdict.

occurredAtUtc is when the change happened. For the few events queued by the previous delivery system before this contract, it is the send time instead. file is null for bundle-level events, and { "id": 42, "missing": true } if the file no longer exists.

{
  "eventId": "8f55f15d0c1f4b4c96f403edb89bb720",
  "event": "bundle.file_updated",
  "changeKind": "flagged",
  "occurredAtUtc": "2026-07-10T08:30:00Z",
  "account": {
    "id": "5b0f0c52-7a55-4c3e-9f0e-2f4d6d1f7a11"
  },
  "bundle": {
    "identifier": "c73c4e02-85c5-4b1b-9ad0-9af82b46ec22",
    "clientReference": "loan-10042",
    "status": "Awaiting documents",
    "dateAddedUtc": "2026-07-10T08:00:00Z",
    "deleted": false,
    "fileCount": 2
  },
  "file": {
    "id": 42,
    "fileName": "bank-statement.pdf",
    "status": "Pending",
    "result": {
      "processingState": "pending",
      "code": "ToBeProcessed",
      "label": "To be processed",
      "verdict": "pending",
      "riskBand": "not_applicable",
      "riskScore": null,
      "issues": [],
      "manuallyApproved": false,
      "flaggedForReview": true
    },
    "deleted": false,
    "deletedAtUtc": null
  }
}

bundle.erased carries identifiers only:

{
  "eventId": "0c1d9a6e4f2b4d8e9a7b3c5d1e2f3a4b",
  "event": "bundle.erased",
  "occurredAtUtc": "2026-12-01T04:30:12Z",
  "account": {
    "id": "5b0f0c52-7a55-4c3e-9f0e-2f4d6d1f7a11"
  },
  "bundle": {
    "identifier": "c73c4e02-85c5-4b1b-9ad0-9af82b46ec22"
  }
}

bundle.transactions_ready adds a transactions object (the bundle and file objects are as above):

{
  "eventId": "3e9b7c1d2a4f4e6b8c0d1f2a3b4c5d6e",
  "event": "bundle.transactions_ready",
  "changeKind": null,
  "occurredAtUtc": "2026-07-10T08:31:05Z",
  "account": { "id": "5b0f0c52-7a55-4c3e-9f0e-2f4d6d1f7a11" },
  "bundle": { "identifier": "c73c4e02-85c5-4b1b-9ad0-9af82b46ec22", ... },
  "file": { "id": 42, ... },
  "transactions": {
    "accountId": "kq3Vb0mXo9yR2cJ7tWzL4hN8pF1sD6aE5uG0iT3xY2w",
    "transactionCount": 184
  }
}

Receiver guidance

  • Store the webhookSecret (and your account signing secret) securely and treat them as credentials. They are shown once.
  • Deduplicate by X-OptiTrust-Event-Id, and record the event ID in the same database transaction as its side effects, so both commit or neither does. Alternatively, write the raw delivery to a durable inbox, return 2xx only after that commit, and process the inbox afterwards. Never mark an event as processed before its work has committed: unfinished work must stay recoverable.
  • Do not rely on arrival order. occurredAtUtc orders the triggering changes only; it is not a version of the bundle or file state in the payload. Reconcile current state by fetching the bundle from the API.
  • On bundle.erased, delete the personal data you hold for that bundle and keep an erasure tombstone for its identifier, so a delayed or retried ordinary event for that bundle cannot recreate the data you deleted.
  • Use file.result for the screening outcome; bundle.status is the bundle workflow state.
  • Fetch the latest bundle from the API if your system needs current state after receiving an event.
  • Review webhookDeliveries on the bundle response when diagnosing delivery failures.