Error Codes

All SignCare APIs return a consistent error shape. Here's how to handle each case.

Seeing an error code you don't recognise? This page is the full list: HTTP statuses below, and the statusCode values verification APIs return in the body under Verification result codes. You can also paste the error message into 🤖 Ask AI.

Standard error envelope

Most responses, successful or not, use the same envelope. Read success and statusCode in the body, not only the HTTP status: many failures arrive with HTTP 200 (see Verification result codes).

{
  "success": false,
  "statusCode": 400,
  "message": "Human-readable description of what went wrong.",
  "data": null,
  "messages": [],
  "request_id": "...",
  "isBillable": false
}

isBillable tells you whether this request was charged: true if it was, false if not. Errors are never charged, so they always show false. Flows that finish later (ITR Fetch, SHCIL eStamp, DigiLocker, eSign) show false in their first response, because any charge is made when the result is ready. For eSign, /esign/request shows false, and each signer is charged when they complete signing. For DigiLocker, the credit is deducted from your wallet when the link is created, but that response shows false because the charge is not final yet. If the link expires before the user completes it, the credit is refunded automatically. See the ITR Fetch and eStamp pages.

Two failures use a different shape, because they are answered before your request reaches an API:

  • Missing or invalid credentials (401): { "success": false, "error": "Unauthorized", "message": "Missing or invalid X-API-KEY header." }. See 401.
  • An unexpected error on our side (usually 500): { "statusCode": 500, "messages": [], "exception": "...", "errorId": "...", "supportMessage": "Provide the ErrorId ... to the support team for further analysis." }. See 500.

Always log the errorId and request_id when they are present. Our support team uses them to trace the request in our logs.

HTTP status code reference

StatusMeaningWhat you should do
200SuccessProcess the response body
400Bad RequestFix validation errors and retry
401UnauthorizedCheck your X-API-KEY / X-API-APP-ID headers
402Payment RequiredInsufficient credits — top up via portal
403ForbiddenEndpoint not enabled for your account
404Not FoundCheck URL path and input identifiers
408Request TimeoutThe call may still have been processed: for a call that charges, check the result before sending it again (see Idempotency)
410GoneResource expired (e.g. eSign link), create a new one
422Unprocessable EntitySemantic validation failed (e.g. invalid PAN format)
429Too Many RequestsRefused before anything was processed: wait, then send it again — see Rate Limits
500Internal Server ErrorReads (GET): retry with backoff. Calls that charge: check the result first; contact support with the errorId
502Bad GatewayReads: retry with backoff. Calls that charge: check the result first
503Service UnavailableReads: retry after a few minutes. Calls that charge: check the result first
504Gateway TimeoutThe call may still finish: check the result (or poll the async status endpoint) before sending it again

SignCare does not deduplicate requests. A POST you send again is processed, and charged, again — even with the same request_id. After a timeout or a 5xx the first call may already have succeeded, so check its result (status endpoint, webhook or usage on the console) before resending. Retrying a GET or a 429 is always safe. See Idempotency.

eSign endpoints: read the result from the body. The eSign endpoints (/api/v1/esign/request, /status, /revoke, /audittrail/{documentId}) return { "success", "statusCode", "message", "data" }. The HTTP status matches statusCode for 400, 401, 403 and 500, but other values arrive with HTTP 200: for example 402 (insufficient balance) and 404 ("No matching records were found." on /status, "Document not found for the provided DocumentId." on /audittrail). Always check success and statusCode in the body. For a signing attempt that failed, see failure in the eSign webhook and status.

Common error scenarios

401 — Missing or invalid X-API-KEY header

{
  "success": false,
  "error": "Unauthorized",
  "message": "Missing or invalid X-API-KEY header."
}

Cause: One or both auth headers are missing, empty, or misspelled.

Fix: Confirm your request includes both X-API-KEY and X-API-APP-ID with non-empty values.

400 — Validation error

{
  "success": false,
  "statusCode": 400,
  "message": "",
  "data": null,
  "messages": [
    "The PanNumber field is required.",
    "Timestamp: 2026-04-21 10:15:02 IST",
    "Client IP: 203.0.113.10",
    "User-Agent: ...",
    "Application ID: app_xyz789",
    "Request: POST /api/v1/pan/verify",
    "Note: This validation error was triggered by a request originating from your system."
  ],
  "request_id": null
}

