Errors
Lira uses consistent error shapes and standard HTTP status codes across all endpoints. This page is the central reference for everything that can go wrong and how to handle it.
Error response format
All errors from Lira follow this standard shape:
{
"message": "Human-readable description of the error",
"code": "MACHINE_READABLE_CODE",
"statusCode": 422,
"action": {
"type": "CONTACT_ADMIN",
"hint": "Ask your administrator to enable NG Account Number verification in the Lira dashboard."
}
}| Field | Type | Description |
|---|---|---|
message | string | A human-readable explanation of the error. |
code | string | A machine-readable code for programmatic error handling. |
statusCode | number | The HTTP status code, mirrored in the response body for convenience. |
action | object | Optional. Present when the error is recoverable and there is a clear next step. See Actionable errors below. |
action.type | string | A machine-readable hint type: one of the values in the table below. |
action.hint | string | A human-readable sentence describing what to do. Safe to display directly to users. |
Actionable errors
When action is present in an error response, your application can use action.type to decide how to respond, either routing the user to the right place or surfacing the action.hint text directly.
action.type | Meaning | What to do |
|---|---|---|
CONTACT_ADMIN | An administrator needs to configure something (enable a service, set a price) before this call can succeed | Surface action.hint to the user or display a "Contact your admin" prompt |
TOP_UP_WALLET | The organization's wallet balance is too low to process the request | Redirect to the billing/wallet page, or surface action.hint |
CHECK_API_KEY | The API key is missing, invalid, or revoked | Prompt the user to check their API key configuration |
FIX_INPUT | The request body contains invalid or missing fields | Surface the validation details from error.details alongside action.hint |
RETRY_LATER | A transient upstream or infrastructure issue; retrying will likely succeed | Retry with exponential backoff; surface action.hint if retries are exhausted |
USE_NEW_KEY | The Idempotency-Key was already used with a different request body | Generate a new unique key and retry |
WAIT_AND_RETRY | A rate or brute-force limit is active; the caller must wait before retrying | Back off for the duration shown in Retry-After header or in action.hint |
Example: handling action in JavaScript:
const res = await fetch('https://api.lira.com/api/v1/verify/account', { ... });
const body = await res.json();
if (!res.ok) {
if (body.error?.action) {
switch (body.error.action.type) {
case 'CONTACT_ADMIN':
showAdminPrompt(body.error.action.hint);
break;
case 'TOP_UP_WALLET':
redirectToWallet();
break;
case 'RETRY_LATER':
case 'WAIT_AND_RETRY':
scheduleRetry();
break;
default:
showErrorMessage(body.error.action.hint);
}
} else {
showErrorMessage(body.error.message);
}
}HTTP status codes
| Status | Name | When it occurs |
|---|---|---|
400 | Bad Request | Malformed request body or invalid JSON |
401 | Unauthorized | Missing, invalid, or expired authentication credential |
402 | Payment Required | Insufficient wallet balance, wallet suspended, or service price not configured |
403 | Forbidden | Valid authentication but insufficient role, permissions, or the requested service is not enabled for this organization |
404 | Not Found | The requested resource (webhook, verification, API key) does not exist |
409 | Conflict | Idempotency key conflict, or the same idempotent request is still processing |
422 | Unprocessable Entity | The request is valid JSON but fails validation: missing required field, wrong format, etc. |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Unexpected server error; if persistent, contact support with the request ID |
Verification error codes
These codes appear in the error.code field of a verification response when status is failed or error. They are returned with an HTTP 200 OK: the request was processed successfully, but the verification itself did not pass.
| Code | Verification type | Meaning | Recommended action |
|---|---|---|---|
ACCOUNT_NOT_FOUND | Bank account | Account number not found at the specified bank | Ask the user to check their account number and bank code |
INVALID_BANK_CODE | Bank account | The bank code is not recognized | Check the bank code against the supported banks list |
INVALID_ACCOUNT_NUMBER | Bank account | Account number format is invalid | Validate the format before submitting (10 digits for Nigeria) |
SUBSCRIBER_NOT_FOUND | Phone number | Phone number is not registered with the carrier | Ask the user to verify their number |
INVALID_PHONE_NUMBER | Phone number | Phone number format is invalid | Validate E.164 or local format before submitting |
INVALID_NETWORK_CODE | Phone number | Network code is not recognized for the given country | Check the supported network codes in Verify a Phone Number |
NETWORK_CODE_REQUIRED | Phone number | Ghana verifications require a networkCode | Add the networkCode field to the request |
PROVIDER_ERROR | Both | The upstream registry returned an unexpected error | Retry with exponential backoff |
PROVIDER_TIMEOUT | Both | The upstream registry did not respond within the timeout window | Retry; this is a transient error |
Note
A 200 OK does not mean the verification succeeded. Always check the status field in the response body. A status of failed is a valid, expected outcome. It means the account or number was not found, not that the API call itself failed.
Idempotency error codes
Verification POST endpoints and address verification job creation require an Idempotency-Key header so retries do not create duplicate verifications, jobs, or charges.
| Code | HTTP status | Meaning | Recommended action |
|---|---|---|---|
IDEMPOTENCY_KEY_REQUIRED | 422 | Missing Idempotency-Key header | Generate a unique key for this verification or address job request and retry |
IDEMPOTENCY_KEY_INVALID | 422 | Key is empty, too long, or contains unsupported characters | Use 1-255 printable ASCII characters |
IDEMPOTENCY_IN_PROGRESS | 409 | The same key is already processing | Wait briefly, then retry with the same key |
IDEMPOTENCY_KEY_CONFLICT | 409 | The same key was used with a different request body | Generate a new key for the new request body |
Webhook error codes
Webhook-related errors are returned as standard HTTP error responses when creating or updating a webhook. The status field on a webhook object reflects its delivery health.
| Webhook status | Meaning |
|---|---|
active | Webhook is enabled and will receive deliveries |
inactive | Webhook has been manually disabled |
failed | All retry attempts for the most recent delivery were exhausted |
| Condition | HTTP status | Action |
|---|---|---|
| All retry attempts exhausted | None (webhook status set to failed) | PATCH the webhook status back to active to resume deliveries |
| Invalid or non-HTTPS URL on create or update | 422 | Provide a valid https:// URL that is publicly reachable |
| Webhook not found | 404 | Check the WEBHOOK_ID: use GET /client/webhooks to list all webhooks |
Handling errors in your code
The example below shows a verification request that returns a failed result and how to handle it:
curl -X POST https://api.lira.com/api/v1/verify/account \
-H "X-API-Key: YOUR_API_KEY" \
-H "Idempotency-Key: YOUR_IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountNumber": "0000000001",
"country": "NG",
"bankCode": "000014"
}'Response 200 OK (note: HTTP 200, but verification failed)
{
"id": "a1b2c3d4-...-...-...-...",
"status": "failed",
"verified": false,
"error": {
"code": "ACCOUNT_NOT_FOUND",
"message": "No account found for the provided details"
}
}In your code, always branch on status first, then on error.code:
const result = await response.json();
if (result.status === 'success') {
// proceed: read accountName, bankName, etc.
console.log('Verified:', result.accountName);
} else if (result.status === 'failed') {
// handle based on error.code
switch (result.error.code) {
case 'ACCOUNT_NOT_FOUND':
// prompt user to recheck their account number and bank code
break;
case 'INVALID_BANK_CODE':
// show bank code validation error
break;
case 'SUBSCRIBER_NOT_FOUND':
// prompt user to verify their phone number
break;
default:
// unknown failure: log and surface a generic user message
}
} else if (result.status === 'error') {
// transient failure, retry with exponential backoff
} else if (result.status === 'pending') {
// async mode: wait for webhook or poll GET /verify/:id
}