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_id is 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 and 5xx with 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

CategorySafe to send again?
Any call refused with 429Yes — nothing was processed
GET endpointsYes — reads are never charged
Status/PollAlways 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 initiationNo — 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_id you 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_id and errorId makes 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.