Skip to content

Timeouts, Retries and Idempotency

TransactAPI does not use idempotency keys or conditional writes. This guide explains what the API guarantees instead — when a request has actually completed, how to recover safely after a lost response, and which methods are safe to retry blindly.

When a Request Completes

statusCode 101 is only returned after every database write for the request has completed. Webhooks are delivered after the response has been sent — a webhook never arrives before the response that triggered it, and a response never implies a webhook is still pending.

This also means a client that never receives the response cannot tell, from the absence of a response alone, whether the request succeeded.

The 60-Second Request Limit

The server allows up to 60 seconds to process a request. If your client gives up before that — a timeout, a closed connection, a client-side abort — the server does not stop processing. Work already in progress runs to completion; database writes and the webhook still happen.

A lost response means the outcome is unknown, not failed. Treat it as unknown and read back the current state (below) rather than assuming the request didn't happen and retrying blindly.

If your client times the connection from send time

Some HTTP clients measure the timeout from when the request was sent, not from when the server started processing it. If the server never fully receives a request within 30 seconds of the client connecting, it is never started — there is nothing to complete. Since the server's own limit is 60 seconds, a request that was received is guaranteed to have reached a terminal state (success, rejection, or a completed side effect) within 90 seconds of when your client sent it. A request that is terminated client-side does not resume. If your client cannot distinguish "never received" from "received but slow," 90 seconds after send is a safe point to read back.

Recovering from an Interrupted Call

After a lost or ambiguous response, read the resource back instead of guessing:

GET /v3/parties/{partyId}
GET /v3/accounts/{accountId}

Compare updatedDate to the value you observed before the call. updatedDate advances whenever a real, persisted change is made to the record — this is a change detector, not a version token, but it is enough to tell whether your write landed:

  • updatedDate advanced — the write completed.
  • updatedDate is unchanged — the write has not happened (yet, or at all). It is safe to resend a legacy full-row update in this state, because an identical replay of a call that never took effect changes nothing.

updatedDate has second resolution. Compare against the value you last observed from a prior response, not against wall-clock send time — a write that lands within the same second as your comparison point can look unchanged even though it happened.

Updates to a redacted field (SSN, date of birth)

socialSecurityNumber and dob/date-of-birth fields are redacted on read for most API keys (see Field Redaction by Scope), so you often can't read the new value back directly to confirm a write. updatedDate still works for this: record it before the update, then compare it after. If it advanced, the write landed even though the field itself reads back as REDACTED. If it's unchanged, resend — a replay that changes nothing does not move updatedDate.

Retrying by Endpoint Class

Not every method is safe to retry the same way.

Endpoint class Safe to retry blindly? Behavior on replay
Resource PATCH (e.g. PATCH /v3/accounts/{accountId}) Yes Touches only the fields present in the request body. An identical replay is a no-op — HTTP 200/101 if any field actually changed, or HTTP 304 if literally nothing in the row changed (the body still carries statusCode/statusDesc, even on a 304). No webhook fires on a no-op replay.
Legacy full-row methods (updateParty, updateAccount) Converges on the same end state, but has side effects on every call Rewrites the entire row from the request. A replay of the same payload converges on the same stored values, but every call re-fires the method's webhook and email notification, whether or not anything actually changed. Don't replay these speculatively — read back first.
KYC/AML verification (performKycAmlBasic, performKycAml, performAml) No — check first Each invocation is billed per call, regardless of whether the underlying verification succeeds. Read back the current KYC/AML state before retrying; see below.

Detecting a completed KYC/AML check before retrying

kycDate on the party record only records the day, not the time, so it can't distinguish an interrupted call from an earlier check that already ran the same day. To get a precise read:

POST /v3/getKycAmlResponse
{ "partyId": "...", "type": "basic" }

This returns the most recent stored result for that party and type, including createdAt (accurate to the second) and the verification provider's report id in kycamlDetails.kyc.response.id-number. Every completed check produces a new id and a new createdAt. Call this before performKycAmlBasic to record a baseline, and again after an interruption: a new id-number or a later createdAt than your baseline means the check completed. This call only reads stored results — it does not invoke the verification provider and is not billed.

updatedDate on GET /v3/parties/{partyId} also advances on every completed KYC/AML check, since the stored result updates the row, but getKycAmlResponse is the more precise signal when you need to confirm which specific check completed.

Ordering

There is no server-side request serialization, cross-request transaction isolation, or conditional write (compare-and-swap) support anywhere in the API.

  • Resource PATCH endpoints touch only the fields sent in the request, so two concurrent PATCH calls to different fields on the same resource don't clobber each other.
  • Legacy full-row methods (updateParty, updateAccount) read the entire row at the start of the request and write the entire row back at the end. If two updateParty calls for the same party run concurrently, whichever finishes last overwrites every field from its own starting snapshot — including any change the other call made in between, even to fields the losing call never intended to touch.

Serialize updateParty and updateAccount calls per party/account ID on the client. The API will not order them for you.

Definitive Rejections

The following are all rejected before any database write, so they are always safe to fix and resend without a read-back first:

statusCode Meaning
103 Invalid client ID or API key, or the developer key is not active
106 Required data or parameter missing
110 Permission denied
111 Not authorized to call this method (also returned when the caller's IP is not on the client's allowlist)
148 Account does not exist or is not active
198 Party does not exist
775 The Social Security Number is flagged and cannot be used
776 The email address is flagged and cannot be used
1400 / 1403 / 1404 Resource-style endpoints: bad request / forbidden / not found

See Error Codes for the complete list.

An HTTP 504 carries no statusCode envelope at all — it comes from the load balancer giving up on the connection, not from the API. Treat a 504 the same as any other lost response: outcome unknown, read back before retrying.

Set your client-side request timeout above the server's 60-second processing limit if you want the call itself to return a definitive answer. A shorter client timeout just moves the ambiguity to your side — the server keeps processing, and you're back to the read-back procedure above regardless.