Idempotency and Retries When Calling a Verification API: Avoiding Duplicate Sessions and Duplicate Charges
What to do when a verification call times out: HTTP idempotency, the Idempotency-Key draft, backoff with jitter, and how to keep a retry from creating a second session or a second charge.
Every integration with a remote API meets the same moment sooner or later: the call leaves your server, and no answer comes back. The connection dropped, the timeout fired, a gateway returned an error page, or the process was restarted while the request was in flight. Your code now knows one thing, that it does not know whether the operation happened. For a read that does not matter, because you ask again. For a call that creates a verification session, uploads a document image or starts a paid check, asking again may create a second session, spend a second attempt or produce a second charge.
This article is about that moment. It sets out what the HTTP specification says about repeating a request, what a draft of the Internet Engineering Task Force proposes for a standard idempotency header, how three widely used APIs implement the idea, and how backoff with jitter keeps a fleet of retrying clients from becoming a second outage. It then applies all of this to a verification flow, using only what the NeuroVision documentation states, and says plainly where the documentation is silent.
The article complements the verification session lifecycle, which has a short section on retrying a create call, and receiving verification results by polling or webhook, which treats retries on the result side. Here the subject is the request side: the call you send, the doubt after it, and the rules that make a repeat safe.
What the documentation of the platform states
A retry policy must start from the facts of the interface it protects. These are the ones the NeuroVision documentation gives.
- Every call is a POST. All calls are POST and answer JSON, except file downloads. In the vocabulary of HTTP, POST is the method that is not idempotent by definition, which is why this article exists.
- A failure arrives with a success code. A failure normally arrives as HTTP
200with a body whosestatusiserror, and the documentation tells the reader to branch on thestatusfield of the body. A retry loop that looks only at the HTTP code will treat many failures as successes. - A busy session answers
409. HTTP409means another request to the same session was still being processed. Nothing of the refused request was applied, and the instruction is to repeat it once the other request has been answered. - Validation refusals change nothing. A
400on the upload call means the request was refused and nothing in the session changed, so no image is processed and no try is charged. - Uploads have a limited number of tries. Each step takes a limited number of tries, and when the upload that reaches the limit fails, the step gets the error
[general] too many triesand the session is blocked. - An upload without a known session starts a new one. For the image upload call, a new session is created when
sessionIdis empty or unknown. - A session has a time to live. The session carries
secondsToLive, and at zero anidleorprocessingsession is reported asexpired. - Your own identifier travels with the session.
clientKeyis an identifier that you define; the platform does not generate it, and it is at most 36 characters in its raw form. The session status call accepts eithersessionIdorclientKey, and filter sessions supports a match onclientKey. - A re-run of a database check is billed separately; a skipped check is not. A re-run is billed as a separate request, and a skipped check is not billed.
- Webhooks can repeat. The database check event can arrive again with the full result set, and the documentation says to handle it idempotently by
sessionId.
What the documentation does not show is also part of the picture. It shows no idempotency key header or body field on the create or upload calls, no rule for how a repeated create call is treated, and no statement of how a repeated call is counted for billing beyond the two sentences about database checks above. This article therefore never says that NeuroVision offers an idempotency key. Where it describes keys, it describes what the standards and other providers do, so that you can recognise the feature when a provider documents it and can ask for it when a provider does not. Everything that follows about your own protection against duplicates works without any such feature.
What HTTP says about repeating a request
Idempotent methods
RFC 9110, the specification of HTTP semantics, defines a request method as idempotent when the intended effect on the server of multiple identical requests with that method is the same as the effect of a single such request. Among the methods the specification defines, PUT, DELETE and the safe methods are idempotent. POST is not on that list. The text gives the reason the property is useful: an idempotent request can be repeated automatically if a communication failure occurs before the client can read the response. The specification adds that the property applies to what the user requested, and a server may still log each request separately.
What a client may do with a POST
The rule for the other methods is just as clear. A client should not automatically retry a request with a non-idempotent method unless it has some means to know that the request semantics are actually idempotent, regardless of the method, or some means to detect that the original request was never applied. A proxy must not automatically retry non-idempotent requests, and a client should not automatically retry a failed automatic retry. Those two sentences describe the design problem of this article in miniature. A retry of a POST needs one of two supports: a way to make the request idempotent on the server, which is what an idempotency key does, or a way to find out whether the original took effect, which is what a lookup by your own identifier does. Without one of them, a retry is a guess.
Status codes that bear on retries
RFC 9110 defines several codes that a retry loop has to treat differently.
Code 408, Request Timeout, means the server did not receive a complete request message within the time it was prepared to wait, and a client with a request in transit may repeat it.
Code 409, Conflict, means the request could not be completed because of a conflict with the current state of the target resource.
Code 503, Service Unavailable, means the server is currently unable to handle the request because of a temporary overload or scheduled maintenance, and the server may send a Retry-After field to suggest how long to wait.
Code 504, Gateway Timeout, means a gateway or proxy did not receive a timely response from an upstream server.
The Retry-After field takes either an HTTP date or a number of seconds to delay after receiving the response.
Notice that 504 tells you about the path in front of the server, not about the outcome: the upstream may well have finished the work after the gateway gave up.
A 504 on a create call is therefore the same doubt as a client-side timeout, and it needs the same protection.
The Idempotency-Key header: a draft, not yet a standard
Status
The Internet Engineering Task Force has a working group draft called "The Idempotency-Key HTTP Header Field", with the intended status of Standards Track. At the time of fetching, on 28 October 2026, the current version was draft-ietf-httpapi-idempotency-key-header-08, dated 22 October 2026, and its stated expiry date was 25 April 2027. The datatracker listed it as an Active Internet-Draft of the HTTPAPI working group. It is not an RFC. The draft says of itself that Internet-Drafts are working documents, valid for a maximum of six months, and that it is inappropriate to cite them other than as work in progress. This article cites it that way: as a statement of where the practice is heading, not as a rule that any API must follow today. Details can change in later versions, so check the datatracker before you copy a header name, a status code or an expiry rule into a contract.
What the draft proposes
The abstract says that the header field can be used to make non-idempotent HTTP methods such as POST or PATCH fault-tolerant. The main elements are these.
The key. An idempotency key is a unique value generated by the client, which the resource uses to recognise subsequent retries of the same request. The header is a structured field whose value is a string. The key must be unique and must not be reused with another request with a different payload, and the draft recommends a UUID or a similar random identifier. Its own example is a version 4 UUID, written between quotation marks as the string syntax requires.
Expiry. A resource may require time-based keys so that it can purge them, and it should define the expiration policy and publish it in its documentation.
The fingerprint. A server may combine the key with a fingerprint generated from the request payload, for example a checksum of the whole payload or of selected elements, to decide whether a request is the same as an earlier one.
Who does what. Resources must publish an idempotency-related specification, including expiration policy where applicable. Clients should understand what the server publishes and use a suitable algorithm to generate keys. The draft also says that, without prior knowledge, a general client cannot assume that the server will respect the header. That sentence is the one to remember when you see the header in a library: sending it to a server that ignores it protects nothing.
Enforcement. For a first-time request the resource processes the request normally. For a duplicate that arrives after the original completed, it responds in a way consistent with the original having completed. For a concurrent duplicate, one that arrives before the original completed, it responds with a resource conflict error.
Errors. The draft suggests HTTP 400 when a required key is missing, HTTP 422 when a key is reused with a different payload, and HTTP 409 when a request with the same key is still outstanding, each with a body that points to documentation, and it shows the bodies in the problem details format.
It says that clients must correct a request before retrying after the first two, and that no correction is needed after a 409.
For other errors, such as 401, 403, 500, 502, 503, 504 and 429, the client should follow the documentation of the resource.
Security. The draft warns that a resource without strong keys or validation could suffer injection attacks, when it uses an unvalidated key in a cache lookup, and data leaks, when low-entropy keys let an attacker guess the keys of other clients and fetch their cached responses. It recommends a fixed, published key format, validation of the key before processing, and a composite cache key that combines the client's key with attributes of the client known only to the server.
The draft also lists implementations that use a custom header called Idempotency-Key, among them Stripe and Adyen, and others that reach the same goal by other means, such as a field in the request body. The list is described in the draft as unverified information supplied by contributors and not as an endorsement.
How established APIs implement the idea
Three public documents show how the idea looks in practice. They differ in detail, and the differences are the reason to read the provider's own documentation before relying on any of it.
| Aspect | Stripe | Adyen | Google API improvement proposal 155 |
|---|---|---|---|
| Where the key goes | Idempotency-Key header | idempotency-key header | A request_id field in the request message |
| Length | Up to 255 characters | At most 64 characters | 36 ASCII characters |
| Recommended form | V4 UUID or another random string with enough entropy | A UUID; version 4 for security | A random UUID |
| How long the key is honoured | Keys can be removed automatically after at least 24 hours (API v1) | Valid for 7 to 14 days after first submission | Any reasonable timeframe the API chooses |
| What a duplicate returns | The saved status code and body of the first request, including 500 errors (API v1) | The response to the first attempt, without duplication | The response for the previously successful request |
| A different payload under the same key | An error | Not addressed in the page fetched | Not addressed |
| A concurrent duplicate | Not saved; the request can be retried | 422 or 409 with error code 704 | Not addressed |
Every cell in the table is a statement about a document fetched on 28 October 2026, and each may change.
Stripe
The Stripe API reference describes idempotency as the way to retry requests without accidentally performing the same operation twice.
For API version 1, Stripe saves the status code and body of the first request made with a given key, after execution of the endpoint begins, and returns the saved response on retries, including for 500 errors.
The reference advises generating keys as V4 UUIDs or another random string with enough entropy, allows keys up to 255 characters, and says to avoid sensitive data such as email addresses or personal identifiers as keys.
It says keys may be removed automatically once they are at least 24 hours old, and that a new request is generated if a key is reused after the original has been pruned.
The layer compares incoming parameters with those of the original request and returns an error if they differ, to prevent accidental misuse.
Results are saved only after execution begins, so a request that fails validation, or that conflicts with a concurrent request, leaves no saved result and can be retried.
The last two statements matter for design: the key is tied to the parameters, and a refusal before execution is not remembered.
Adyen
The Adyen documentation says that its API supports idempotency on POST requests, with GET, DELETE and PUT idempotent by definition.
The key is sent in the header, is a unique identifier with a maximum of 64 characters, and a UUID is recommended.
If the first request was already processed, the response to the first attempt is returned without duplication, and the response header repeats the key so that you can check that the request was handled idempotently.
Keys are stored at the level of the company account and are valid for 7 to 14 days after first submission.
Two further points are useful beyond payments.
A transient error, which has no side effects and can be retried, carries a header named transient-error with the value true; without that header, or with the value false, the documentation says not to retry.
And when the data store behind idempotency is unavailable, the API answers 503 and marks the error as transient, and the documentation offers two choices: pause and retry later, or fall back to non-idempotent processing by not sending the header.
The second choice is a trade that your own policy should decide on purpose, not by default.
For a create call it may be acceptable; for a payment it usually is not.
The same page also recommends an exponential backoff strategy for retrying and the use of asynchronous webhooks to track missing responses.
Google API improvement proposal 155
The Google guidance on request identification says that a request identifier is useful for de-duplicating requests from parallel processes, making retries safe and auditing, and that its most important purpose is idempotency. Providing a request identifier must guarantee idempotency, and if a duplicate is detected the server should return the response for the previously successful request, because the client most likely did not receive it. The example restricts the field to 36 ASCII characters and recommends a random UUID. The guidance includes a case that is easy to overlook, called stale success responses: a duplicate create request may arrive after the resource was created and then changed, and the service may return the current state of the resource instead of an identical copy of the historical response. For a verification API this means that a retried call may legitimately return the session as it stands now, with a status that has moved on from the one the first call would have returned. Code that retries must therefore read the state it gets back and not assume it is the state of the first answer.
What the three have in common
Three rules recur. The client, not the server, generates the key, and it is random. The key is bound to the content of the request, so that reusing it for different content is an error. The server publishes how long it remembers keys. All three put the burden of writing down the intent on the client, because only the client knows that two calls are the same attempt.
Deciding what to retry
A retry policy begins with a classification. Treat each outcome of a call as one of five kinds, and let the kind decide what happens next.
| Outcome | Kind | Action |
|---|---|---|
Body with status of ok, or a session status you expect | Success | Store the result; no retry |
| Connection refused, DNS failure, TLS failure before the request was sent | Not sent | Retry with backoff; nothing happened |
Timeout or reset after the request was sent, or a 504 | Outcome unknown | Check before you repeat, or repeat only with a key |
HTTP 409 busy session | Rejected, nothing applied | Wait until the other request is answered, then repeat |
HTTP 400 or status of error with details | Your request is wrong | Do not retry; fix the request |
HTTP 401 or 403 | Credentials or address are wrong | Do not retry; alert someone |
HTTP 500 with the text internal error | Server failure, outcome unknown | Treat like an unknown outcome; limit the attempts |
HTTP 503 on a read | Temporarily unavailable | Retry with backoff; honour Retry-After if sent |
HTTP 503 on a create or an upload | Temporarily unavailable, but a gateway may answer 503 after the work began, so the outcome can be unknown | Reconcile first. Repeat only under a documented key, a documented guarantee that a repeat is safe, or evidence that the original was never applied; then back off and honour Retry-After |
failed as the status of a session or a step | The verification result, not a call failure | Do not retry the call; act on the result |
The first column uses the shapes the documentation describes: the success code with status of error, the 409 for a busy session, the 400 with details for validation, and the 401 and 403 refusals that come before the operation.
The platform states that its 500 carries the fixed text internal error and that nothing in the body helps you branch beyond the code.
The last row is the one most often confused.
A session that ends failed is a completed verification with a negative outcome; it is not an error of the transport, and repeating the creation call will not turn it into a pass.
The documentation also says to branch on the status field of the body and never on message text, which is English prose that is reworded without notice.
Unknown outcomes are the hard case
Only the row "outcome unknown" needs design effort. Everything else is a plain rule. With an unknown outcome you have three honest options, in order of preference.
- Look before you repeat. Ask the other side whether the first call took effect, using an identifier you chose.
- Repeat under a key. If the provider documents an idempotency key, resend the identical request with the identical key.
- Accept a possible duplicate and make it harmless. Design the next stages so that a second session, or a second upload, is discarded without cost to the user.
RFC 9110 names the first two when it speaks of a client that knows the request is idempotent or can detect that the original was never applied. The third is the fall-back that every integration needs, because the first two depend on the provider.
Applying it to a verification flow
Creating a session
The create call takes a permanent API token and a scenario identifier, and it answers with a sessionId, a status of idle, the steps of the scenario, and secondsToLive.
The documented request has no field for your own identifier of the person.
So after a timeout on the create call, you cannot search for the session by a key that you sent, because you did not send one.
What you can do is separate the intent from the session.
- Before the call, write a row in your own database: the applicant, the purpose, the time, a state of
requested, and a random identifier of your own. Allow only one create call at a time for that row, with a lock or a conditional update, so that two workers cannot both send it. - Make the call. On success, store the returned
sessionIdagainst the row and move it tocreated. - On an unknown outcome, leave the row in
requested. Do not call again at once. - Let the row wait on your side for a short period. If the applicant is still waiting, create a new session and link it to the same row, knowing that the first call may have created a session you cannot see.
- Treat any orphan session on the platform side as an object that lapses when its own time to live runs out, which is a separate clock from yours.
This design does not guarantee one live session per applicant, and it should not be described as doing so.
The create call carries no correlation key, so after an unknown outcome the platform may hold a session from the first call and another from your second, and both can stay alive until their own secondsToLive ends.
The applicant may even open the link of the orphan.
What the design does is limit the damage and keep it visible: the number of creates for one row stays small, every sessionId you receive is attached to the row, and a session you never learned about can do little because no link to it was given to the applicant.
The duplication of sessions and the duplication of decisions are two separate problems.
Whatever the number of sessions, your decision logic must accept one result per row, keyed on your own row and not on the arrival order, and a second result for the same row must not overwrite the first without an explicit rule.
Uploading images from your backend
The image upload call is the one where retries can cost something real. Each step takes a limited number of tries, so a retry of an upload that actually succeeded can use up a try and, at the limit, block the session. Three documented facts shape the policy.
First, you can attach your own identifier.
The upload call accepts a clientKey, and the session status call resolves a session by clientKey as well as by sessionId.
That makes a look-before-you-repeat possible, but only as a reconciliation, because a lookup of the session does not say which upload executed.
The session object exposes, for each step, the number of uploads made (tries), the start of the current attempt (startedAt) and earlier attempts (history).
So the useful pattern is to store a baseline before you upload: the sessionId, the step, and the tries and startedAt you last saw.
After a timeout, read the session and compare.
If tries has risen above the baseline, an upload reached the step, and sending the image again would use another try.
If the state has not moved, you still do not know that the upload never ran: in async mode the call answers at once with the session in processing, and the outcome appears only later.
Unchanged or absent state therefore justifies one more check after a short wait, not an immediate repeat, and a repeat is acceptable only when the cost of a lost try is lower than the cost of waiting.
Send uploads for one session one at a time, so that no second upload is in flight while you are reconciling the first.
Prefer the sessionId you already hold for the lookup, and use clientKey when you do not have it.
Use an opaque identifier. The documentation states that the client key stays on the session after personal data is erased and that personal data should not be put in it.
Second, pass the sessionId of the session you already have.
An empty or unknown sessionId creates a new session, so a retry that loses the identifier, through a bug or two calls running at once, silently starts another verification.
Third, consider mode set to async.
In that mode the call answers at once with the session in processing and no results, and you poll the session status for the outcome.
The window in which an upload is in doubt shrinks from the whole processing time to the short time of an immediate answer.
A 409 on this call means the session was busy for the whole wait and nothing of your request was applied, so the right response is to wait for the other request to finish and send the image again.
A 400 means no try was charged.
Neither of the two should be retried in a hurry: one needs the other request to finish first, and the other needs a corrected request.
Re-running a check
The one place where the documentation connects a repeat to money is the database check. A re-run of a database check is billed as a separate request, and a skipped check is not billed. So the rules are simple. Do not re-run a check automatically in a retry loop. Re-run it as a deliberate decision, for example after you have supplied the missing input that a skipped check named, and record who or what decided. For the pricing of verification and screening, read the pricing page and do not infer a rule for repeated API calls from this article.
Counting charges
If a duplicate call can cost money, count it. Keep a counter of sessions created, sessions that reached a terminal state, and sessions you can link to an applicant record of your own. A growing gap between the first and the third is the signature of retries that did not find their original. Compare the counts with your invoice once, when you integrate, and ask the provider how a repeated create or upload call is counted, because this article cannot answer that for any provider.
Backoff and jitter
Why retries need spacing
When a service fails, every client that depends on it retries. If they all wait the same time, they all return at the same moment, and the service that was recovering is hit by a synchronised wave. The AWS Architecture Blog explains the effect with a simulation of many clients writing to one database row. Capped exponential backoff, where the client multiplies its wait by a constant after each failed attempt up to a maximum, spreads the calls over time but still leaves clusters of calls, because clients that started together stay together. Adding jitter, which is randomness in the wait, removes the clusters. In the post's simulation with 100 contending clients, jitter cut the number of calls by more than half and shortened the time to completion compared with the same backoff without jitter. The author names three variants, called full jitter, equal jitter and decorrelated jitter, finds no-jitter backoff the clear loser, and concludes that the return on the complexity of jittered backoff is huge and that it should be a standard approach for remote clients. Those figures come from a simulation of optimistic concurrency control with a mean network delay of 10 milliseconds, not from a verification service; take the direction, not the numbers.
A policy you can write down
The following is a policy, not a prescription. Every number in it is a placeholder for your own measurement.
| Parameter | What it controls | How to choose |
|---|---|---|
| Per-attempt timeout | How long one call may take | A little above the slowest normal answer you have measured |
| Overall deadline | When you stop and tell the user | The longest the applicant will stay on the screen |
| First wait and growth factor | How fast the wait grows | Small first wait, doubling is common |
| Cap | The longest single wait | Well below the overall deadline |
| Jitter | How the wait is randomised | Pick a random wait between zero and the current ceiling |
| Attempt limit | How many tries | Few for a user waiting; more for a background job |
| Retry budget | How many retries your fleet may send per minute | A share of normal traffic, so that retries cannot exceed the load that caused them |
Honour the server's hint when it gives one.
A Retry-After value on a 503 states how long to wait, and a retry sooner than that is an error in your policy.
The hint says when to try again; it does not say that a repeat of a create or an upload is safe, which is the question of the previous sections.
// Illustrative only. attemptCall must say "retry" only for outcomes the server can recover from,
// and must pass the signal to the HTTP client so that a hanging call can be cancelled.
async function callWithRetry(attemptCall, {
maxAttempts = 4, baseMs = 500, capMs = 8000, deadlineMs = 20000, attemptTimeoutMs = 5000,
}) {
const started = Date.now();
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const remainingMs = deadlineMs - (Date.now() - started);
if (remainingMs <= 0) break; // check the deadline before every dispatch
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), Math.min(attemptTimeoutMs, remainingMs));
let outcome; // { kind: "success" | "retry" | "stop", retryAfterMs? }
try {
outcome = await attemptCall(attempt, { signal: controller.signal, remainingMs });
} finally {
clearTimeout(timer);
}
if (outcome.kind !== "retry") return outcome;
if (attempt === maxAttempts - 1) break; // no sleep after the last permitted attempt
const ceiling = Math.min(capMs, baseMs * 2 ** attempt);
const wait = Math.max(outcome.retryAfterMs ?? 0, Math.random() * ceiling); // full jitter
if (Date.now() - started + wait >= deadlineMs) break;
await new Promise((r) => setTimeout(r, wait));
}
return { kind: "stop", reason: "retries exhausted" };
}
The loop passes each attempt a signal and the time that remains, so a call that hangs is cancelled at the per-attempt timeout or at the overall deadline, whichever comes first.
It checks the deadline before it sends and does not sleep after the last permitted attempt.
A cancelled call to a create or an upload is an unknown outcome and not a plain failure, so attemptCall must answer stop and hand the case to reconciliation, not retry.
The loop knows nothing about verification.
All of the domain knowledge lives in attemptCall, which returns retry only for the kinds in the classification table and applies the look-before-you-repeat rule for unknown outcomes.
Retry layers multiply
If your application retries three times, and the library under it retries three times, and the proxy in front retries twice, one failing call becomes eighteen. Pick one layer to own the retry policy and switch the others off. RFC 9110 already forbids a proxy from automatically retrying non-idempotent requests, and the same discipline applies to your own client libraries and service meshes.
Error bodies and machine-readable problems
RFC 9457, published in July 2023 as a Standards Track document that obsoletes RFC 7807, defines a problem detail format for carrying machine-readable details of errors in HTTP responses, so that APIs need not invent new error formats.
It uses the media type application/problem+json, and the object has members for a type URI, a status, a title, a detail and an instance.
Three of its rules are useful to anyone who writes a retry loop.
The status member is only advisory, and the real status code is the one in the HTTP response.
Consumers should not parse the detail member for information, because extensions are a more suitable and less error-prone way to carry it.
And the type identifies the kind of problem, so branching on it is safe where branching on the title or detail text is not.
The NeuroVision documentation does not use this format.
Its error bodies carry a status field, a details list for validation errors and an English message that the documentation says to ignore for branching.
The principle carries over anyway: branch on a stable code, never on prose.
If your own service sits in front of the platform and returns errors to your clients, the problem details format is a good choice for those responses.
Your own side of the line, and one failure trace
A retry has two ends, and the receiving end is covered elsewhere. The article on polling and webhooks already sets out conditional state transitions, a job written in the same transaction as the transition, and stable identifiers for downstream actions, and this article does not repeat them. The one point that belongs here is the documented one: the database check event can arrive again with the full result set, so replace what you stored for that session with the latest set and do not count events. For the security of the endpoint that receives the events, see securing a webhook receiver.
A trace shows how the pieces above fit. The trace is hypothetical.
- The backend writes its row for the applicant, in the state
requested, and sends the create call. - The platform creates session A.
- A gateway on the path passes the request on, loses the upstream answer and returns
503to the backend, so the backend cannot tell whether session A exists. - A loop that treats every
503as safe to repeat waits a moment and sends the call again. The platform creates session B, and the backend stores B. Session A is now an orphan with its own time to live, and the log shows one failure and one success. - With the design of this article, the row stays in
requestedafter step three, the backend waits, and it creates session B only if the applicant is still waiting. Session B is linked to the row, session A lapses, and the counters show one row with two creates, which a person can read. - If the applicant had opened the link of session A, two results could arrive for one row, and the rule that accepts one result per row decides which counts.
The trace does not claim that the platform behaves this way, because the documentation does not describe a gateway; it shows why the unknown outcome needs a rule.
A test plan
Retry code is the code that runs least often and matters most when it does. Test it with injected faults, in a staging setup, with test credentials and no live data.
- Drop the response. Let the call reach the server and cut the connection before the answer returns. Your code must end with one local applicant record, the ambiguous outcome recorded, no second create started while the first is unresolved, and at most one decision acted on. Because the create request carries no correlation field, an orphan session from the first call may still exist on the platform; the test passes when your recovery policy accounts for it, not when it is absent.
- Delay beyond the timeout. Make the server slow, so your client gives up while the server finishes. The row must stay in
requestedand the look-before-you-repeat path must run. - Return a
503with a retry hint. On a read the loop must wait at least as long as the hint; on a create or an upload it must reconcile before it repeats. - Return
409. The code must wait for the other request and must not count the refusal as a failed attempt of the applicant. - Return a success code with
statusoferror. The code must not store a result. - Return
400. There must be no retry and a visible error for the developer. - Kill the process between the call and the write of the
sessionId. After a restart the row must be recoverable and not duplicated. - Replay the same event twice. The decision must be applied once.
- Count. After the run, compare sessions created with applicant rows; the gap must be explained.
Where this sits in a verification flow
The platform creates a session from a scenario, takes the images your user provides through the widget or the REST API, and reports the result by polling the REST API or through webhooks. The retry rules in this article apply to the calls that start and feed that flow. The pages that matter are the REST API reference, the response format and errors and receiving webhooks. To discuss the behaviour of a repeated call on your account, contact the team.
Sources
All sources were fetched on 28 October 2026, except RFC 9110, fetched on 27 October 2026.
- The Idempotency-Key HTTP Header Field, draft-ietf-httpapi-idempotency-key-header-08, Internet Engineering Task Force HTTPAPI working group, 22 October 2026, an Internet-Draft that expires on 25 April 2027; work in progress, not an RFC.
- RFC 9110, HTTP Semantics, Internet Engineering Task Force, published through the RFC Editor. Idempotent methods, automatic retries, and the status codes
408,409,503and504, and the Retry-After field. - RFC 9457, Problem Details for HTTP APIs, Internet Engineering Task Force, July 2023, published through the RFC Editor.
- Idempotent requests, Stripe API Reference.
- API idempotency, Adyen Docs.
- AIP-155: Request identification, Google API Improvement Proposals.
- Exponential Backoff And Jitter, AWS Architecture Blog.
- Response format and errors, Create a KYC session, Process images, Session status, Working with clientKey and Running AML checks, NeuroVision documentation.