Documentation
Errors
Every error has the same shape, a stable machine-readable code, and — where we can work it out — a hint that says what to do about it.
The error envelope
Errors always come back as a single error object. Branch on code, which is stable within an API version. Do not branch on message, which we reword whenever we find a clearer way to say it.
{
"error": {
"type": "invalid_request_error",
"code": "validation_failed",
"message": "The request could not be processed. See details.",
"request_id": "req_01J9ZT4N8K3M",
"doc_url": "https://easarctech.com/docs/errors#validation_failed",
"details": [
{
"field": "line_items[0].hsn",
"code": "invalid_hsn",
"message": "HSN 5407 requires 6 digits for your turnover slab. Use 540752."
},
{
"field": "line_items[1].quantity.uom",
"code": "invalid_enum",
"message": "'mtr' is not a valid UQC code. Did you mean 'MTR'?"
},
{
"field": "due_on",
"code": "date_in_past",
"message": "due_on must be on or after ordered_on."
}
]
}
}type— a broad family:authentication_error,invalid_request_error,rate_limit_error,api_error.code— the specific, stable identifier to switch on.request_id— always present. Quote it to support and we can find the exact request in seconds.details— present on validation failures, one entry per field, and we return all of them rather than stopping at the first.
HTTP statuses
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 204 | Success, no body — deletions and acknowledgements |
| 400 | The request itself is malformed |
| 401 | Not authenticated |
| 403 | Authenticated, but not permitted |
| 404 | No such object, or not yours |
| 409 | Conflicts with the current state of the resource |
| 413 | Body larger than 1 MB |
| 422 | Well-formed, but the values cannot be accepted |
| 429 | Rate limited |
| 500 | Our bug |
| 503 | Temporarily unavailable, including upstream government portals |
| 504 | Upstream timeout |
Every error code
| Code | Status | Meaning | Retry? |
|---|---|---|---|
| missing_credentials | 401 | No Authorization header was sent | No |
| invalid_api_key | 401 | The key does not exist or has been revoked | No |
| expired_token | 401 | The OAuth access token has expired — refresh it | After refresh |
| insufficient_scope | 403 | The credential is valid but lacks the required scope | No |
| environment_mismatch | 403 | A test key was used against production, or the reverse | No |
| account_suspended | 403 | The account is suspended, usually for non-payment | No |
| resource_not_found | 404 | No object with that id, or it belongs to another account | No |
| method_not_allowed | 405 | Wrong HTTP verb for this path | No |
| invalid_request | 400 | Malformed JSON, or a query parameter that will not parse | No |
| unknown_field | 400 | A field we do not recognise — usually a typo | No |
| validation_failed | 422 | The request parsed but the values are not acceptable | No |
| duplicate_reference | 409 | An order already exists with that reference | No |
| order_not_cancellable | 409 | The order has an invoice with an active IRN | No |
| stage_out_of_sequence | 409 | That stage cannot follow the current one | No |
| idempotency_key_reused | 409 | The same key was sent with a different request body | No |
| eway_bill_active | 409 | Cancel the e-way bill before cancelling the IRN | No |
| irn_window_expired | 422 | More than 24 hours since IRN generation — issue a credit note | No |
| irp_rejected | 422 | The government IRP rejected the invoice; see irp_errors | After fixing |
| irp_unavailable | 503 | The IRP is down or timing out | Yes |
| rate_limited | 429 | Too many requests — honour Retry-After | Yes |
| payload_too_large | 413 | Request body above 1 MB | No |
| internal_error | 500 | Our fault. Quote the request_id if it persists | Yes |
| service_unavailable | 503 | Deploying or briefly degraded | Yes |
| gateway_timeout | 504 | An upstream took too long | Yes |
GST e-invoice failures
When the IRP rejects an invoice we return its codes verbatim under irp_errors, because your CA will recognise them. Where we can tell what went wrong, we add a hint in plain language.
{
"error": {
"type": "invalid_request_error",
"code": "irp_rejected",
"message": "The Invoice Registration Portal rejected this invoice.",
"request_id": "req_01J9ZT6P2R9V",
"irp_errors": [
{
"code": "2172",
"message": "For inter-state supply, IGST must be charged. CGST and SGST are not applicable."
}
],
"hint": "place_of_supply is 27 (Maharashtra) but the tax lines carry CGST and SGST. Set the tax type from place_of_supply, or correct the ship-to address."
}
}irp_rejected is never worth an automatic retry: the same payload will be rejected again. irp_unavailable is the opposite — the portal is simply down, and the request should be queued and retried.
Handling errors in practice
import { EasarcError } from '@easarc/sdk'
try {
await easarc.orders.invoice(orderId, { generate_eway_bill: true })
} catch (error) {
if (!(error instanceof EasarcError)) throw error
switch (error.code) {
case 'irp_rejected':
// A human has to fix the data. Surface the hint, do not retry.
await flagForReview(orderId, error.irpErrors, error.hint)
break
case 'irp_unavailable':
case 'rate_limited':
// Transient. The SDK has already retried; queue it for later.
await retryQueue.add({ orderId }, { delay: 60_000 })
break
case 'duplicate_reference':
// Already registered — reconcile instead of creating a second one.
await reconcileExisting(error.details?.[0]?.existing_id)
break
default:
throw error
}
}- Retry only
429,500,503and504, with exponential backoff and jitter. Retrying a422just produces the same422. - Always send an
Idempotency-Keyon writes, so a retry after a timeout cannot create a second order. - Log
request_idalongside your own correlation id. It is the single most useful thing in a support conversation. - Treat
irp_rejectedas work for a person, not for a queue. Somebody has to correct an HSN or an address.