Idempotency
How to retry SignCare requests safely, without charging twice or creating duplicate workflows.
Why idempotency matters
Network failures happen. A client may think a request failed when it actually succeeded — leading to double-billing, duplicate workflows, or duplicated eSign requests.
SignCare does not deduplicate requests. Every request you send is processed, and charged if the endpoint charges, even when it repeats an earlier request exactly. Safe retries are your client's job; this page shows how.
Request correlation IDs
Most SignCare endpoints accept an optional request_id (sometimes requestId or txnId depending on the endpoint — see each endpoint reference) that you generate:
{
"pan_number": "ABCDE1234F",
"pan_holder_name": "JOHN DOE",
"consent": "Y",
"consent_text": "...",
"request_id": "YOUR-UNIQUE-ID-abc123"
}What a good request_id looks like
- Unique per call — generate a fresh one for every request you send.
- Format: UUID v4 is a safe default. A composite like
{user_id}:{operation_type}:{random}also works. - Meaningful to you — it is a correlation id: it lets you, and our support team, find one specific call.
What SignCare does with it
- It is a correlation id only. Log it with the time and outcome of every call, and quote it (with the
errorId, if any) when you contact support. - It does not deduplicate: a second request with the same
request_idis processed, and charged, again.
Example retry pattern
Retry only when it is safe:
429 Too Many Requests— the rate limiter refused the call before anything ran. Wait, then send it again.- Reads (
GET) and status endpoints — never charged; retry network errors and5xxwith backoff. - Anything else after a timeout, a network error or a
5xx— the first call may already have succeeded. Check the result first (the flow's status endpoint, its webhook, or usage on the console), and send again only if it was not processed.
async function verifyPan(panNumber, panHolderName, userId) {
const requestId = `pan:${userId}:${crypto.randomUUID()}`; // for your logs and support
for (let attempt = 1; attempt <= 3; attempt++) {
const res = await fetch(`${BASE_URL}/api/v1/pan/verify`, {
method: 'POST',
headers: {
'X-API-KEY': API_KEY,
'X-API-APP-ID': APP_ID,
'Content-Type': 'application/json'
},
body: JSON.stringify({
pan_number: panNumber,
pan_holder_name: panHolderName,
consent: 'Y',
consent_text: '...',
request_id: requestId
})
});
// Refused before processing: safe to send again.
if (res.status === 429 && attempt < 3) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
continue;
}
const body = await res.json().catch(() => ({}));
if (res.status >= 500) {
// Outcome unknown: this call may have been processed and charged. Do not resend automatically.
throw Object.assign(new Error('Outcome unknown; check before resending'), { requestId, body });
}
return body; // read success and statusCode in the body
}
}A network error thrown by fetch is the same case as a 5xx: the request may have reached SignCare, so do not resend it automatically.
What breaks idempotency
Because SignCare does not deduplicate, every request you send is a new request, whatever its request_id. Resending is safe only when you know the first call was not processed: it was refused with 429 (or 400, 401, 402, 403), or you checked and found no result.
Long-running operations
For async flows (eSign, eStamp, Aadhaar OVSE), the txnId returned on initiation identifies the flow. Calling the initiation endpoint again starts a new flow, and is charged again (for example, each Aadhaar OVSE /init call debits a credit). To check the state of a flow, use its status endpoint — you can call it repeatedly without side effects.
Example:
// 1. Start the flow — returns txnId. Do not call this again to "retry": it starts a new flow.
const init = await fetch('/api/v1/aadhaar-ovse/init', { ... });
const { txnId } = await init.json();
// 2. Poll the status — safe to call many times
while (true) {
const res = await fetch(`/api/v1/aadhaar-ovse/result/${txnId}`);
const body = await res.json();
if (!body.pending) return body;
await new Promise((r) => setTimeout(r, 2000));
}Safe-to-retry vs unsafe-to-retry endpoints
| Category | Safe to send again? |
|---|---|
Any call refused with 429 | Yes — nothing was processed |
| GET endpoints | Yes — reads are never charged |
| Status/Poll | Always safe |
| Verification POSTs (PAN, GST, Bank) | Only if you know the first call was not processed; otherwise you may be billed twice |
| eSign / OVSE / eStamp initiation | No — a new call creates a new flow and is charged; check the first flow's status |
| Webhooks (inbound to your side) | You must dedupe by the flow's transaction ID — see Webhooks — Idempotency on your side |
Tips
- Log every
request_idyou generate, with timestamp and outcome. Invaluable for debugging "was this charged or not?" - Put it in your support tickets. If you ask us to investigate, including the
request_idanderrorIdmakes it instant. - Keep your own record of what you sent. Store the outcome of each charged call against your own operation id, so your system never sends the same logical operation twice.