SignCareAPI Docs

Idempotency

How to safely retry SignCare requests 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. Idempotency lets you safely retry.

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": "ABCDE1234F",
  "consent": "Y",
  "consent_text": "...",
  "request_id": "YOUR-UNIQUE-ID-abc123"
}

What a good request_id looks like

  • Unique per logical operation — generate fresh for each new operation, but reuse on retries of the same operation.
  • Opaque to SignCare — we don't parse it; just compare bytes.
  • Format: UUID v4 is a safe default. A composite like {user_id}:{session_id}:{operation_type}:{random} also works.
  • Length: Up to 128 characters.

What SignCare does with it

  • If we've already processed a request with the same request_id from your API Key within the idempotency window, we return the same response without re-executing.
  • The idempotency window is 24 hours for most endpoints.
  • The response is cached server-side; your duplicate call doesn't hit the upstream provider or your billing.

Example retry pattern

async function verifyPan(pan, userId) {
  // Generate request_id once per logical operation
  const requestId = `pan:${userId}:${crypto.randomUUID()}`;
 
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      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,
          consent: 'Y',
          consent_text: '...',
          request_id: requestId   // ← SAME on every retry
        })
      });
      if (res.ok) return res.json();
      if (res.status < 500) throw await res.json();
    } catch (err) {
      if (attempt === 2) throw err;
      await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
    }
  }
}

Key principle: generate the ID once at the start of the operation, reuse it on every retry.

What breaks idempotency

Your retry will be treated as a fresh request (and potentially charged again) if:

  • You generate a new request_id on each retry.
  • You change any field in the request body between retries.
  • You retry after the idempotency window has expired.
  • You retry from a different API Key.

Long-running operations

For async endpoints (eSign, eStamp, Aadhaar OVSE), idempotency is handled by the txnId returned on initiation. To check the state of an operation, use the dedicated status endpoint for that flow — you can call it repeatedly without side effects.

Example:

// 1. Start the flow — returns txnId
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 retry without request_id?
GET endpointsYes — GETs are always idempotent
Verification POSTs (PAN, GST, Bank)Yes with request_id; No without — you may be billed twice
eSign initiationYes with request_id; otherwise creates duplicate flow
Status/PollAlways safe
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 makes it instant.
  • Don't reuse across environments. A stage request_id will not dedupe against a live one even if the string matches.

On this page