Error Handling
Error response format, HTTP status codes, and common error scenarios
The BizCARE Books e-Invoicing API uses conventional HTTP status codes and a consistent error envelope.
Error Response Format
All API errors use a flat envelope:
{
"success": false,
"error": "Human-readable error message",
"message": "Human-readable error message",
"code": "MACHINE_CODE",
"details": [ ... ]
}| Field | Type | Description |
|---|---|---|
success | boolean | Always false on errors |
error | string | Human-readable message |
message | string | Mirror of error — kept for compatibility so data.message still resolves |
code | string (optional) | Machine-readable code (e.g. NOT_FOUND, QUOTA_EXCEEDED) |
details | array (optional) | Present on validation errors — see below |
error and message always carry the same string. Read either. Branch on code for
programmatic handling.
Authentication failures at the API-key layer (missing/invalid/rate-limited key, or a key
whose owner is no longer an org member) use this same envelope with a code
(MISSING_API_KEY, INVALID_API_KEY, RATE_LIMITED, NO_ORG_USER, NOT_ORG_MEMBER) —
they just omit details.
HTTP Status Codes
| Code | Meaning | When It Occurs |
|---|---|---|
200 | OK | Request succeeded |
400 | Bad Request | Validation failed or invalid input (code: VALIDATION_ERROR) |
401 | Unauthorized | Missing or invalid X-Api-Key |
402 | Payment Required | Usage quota exceeded (code: QUOTA_EXCEEDED) |
403 | Forbidden | Action not permitted in the resource's current state, or missing LHDN credentials |
404 | Not Found | Resource does not exist or is not in your environment/organization |
409 | Conflict | Uniqueness constraint violated (e.g. duplicate invoice code) |
429 | Too Many Requests | API-key rate limit exceeded (code: RATE_LIMITED) |
500 | Internal Server Error | Unexpected server error (code: INTERNAL_ERROR) |
502 | Bad Gateway | Upstream LHDN failure (code: LHDN_ERROR / SUBMISSION_FAILED) |
Common error codes
code | Typical status | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | Request body/query failed schema validation |
NOT_FOUND | 404 | Resource not found in your scope |
CODE_EXISTS | 409 | Invoice code already taken in your company, in test or live; details.invoiceId names the holder and details.environment says where |
CONFLICT | 409 | Other duplicates (adjustment-note code, company name) |
INVOICE_NOT_VALID / NOTE_IN_PROGRESS / OVER_CREDIT / DUPLICATE_CODE | 409 | Credit-note gate refusals; see Issue a credit note |
SELF_DEALING | 409 | Buyer or supplier TIN is your own company's |
SYNC_IN_PROGRESS | 409 | Another company-wide sync-status run is active. Wait and retry. |
CANCEL_FAILED | 403 | Cancel refused; details.reason: "CANCEL_WINDOW_EXPIRED" when past the 72-hour window |
QUOTA_EXCEEDED | 402 | Plan submission quota reached |
MISSING_CREDENTIALS | 403 | No LHDN credentials for the active environment |
UPDATE_FORBIDDEN / DELETE_FORBIDDEN | 403 | Document is Submitted/Valid and can't be modified |
NOT_READY / INVALID_TYPE | 400 | Invoice not ready, or a PENDING_FINALIZE_* type can't be submitted |
SUBMISSION_FAILED / LHDN_ERROR | 502 | Transient failure talking to LHDN |
The full set of codes per endpoint is in the API Reference.
Validation Errors (400)
details is an array of field errors — each { path, message } (plus optional summary,
value):
curl -X POST https://api.bizcare.my/api/v1/invoices \
-H "X-Api-Key: biz_test_your-api-key" \
-H "Content-Type: application/json" \
-d '{"type": "INVOICE"}'{
"success": false,
"error": "Validation failed",
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"details": [
{ "path": "invoiceCode", "message": "Required" },
{ "path": "invoiceDateTime", "message": "Required" },
{ "path": "lineItems", "message": "Required" }
]
}Each details[].path is the request body field name. Use it to display targeted errors or
to programmatically fix and retry. The message text varies, so branch on path, not on the
message.
Quota Errors (402)
The API is metered. When your plan's submission quota is exhausted, invoice create and submit
calls fail with (details is present on every such 402):
{
"success": false,
"error": "Usage limit reached",
"message": "Usage limit reached",
"code": "QUOTA_EXCEEDED",
"details": { "currentUsage": 500, "limit": 500, "planName": "Standard" }
}Quota is enforced on the live environment only — the test environment does not count
against it. Ensure your organization is on an adequate plan before going live.
Forbidden Errors (403)
Returned when authenticated but the action isn't allowed. Deleting a submitted invoice:
{
"success": false,
"error": "A submitted invoice cannot be deleted",
"message": "A submitted invoice cannot be deleted",
"code": "DELETE_FORBIDDEN"
}Missing LHDN credentials for the key's environment returns code: MISSING_CREDENTIALS.
You cannot delete invoices or adjustment notes submitted to LHDN (status Submitted or
Valid). To reverse a valid invoice, issue a credit note or cancel it.
Not Found Errors (404)
{
"success": false,
"error": "Company not found",
"message": "Company not found",
"code": "NOT_FOUND"
}"Company not found" usually means no company is registered for your account in this key's environment. Complete company setup in the dashboard before other calls.
The status is not the same on every route. Always branch on code: NOT_FOUND, not on the HTTP
status. POST /invoices, POST /submitted-documents/sync-status and POST /validation/invoice-code
return 400. Most other routes return 404. GET /companies is the exception: it returns 200
with data: null.
Conflict Errors (409)
Creating an invoice whose code is already taken returns CODE_EXISTS, naming the invoice that
holds it:
{
"success": false,
"error": "Invoice code already exists for this company",
"message": "Invoice code already exists for this company",
"code": "CODE_EXISTS",
"details": { "invoiceId": "f7e6d5c4-b3a2-1098-7654-321fedcba098", "environment": "test" }
}Invoice codes are unique per company for all time (no year component) and across both
environments. A test invoice with code 1001 blocks code 1001 in live. Check
details.environment to see which one holds the code. Send an
Idempotency-Key on every create: a retry with the same key returns the original invoice
(200, idempotentReplay: true) instead of a 409, so a 409 always means a different
document holds the code. Other duplicates (adjustment-note code, company name) return
code: CONFLICT. Check availability first with POST /api/v1/validation/invoice-code or
/validation/adjustment-note-code.
Cancel Errors
A cancel request must send a non-empty reason in the body ({ "reason": "Wrong buyer" }).
Without it you get 400 VALIDATION_ERROR.
Cancelling is allowed only within 72 hours of submission. Past that window the request is
refused with 403, code: "CANCEL_FAILED" and details: { "reason": "CANCEL_WINDOW_EXPIRED" }
(branch on details.reason; the message text may change). Issue a credit note instead.
Other refusals are also 403 CANCEL_FAILED, without details.reason: the document was never
submitted, it is already cancelled or invalid, or the invoice still has active adjustment notes.
Submission Errors
Submitting to LHDN can also fail with:
- Not ready —
code: NOT_READY(missing required buyer/supplier/line-item data). - Wrong type —
INVALID_TYPE: aPENDING_FINALIZE_*invoice can't be submitted directly. - Transient LHDN failure —
SUBMISSION_FAILED(502); retry after a brief delay.
Best Practices
- Branch on
code, not on message text — messages may change; codes are stable. - Check
isCompanyReadybefore invoice operations — many errors stem from incomplete setup. - Validate codes before creating documents to avoid
409. - Parse
details[].pathon validation errors to give users specific feedback. - Retry
500/502with exponential backoff; do not retry401/403/402— they need manual action.