My App

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": [ ... ]
}
FieldTypeDescription
successbooleanAlways false on errors
errorstringHuman-readable message
messagestringMirror of error — kept for compatibility so data.message still resolves
codestring (optional)Machine-readable code (e.g. NOT_FOUND, QUOTA_EXCEEDED)
detailsarray (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

CodeMeaningWhen It Occurs
200OKRequest succeeded
400Bad RequestValidation failed or invalid input (code: VALIDATION_ERROR)
401UnauthorizedMissing or invalid X-Api-Key
402Payment RequiredUsage quota exceeded (code: QUOTA_EXCEEDED)
403ForbiddenAction not permitted in the resource's current state, or missing LHDN credentials
404Not FoundResource does not exist or is not in your environment/organization
409ConflictUniqueness constraint violated (e.g. duplicate invoice code)
429Too Many RequestsAPI-key rate limit exceeded (code: RATE_LIMITED)
500Internal Server ErrorUnexpected server error (code: INTERNAL_ERROR)
502Bad GatewayUpstream LHDN failure (code: LHDN_ERROR / SUBMISSION_FAILED)

Common error codes

codeTypical statusMeaning
VALIDATION_ERROR400Request body/query failed schema validation
NOT_FOUND404Resource not found in your scope
CODE_EXISTS409Invoice code already taken in your company, in test or live; details.invoiceId names the holder and details.environment says where
CONFLICT409Other duplicates (adjustment-note code, company name)
INVOICE_NOT_VALID / NOTE_IN_PROGRESS / OVER_CREDIT / DUPLICATE_CODE409Credit-note gate refusals; see Issue a credit note
SELF_DEALING409Buyer or supplier TIN is your own company's
SYNC_IN_PROGRESS409Another company-wide sync-status run is active. Wait and retry.
CANCEL_FAILED403Cancel refused; details.reason: "CANCEL_WINDOW_EXPIRED" when past the 72-hour window
QUOTA_EXCEEDED402Plan submission quota reached
MISSING_CREDENTIALS403No LHDN credentials for the active environment
UPDATE_FORBIDDEN / DELETE_FORBIDDEN403Document is Submitted/Valid and can't be modified
NOT_READY / INVALID_TYPE400Invoice not ready, or a PENDING_FINALIZE_* type can't be submitted
SUBMISSION_FAILED / LHDN_ERROR502Transient 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: a PENDING_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 isCompanyReady before invoice operations — many errors stem from incomplete setup.
  • Validate codes before creating documents to avoid 409.
  • Parse details[].path on validation errors to give users specific feedback.
  • Retry 500/502 with exponential backoff; do not retry 401/403/402 — they need manual action.

On this page