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
| Status | Meaning | What you should do |
|---|---|---|
200 | Success | Process the response body |
400 | Bad Request | Fix validation errors and retry |
401 | Unauthorized | Check your X-API-KEY / X-API-APP-ID headers |
402 | Payment Required | Insufficient credits — top up via portal |
403 | Forbidden | Endpoint not enabled for your account |
404 | Not Found | Check URL path and input identifiers |
408 | Request Timeout | The call may still have been processed: for a call that charges, check the result before sending it again (see Idempotency) |
410 | Gone | Resource expired (e.g. eSign link), create a new one |
422 | Unprocessable Entity | Semantic validation failed (e.g. invalid PAN format) |
429 | Too Many Requests | Refused before anything was processed: wait, then send it again — see Rate Limits |
500 | Internal Server Error | Reads (GET): retry with backoff. Calls that charge: check the result first; contact support with the errorId |
502 | Bad Gateway | Reads: retry with backoff. Calls that charge: check the result first |
503 | Service Unavailable | Reads: retry after a few minutes. Calls that charge: check the result first |
504 | Gateway Timeout | The 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
}statusCode | Meaning | What to do |
|---|---|---|
100 | Valid authentication: the input was verified | Read data |
101 | Valid 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 input | On those endpoints, show the user that the details could not be verified |
102 | Invalid ID number or combination of inputs | Fix the input; don't retry as-is |
103 | No records found for the given ID or combination of inputs | Ask the user to check the details; don't retry as-is |
104 | Max retries exceeded | Stop retrying this input |
105 | Missing consent | Send consent and consentText |
106 | Multiple records exist | Narrow the input (for example add a name or date of birth) |
107 | Not supported for this input | Don't retry the same input |
108 | Internal resource unavailable | Retry later with backoff |
109 | Too many records found | Narrow 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.
| Result | Charged? |
|---|---|
100 / 101: verified | Yes |
102 / 103: invalid input or no record, after the source searched | Yes, on APIs where the source completes a search, for example Driving Licence |
104 max retries, 106 multiple records, 109 too many records | No |
400, 401, 402, 403, 429 | No, never |
500, 502, 503, or no answer from the source | No |
Your client timed out, or you received 504 | The 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:
| Flow | Charged | Not charged |
|---|---|---|
| OTP verification (for example mobile) | The verify step | Sending the OTP |
| Aadhaar OVSE, Offline Aadhaar KYC | Each init call: it starts a new verification | OTP verify and status calls |
| DigiLocker | One 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 fetch | Links that expire before the user completes them: the credit is refunded to your wallet automatically. Fetching the same documents again is not charged |
| eSign | Each signer, when they complete signing | Creating 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_code | HTTP | Meaning and what to do |
|---|---|---|
INVALID_CREDENTIALS | 400 | The 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_LOCKED | 403 | The portal locked the account. The taxpayer must unlock it on the portal first |
OTP_REQUIRED / CAPTCHA_REQUIRED | 422 | The portal asked for something an automated fetch cannot provide |
NO_RETURNS_FOUND | 404 | The login worked but no filed return exists for the period |
PORTAL_UNAVAILABLE | 503 | The 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 |
TIMEOUT | 504 | The fetch did not finish in time. Submit again with a new reference_id |
INTERNAL_ERROR | 500 | Something 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:
| Class | Codes | Strategy |
|---|---|---|
| Authentication | 401, 403 | Surface to ops team; don't retry |
| Validation | 400, 422 | Surface to end user; don't retry |
| Rate limit | 429 | Back off + retry (refused before processing, so always safe) |
| Transient server | 408, 500, 502, 503, 504 | Reads (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));