Error responses
Error response format and common error scenarios
Kashier returns more than one error shape. Operational errors from the payment and processing services use the standard envelope below. Authentication, schema, and validation errors are produced before the request reaches a controller and return their own flat shapes instead. Branch your error handling on the HTTP status code first, then on which keys are present.
Standard error envelope (operational errors)
Most business-logic errors — invalid merchant configuration, invalid request objects, and similar — are emitted through a shared envelope, with the HTTP status the error itself carried:
{
"error": {
"cause": string // The technical cause of the error
},
"messages": {
"en": string, // English error message
"ar": string // Arabic error message
},
"status": "FAILURE" // Always "FAILURE" for this envelope
}Not every error uses this envelope
Authentication, authorization, schema, and request-validation failures return a flat { "message": ... }, an { "error": ... } string, or a { "more_info": [...] } array — with no messages object and no status field. Do not require status to be present before treating a non-2xx response as an error.
Common error scenarios
Invalid merchant ID
Occurs when attempting to use an incorrect or non-existent merchant identifier. This is an operational error, so it uses the envelope:
{
"error": {
"cause": "The request object is invalid"
},
"messages": {
"en": "This merchant is not found on kashier, example 'MID-XX-XX'",
"ar": "This merchant is not found on kashier, example 'MID-XX-XX'"
},
"status": "FAILURE"
}A separate check rejects a merchantId whose format is wrong — anything not matching MID-<digits>-<digits> — with HTTP 400 and an error string rather than the envelope:
{
"error": "Invalid merchantId format. It should be in the format MID-XXX-XXX"
}Missing or rejected authentication
These are returned by the authentication layer before your request reaches an endpoint, so they use flat shapes.
| Scenario | Status | Body |
|---|---|---|
No Authorization header sent | 403 | { "message": "No auth token provided" } |
| Credential rejected (bad, expired, or wrong user type) | 401 | { "error": "Authorization error", "message": "..." } |
| Secret key missing or malformed at the strategy level | 400 | { "message": "Missing token" } |
| Secret key cannot be decrypted | 401 | { "message": "Invalid secret key" } |
| Secret key caller IP not allow-listed | 403 | { "message": "Unauthorized IP address" } |
| Dashboard session no longer valid | 401 | { "message": "Please re-authenticate and try again" } |
| No user resolved for the credential | 401 | { "message": "Unauthorized" } |
Two of these are worth calling out because they are the most common real integration failures and neither returns the status people expect:
- A missing
Authorizationheader returns 403, not 401. Only an invalid credential returns 401. See Authentication. - IP allow-list rejection returns
403 { "message": "Unauthorized IP address" }. Every Secret Key call is checked against your merchant's IP allow-list after the credential itself has been validated. An empty allow-list permits all IPs, so this only bites once you have added at least one entry — and it looks identical to a permissions problem unless you read the message. Manage the list from API keys. Dashboard session (JWT) callers are not IP-checked.
Authorization failures — a valid credential whose role lacks the privilege the route requires — surface through the same 401 Authorization error shape, with a message such as Not authorized to perform this action.
Validation errors (400)
Two distinct validation layers can reject a request, and they return two distinct shapes. Check which one you're looking at before parsing.
Schema validation
Requests that fail request-schema validation (missing or malformed fields at the schema level) are caught before anything else and return only a list of the individual violations:
{
"more_info": [
{ "message": "should have required property 'amount'" }
]
}| Field | Type | Meaning |
|---|---|---|
more_info | array | The individual schema validation errors that were found. |
HTTP status: 400. There is no message, messages, or status key on this shape.
Field-level (body/query/param) validation
Most other input validation — required fields, format checks on individual body, query, or param values — returns a flat, localized message instead:
{
"message": "merchant ID is required"
}| Field | Type | Meaning |
|---|---|---|
message | string | The first validation error found, localized based on your request's language header. |
HTTP status: 400. Only the first error is returned even when several fields are invalid, so fix and re-send rather than expecting the full list.
Non-operational / unknown errors
An unexpected server-side failure — an unhandled exception rather than a recognized business error — does not use the envelope. It returns a flat message only:
{
"message": "Cannot read properties of undefined"
}The HTTP status is the error's own statusCode when it has one, and 500 otherwise. A service outage or acquirer downtime (a rare occurrence) surfaces here rather than through the standard envelope.
Live-mode-only routes
Some routes only operate in live mode and reject calls made with test credentials:
{
"message": "This API is only available on live mode"
}HTTP status: 403.
Duplicate / conflicting records (409)
Returned when a request would create a record whose unique field already exists — for example reusing an invoice number, a category name, or a payment-request reference for the same merchant:
{
"status": 409,
"message": "The invoice number has already been used. Please enter another invoice number.",
"code": "DUPLICATE_KEY_ERROR"
}Match on code: "DUPLICATE_KEY_ERROR" rather than on the message text, which varies by the conflicting field:
| Conflicting field(s) | Message |
|---|---|
| Invoice reference | The invoice number has already been used. Please enter another invoice number. |
| Category name (slug) | The category name has already been used. Please enter another category name. |
merchantId + referenceId (payment requests) | A payment request with merchantId "..." and referenceId "..." already exists. |
| Any other duplicate key | A duplicate key error occurred. Please check your input and try again. |
Retrying a 409 with the same payload will fail again. Change the conflicting value or treat the existing record as the result.
Other, non-duplicate database errors return a 400 instead. A Mongo-originated error carries an extra error: "MongoError" key alongside message; other downstream or generic errors return just { "message": "<description>" }.
Quick reference by source
The top-level keys are not uniform across error paths — some responses use error.cause / messages / status, others use a flat message, an error string, or a more_info array. Branch your client error handling on the HTTP status code together with whichever of these keys is present in the body.
| Error source | HTTP status | Top-level keys |
|---|---|---|
| Operational / business-logic error | varies (error's own status) | error.cause, messages.{en,ar}, status: "FAILURE" |
| Non-operational / unexpected error | error's own status, or 500 | message |
| Schema validation | 400 | more_info[] |
| Field-level validation | 400 | message |
Missing Authorization header | 403 | message |
| No user resolved from credentials | 401 | message |
| Missing/malformed secret-key token | 400 | message |
| Invalid secret key | 401 | message |
| Unauthorized IP address | 403 | message |
| Dashboard session expired | 401 | message |
| Other auth failure | 401 | error, message |
Invalid merchantId format | 400 | error |
| Live-mode-only route called with test credentials | 403 | message |
| Mongo / DB error | 400 | error: "MongoError", message |
| Duplicate key / conflicting record | 409 | status, message, code: "DUPLICATE_KEY_ERROR" |
Best practices
- Treat any non-2xx HTTP status as an error. Do not rely on a
statusfield being present — several error paths omit it - Read the message from
messages.enwhen present, and fall back to the flatmessageorerrorkey, then tomore_info - Handle both English and Arabic messages appropriately based on your user's locale
- Pay special attention to the
error.causefield for debugging purposes - For authorization errors, verify your API key, your hash generation process, and your IP allow-list
- Match 409 conflicts on
code: "DUPLICATE_KEY_ERROR"and do not blind-retry them - In live mode, ensure your account has the necessary credentials configured for each operation