Cause: The request body is missing, is not valid JSON, or a field is missing or has the wrong type or format.

Fix: Read messages. The first lines name each problem (for example The PanNumber field is required. or Invalid value provided for field 'mobile'.); the lines after them (Timestamp, Request, ...) are context for your logs and for support. Fix the input before sending again. Nothing was processed or charged.

402 — Insufficient credits

{
  "success": false,
  "statusCode": 402,
  "message": "Insufficient wallet balance. Please recharge your wallet to continue using this API.",
  "data": null
}

Many endpoints send this with HTTP 200, so check statusCode in the body.

Cause: Your prepaid credit balance is below the cost of this endpoint.

Fix: Top up via the SignCare portal or contact your account manager to adjust your credit terms.

Verification result codes

Verification APIs (PAN, bank account, GST, UPI, mobile, IFSC and similar) answer with HTTP 200 whenever the provider gave an answer, including when that answer is "not verified". The outcome is in the body's statusCode, so always read it:

{
  "success": false,
  "statusCode": 103,
  "message": "No records found for the given ID or combination of inputs",
  "requestId": "...",
  "isBillable": true,
  "data": null
}
statusCodeMeaningWhat to do
100Valid authentication: the input was verifiedRead data
101Valid authentication on endpoints that do not use 100. On an endpoint that returns 100 for success, 101 means the provider answered but could not verify the inputOn those endpoints, show the user that the details could not be verified
102Invalid ID number or combination of inputsFix the input; don't retry as-is
103No records found for the given ID or combination of inputsAsk the user to check the details; don't retry as-is
104Max retries exceededStop retrying this input
105Missing consentSend consent and consentText
106Multiple records existNarrow the input (for example add a name or date of birth)
107Not supported for this inputDon't retry the same input
108Internal resource unavailableRetry later with backoff
109Too many records foundNarrow the input

Branch on statusCode, not success. Some endpoints return success: true with 103, because the lookup itself completed. A code not listed here is a provider-side failure. Sending the request again is a new call and may be charged like one (SignCare does not deduplicate requests), so retry at most once, after a backoff, and contact support with the requestId if it repeats.

Calls rejected before they reach the provider (400 validation, 401, 402, 403) are never charged.

Which results use a credit

Use isBillable to track credits. Every verification API response includes isBillable: true means this call used a credit, false means it did not. It always matches what was deducted from your wallet, so you do not need to work out charges from statusCode.

{
  "success": true,
  "statusCode": 100,
  "message": "Valid Authentication",
  "isBillable": true
}

The tables below explain when a call is charged. A verification call is charged when the source was actually searched and answered, including when the answer is "no record found". A call that never reached the source, or that the source did not answer, is not charged.

ResultCharged?
100 / 101: verifiedYes
102 / 103: invalid input or no record, after the source searchedYes, on APIs where the source completes a search, for example Driving Licence
104 max retries, 106 multiple records, 109 too many recordsNo
400, 401, 402, 403, 429No, never
500, 502, 503, or no answer from the sourceNo
Your client timed out, or you received 504The call may still have finished and been charged; check its result before sending it again

Driving Licence returns 101 when the licence is found and 103 for every other outcome. A 103 after a completed search (no record, or invalid DL number / date of birth) is charged. A 103 returned because the source was temporarily unavailable is not. isBillable tells you which one you got.

Multi-step flows are charged once, at the step that starts or completes the work:

FlowChargedNot charged
OTP verification (for example mobile)The verify stepSending the OTP
Aadhaar OVSE, Offline Aadhaar KYCEach init call: it starts a new verificationOTP verify and status calls
DigiLockerOne credit, deducted up front when the link is created (isBillable: false in that response) and made final on the first document fetch (isBillable: true). If the user shares more than one document, each additional document uses one more credit at that fetchLinks that expire before the user completes them: the credit is refunded to your wallet automatically. Fetching the same documents again is not charged
eSignEach signer, when they complete signingCreating the request (/esign/request shows isBillable: false), status calls, signers who never sign

For calls made before isBillable was available, open Usage in the SignCare Console and use the Billable filter. Your wallet history lists every debit and refund.

ITR Fetch errors

