API Reference
The complete, always-current endpoint reference — generated from the live API
The full endpoint reference — every route, request body, response schema, enum, and error — is published as an OpenAPI 3.0 specification generated directly from the running API. Because it is generated from the same code that serves your requests, it never drifts out of sync.
Interactive API Reference
Browse every endpoint with schemas and try-it-out (Scalar UI).
OpenAPI Spec (JSON)
Machine-readable spec — import into Postman, generate a client, etc.
Prefer the interactive reference for exact field names, casing, required/optional flags, and response shapes. This guide site covers concepts and workflows; the OpenAPI spec is the authoritative reference.
Conventions
- Host and base path:
https://api.bizcare.my, every endpoint under/api/v1. This is the only API host. - Auth: send your organization API key in the
X-Api-Keyheader. Keys are environment-bound — see Authentication. - Casing: request and response bodies are camelCase throughout.
- Envelopes: success responses are
{ success: true, data, message?, pagination? }; errors are{ success: false, error, message, code?, details? }— see Error handling. - Pagination: list endpoints accept
?page=&per_page=(per_pageis 1 to 1000, default 10) and return apaginationobject (sibling ofdata) withpage,perPage,total,totalPages,hasNextPage,hasPrevPage.GET /webhooks/:id/deliveriesis the exception. It useslimit(1 to 100, default 25) and abeforecursor. - Issue dates:
invoiceDateTime.dateandadjustmentNoteIssueDatemust be today or up to 3 days back, never in the future. See Issue dates.
Integration behaviour worth knowing
These are the contract points integrators most often need. The OpenAPI spec carries the per-field detail.
Idempotent create
POST /invoices accepts an Idempotency-Key header (1–128 visible ASCII characters).
Send a fresh key for each intended invoice and reuse it on every retry of that attempt.
- The key is scoped to your company and the key's environment (test and live never collide).
- A repeat returns
200with the invoice the first request created andidempotentReplay: true. It never creates a second invoice and never submits again. - If the repeat carries a different body, the existing invoice is returned unchanged and the new body is ignored — a key names one attempt, not a payload.
- The key is looked up before the invoice code, because a replay's code is held by the invoice it created.
- Deleting the invoice frees its key.
POST /invoices/bulktakes the same value per row asidempotencyKey; a replayed row is reported ascreatedwithidempotentReplay: true.
An idempotency key is not an external id. Use externalId (≤255 chars) and
externalIdPlatform (≤64) on create to store your record reference (an order or donation id).
They are filterable but deliberately not unique.
Duplicate invoice codes
Invoice codes are unique per company for all time (no year component). Creating an invoice
whose code is taken returns 409 with code: "CODE_EXISTS" and details.invoiceId — the id
of the invoice that holds it, and details.environment (test or live) saying where it lives.
The holder can be in the other environment: a test invoice with code 1001 blocks code 1001 in
live. A bulk row returns DUPLICATE_CODE with existingInvoiceId.
Because every create should carry an Idempotency-Key, a lost response is always a replay, so a
409 means a different document holds the code.
Reading LHDN references: latestSubmission
Invoice and consolidated-invoice objects carry environment and latestSubmission (or
null if never submitted): id, uuid, longId, status
(Submitted | Valid | Invalid | Cancelled), failReason, failDetails,
submissionUid, submittedAt, updatedAt, cancelledAt, cancelReason. One call gives you
the LHDN uuid, long id and rejection reason; no second request to submitted-documents is needed.
For an invoice folded into a consolidated invoice, latestSubmission is null — read the
status from the consolidated invoice.
failReason is LHDN's concise reason for an Invalid document (for example the innermost
validation error such as DR328 Reference Document is missing) and failDetails holds the
structured validation results.
List filters
GET /invoices:status,type,search,date_from,date_to(yyyy-mm-dd, Malaysian civil dates),invoiceCode(numeric),externalId,externalIdPlatform,consolidatedInvoiceId, pluspage/per_page.GET /adjustment-notes:view,status,type,search, pluspage/per_page.GET /consolidated-invoices:status,date_from,date_to. It has nosearchortype.
status matches the status string the API returns: Draft, Pending, Submitted, Valid,
Invalid, Cancelled or AWAIT_SUBMISSION_TO_LHDN. Unknown query parameters are ignored, not
rejected — check your parameter names against the spec.
Submitted documents
GET …/submitted-documents returns a typed record per LHDN submission (status, uuid,
longId, failReason, failDetails, cancellation audit). The raw base64 document and the raw
LHDN submit/status responses are no longer included on /api/v1. Use
GET /submitted-documents/export or GET /submitted-documents/:id/export when you need them.
How statuses stay fresh (polling)
You normally do not poll LHDN yourself. BizCARE:
- polls LHDN every 60 seconds after a submit while LHDN reports "in progress";
- runs a sweep every half hour that re-checks any document still
Submittedmore than 10 minutes after submission (up to 30 days back), so a lost job cannot strand a document; - pushes changes to your webhooks as soon as it sees them.
To force a check, call POST /submitted-documents/sync-status (all still-Submitted documents
of your company in the key's environment, oldest first, at most 20 per call; returns
{ checked, updated }) or POST /submitted-documents/:id/sync-status for one document.
Cancelling
An LHDN document can be cancelled only within 72 hours of submission. After that, the cancel
endpoints refuse with code: "CANCEL_FAILED" and details: { reason: "CANCEL_WINDOW_EXPIRED" };
issue a credit note instead. The cancel body needs a reason. See Cancel a document.
Plan quota (402)
402 QUOTA_EXCEEDED carries details: { currentUsage, limit, planName } on the invoice create
and submit routes. POST /invoices/bulk is different: it creates the invoices unsubmitted and reports
quota exhaustion in the 200 body (submission.error = "QUOTA_EXCEEDED" with the same fields).
Create an e-invoice request
POST /einvoice-requests records a buyer's request for an e-invoice against one of your invoice
codes. The request starts as Pending. Approve or reject it with PUT /einvoice-requests/:id.
curl -X POST https://api.bizcare.my/api/v1/einvoice-requests \
-H "X-Api-Key: $BIZCARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"invoiceCode": "INV-0001",
"buyer": {
"name": "Buyer Sdn Bhd",
"partyType": "business",
"tin": "C1234567890",
"registrationType": "BRN",
"registrationNumber": "201901000001",
"contactNumber": "0123456789",
"email": "buyer@example.com"
}
}'The values above are placeholders. Replace them with your own. A new request returns 201 with the
request in the same shape as GET /einvoice-requests/:id. If a Pending request for the same
invoiceCode already exists, a retry returns 200 with that request and idempotentReplay: true.
A request that was approved or rejected does not block a new one.
Monthly consolidation
A company whose monthlyConsolidationDay is unset is consolidated on day 7 — the default and
LHDN's statutory deadline — so no company is silently skipped. Consolidating with nothing to
consolidate is a no-op.
Endpoint groups
Each group below says what it is for and shows one call. The
OpenAPI spec has every field. All examples use
https://api.bizcare.my/api/v1 and an X-Api-Key header.
Adjustment notes
Credit, debit and refund notes that correct a Valid invoice. Create one with
POST /invoices/:id/adjustment-note (see Issue a credit note).
Everything after creation lives under /adjustment-notes.
| Method | Path | Purpose |
|---|---|---|
GET | /adjustment-notes | List (filters above) |
GET PUT DELETE | /adjustment-notes/:id | Read, update or delete. Update and delete work only before submission. |
POST | /adjustment-notes/:id/submit-to-myinvois | Submit to LHDN |
POST | /adjustment-notes/:id/cancel-latest-adjustment-note-submitted | Cancel (needs reason) |
GET | /adjustment-notes/:id/submitted-documents | LHDN submission history |
GET | /adjustment-notes/export, /adjustment-notes/:id/export | JSON export |
curl "https://api.bizcare.my/api/v1/adjustment-notes?status=Valid&per_page=20" \
-H "X-Api-Key: biz_test_your-api-key"Credit-note create refusals (INVOICE_NOT_VALID, NOTE_IN_PROGRESS, OVER_CREDIT,
DUPLICATE_CODE) are 409 responses. A submit refused because the parent invoice is no longer
Valid is also 409 INVOICE_NOT_VALID.
Consolidated invoices
One LHDN document that rolls up many small invoices. BizCARE creates them on a monthly schedule
(see Monthly consolidation). You read them here and can correct them
with notes. POST /companies/consolidate-now runs the consolidation immediately.
| Method | Path | Purpose |
|---|---|---|
GET | /consolidated-invoices | List (status, date_from, date_to) |
GET | /consolidated-invoices/:id | One consolidated invoice, with latestSubmission |
GET | /consolidated-invoices/:id/submitted-documents | LHDN submission history |
POST | /consolidated-invoices/:id/adjustment-notes | Create a note against it (plural, unlike the invoice route) |
GET | /consolidated-invoices/export, /consolidated-invoices/:id/export | JSON export |
curl "https://api.bizcare.my/api/v1/consolidated-invoices?status=Valid" \
-H "X-Api-Key: biz_test_your-api-key"Submitted documents
One record per LHDN submission, across invoices, notes and consolidated invoices. Use it to see
the LHDN uuid, longId and rejection reason, or to force a status check.
| Method | Path | Purpose |
|---|---|---|
GET | /submitted-documents | List all submissions |
GET | /submitted-documents/:id | One submission |
POST | /submitted-documents/sync-status | Re-check all Submitted documents (max 20 per call) |
POST | /submitted-documents/:id/sync-status | Re-check one |
GET | /submitted-documents/:id/ubl-totals | UBL totals for one submitted document |
GET | /submitted-documents/export, /submitted-documents/:id/export | Export with the raw documents |
curl -X POST https://api.bizcare.my/api/v1/submitted-documents/sync-status \
-H "X-Api-Key: biz_test_your-api-key"The company-wide sync returns 409 SYNC_IN_PROGRESS if another run is active. Wait and retry.
Exports
Export routes return a JSON file download (Content-Disposition: attachment). They exist for
invoices, adjustment notes, consolidated invoices and submitted documents. The list-wide route
is /export (for example GET /invoices/export). The single-document route is /:id/export.
GET /invoices/export returns at most 1000 invoices.
curl -o invoice.json https://api.bizcare.my/api/v1/invoices/f7e6d5c4-b3a2-1098-7654-321fedcba098/export \
-H "X-Api-Key: biz_test_your-api-key"Bulk create and bulk submit
POST /invoices/bulk creates many invoices in one call. Each row takes the same fields as
POST /invoices plus an optional idempotencyKey, and it never submits.
POST /invoices/bulk-submit then submits invoices you already created. Send up to 100 ids. The
invoices must be ready. The response has the same shape as a single submit.
curl -X POST https://api.bizcare.my/api/v1/invoices/bulk-submit \
-H "X-Api-Key: biz_test_your-api-key" \
-H "Content-Type: application/json" \
-d '{ "invoiceIds": ["f7e6d5c4-b3a2-1098-7654-321fedcba098"] }'Read data.accepted, data.rejected and each results[].outcome. HTTP 200 does not mean
LHDN accepted every invoice. Plan quota can fail the call with 402 QUOTA_EXCEEDED.
Cancel a document
Cancel a Valid or Submitted LHDN document within 72 hours of submission.
| Method | Path |
|---|---|
POST | /invoices/:id/cancel-latest-invoice-submitted |
POST | /adjustment-notes/:id/cancel-latest-adjustment-note-submitted |
The body must carry a non-empty reason:
curl -X POST https://api.bizcare.my/api/v1/invoices/f7e6d5c4-b3a2-1098-7654-321fedcba098/cancel-latest-invoice-submitted \
-H "X-Api-Key: biz_test_your-api-key" \
-H "Content-Type: application/json" \
-d '{ "reason": "Wrong buyer" }'Refusals are 403 CANCEL_FAILED. The cases are: the 72-hour window passed (details.reason is
CANCEL_WINDOW_EXPIRED), the invoice was never submitted, the document is already cancelled or
invalid, or the invoice still has active adjustment notes (cancel the notes first). A successful
cancel fires a document.cancelled webhook.
Usage
Shows how much of your plan quota the key's environment has used. test and live are counted
separately.
curl https://api.bizcare.my/api/v1/usage -H "X-Api-Key: biz_live_your-api-key"The response data has currentUsage, limit, planName, allowed and environment.
POST /usage/event records a usage event (action), and fails with 402 QUOTA_EXCEEDED when
the quota is full.
Reference data: MSIC codes
GET /reference/msic?q=software&page=1&perPage=10 searches LHDN MSIC business-activity codes.
Use it to find the msic value for a supplier. perPage is capped at 50.
curl "https://api.bizcare.my/api/v1/reference/msic?q=software" \
-H "X-Api-Key: biz_test_your-api-key"Validation helpers
Besides TIN validation, these pre-checks avoid a failed create.
| Method | Path | Result |
|---|---|---|
POST | /validation/invoice-code | Is this invoice code free? |
POST | /validation/adjustment-note-code | Is this note code free? |
POST | /validation/company-name | Is this company name free? Body { "name": "..." }. 200 with available: true, or 409 CONFLICT if taken. |
curl -X POST https://api.bizcare.my/api/v1/validation/company-name \
-H "X-Api-Key: biz_test_your-api-key" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Sdn Bhd" }'Credentials availability
GET /credentials/availability says whether pre-registered LHDN credentials are available to
assign. The response data is { available, count }.
curl https://api.bizcare.my/api/v1/credentials/availability \
-H "X-Api-Key: biz_test_your-api-key"E-invoice requests
Requests a buyer raises asking you for an e-invoice. You can create, list, read, change the status of, and delete them. See Create an e-invoice request.
| Method | Path | Purpose |
|---|---|---|
POST | /einvoice-requests | Create a Pending request |
GET | /einvoice-requests | List (page, per_page) |
GET | /einvoice-requests/:id | Read one |
PUT | /einvoice-requests/:id | Set status to Pending, Approve or Reject |
DELETE | /einvoice-requests/:id | Delete |
curl -X PUT https://api.bizcare.my/api/v1/einvoice-requests/REQUEST_ID \
-H "X-Api-Key: biz_test_your-api-key" \
-H "Content-Type: application/json" \
-d '{ "status": "Approve" }'Migrating from the v1 API? See the Migration guide.