ITR Fetch (POST /api/v1/itr/requests) answers in the same call when the outcome is known within about 25 seconds, and by webhook or status API otherwise. In both places the failure has an error_code and the same HTTP status (in the status API and webhook it is the envelope's statusCode).

error_codeHTTPMeaning and what to do
INVALID_CREDENTIALS400The portal did not accept the user ID or password. Never retried automatically, because repeated wrong passwords lock the account. Ask the taxpayer to check the password on the portal, then submit again with a new reference_id
ACCOUNT_LOCKED403The portal locked the account. The taxpayer must unlock it on the portal first
OTP_REQUIRED / CAPTCHA_REQUIRED422The portal asked for something an automated fetch cannot provide
NO_RETURNS_FOUND404The login worked but no filed return exists for the period
PORTAL_UNAVAILABLE503The income tax portal is down or blocking logins. If retry_at is present, a new request before that time will most likely be blocked again
TIMEOUT504The fetch did not finish in time. Submit again with a new reference_id
INTERNAL_ERROR500Something went wrong on our side. Retry, and contact support if it repeats

Refused before anything starts. Nothing is queued and nothing is charged. 429 CREDENTIALS_RECENTLY_REJECTED means the portal rejected this taxpayer's password a short while ago (and, after two rejections in 24 hours, for up to a day), so sending it again could lock the account. 429 TOO_MANY_ATTEMPTS and TOO_SOON limit how often one taxpayer can be fetched. 403 ACCOUNT_LOCKED means the portal locked the account recently. 503 PORTAL_UNAVAILABLE means the portal is blocking us right now. The 429 and 503 refusals carry data.retry_at (UTC), the time a new request is accepted. 402 is not enough credits and 403 is not subscribed.

Delayed, not failed. If the portal blocks the login, the request stays PROCESSING and is retried automatically. The response (or status API) shows a notice and retry_at. You do not need to submit again.

Failed, timed-out, refused and delayed requests are never charged.

429 — Rate limit exceeded

See Rate Limits for the full guide on handling 429.

500 — Internal server error

{
  "statusCode": 500,
  "messages": [],
  "exception": "...",
  "errorId": "a1b2c3d4-e5f6-...",
  "supportMessage": "Provide the ErrorId a1b2c3d4-e5f6-... to the support team for further analysis."
}

Cause: A bug, unhandled edge case, or infrastructure issue on our side.

Fix: For a read (GET), retry once with backoff. For a call that charges, the request may have been processed before the error: check the result first (status endpoint, webhook, or usage on the console), and do not resend it automatically. If it persists, contact support@signcare.io with the errorId.

Provider-specific errors

When a government or financial provider answers that it cannot verify the input, you get a normal response, usually with HTTP 200, and the outcome in the body's statusCode and message. The codes are listed under Verification result codes, for example:

{
  "success": false,
  "statusCode": 103,
  "message": "No records found for the given ID or combination of inputs",
  "data": null
}

These are user-level errors — your user gave a bad input or the provider genuinely has no record. Show a friendly message to your user; do not retry the same input.

Designing your error handling

A robust client should handle these four classes differently:

ClassCodesStrategy
Authentication401, 403Surface to ops team; don't retry
Validation400, 422Surface to end user; don't retry
Rate limit429Back off + retry (refused before processing, so always safe)
Transient server408, 500, 502, 503, 504Reads (GET): retry 2-3 times with backoff. Calls that charge: check the result before sending again

Example pattern:

async function callSignCare(method, path, body) {
  const isRead = method === 'GET';
  for (let attempt = 1; attempt <= 3; attempt++) {
    let res;
    try {
      res = await fetch(`${BASE_URL}${path}`, {
        method,
        headers: {
          'X-API-KEY': API_KEY,
          'X-API-APP-ID': APP_ID,
          'Content-Type': 'application/json'
        },
        body: body === undefined ? undefined : JSON.stringify(body)
      });
    } catch (err) {
      // Network error: a read can be retried; a call that charges may already have been processed.
      if (isRead && attempt < 3) { await sleep(2 ** attempt * 500); continue; }
      throw err;
    }

    const data = await res.json().catch(() => ({}));
    if (res.ok && data.success !== false) return data;

    // 429 is refused before anything runs: safe to send again for any method.
    // 5xx: only reads are retried. SignCare does not deduplicate, so a resent POST can be charged twice.
    const retry = res.status === 429 || (isRead && res.status >= 500);
    if (!retry || attempt === 3) throw data;
    await sleep(2 ** attempt * 500);
  }
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));