Migrating from v1
What changed between the legacy v1 API and v2, and how to update your integration
This guide covers the differences an existing v1 integrator needs to handle. For the exact per-endpoint contract, always defer to the API Reference.
The database carries over — companies, invoices, submission history, and invoice codes are
the same records. This is a client swap, not a data migration. Your in-flight
PENDING_FINALIZE_* drafts survive the switch.
1. Base URL + versioned path
All endpoints are now under the /api/v1 prefix on https://api.bizcare.my, the only API
host. The old hosts v2.myinvois.bizcare.my and myinvois.bizcare.my are shut down and redirect
to bizcare.my. They no longer answer API calls.
2. API keys are reissued per environment
v1 keys do not carry over — they were issued by the old platform. Generate new keys in the dashboard. Each key is bound to a single LHDN environment and is prefixed accordingly:
biz_test_…— sandbox / pre-productionbiz_live_…— production
A test key can never read or write live data, and vice-versa. Send it in the X-Api-Key
header (case-insensitive). There is no separate publishable key — the API is server-to-server.
See Authentication.
3. Response casing is now camelCase (the main change)
v1 mixed casing: top-level scalar fields were snake_case while the embedded LHDN document
(buyer/lineItems/…) was camelCase. v2 is uniformly camelCase.
- Nested objects (
buyer,supplier,lineItems,legalMonetaryTotal,invoiceDateTime, …) are unchanged — they were already camelCase, identical to what you send on create. - Top-level scalars you read for status tracking are renamed snake → camel:
| v1 (snake) | v2 (camelCase) |
|---|---|
fail_reason | failReason |
fail_details | failDetails |
long_id | longId |
submission_uid | submissionUid |
is_submitted_to_lhdn | isSubmittedToLhdn |
invoice_code | invoiceCode |
created_at / updated_at | createdAt / updatedAt |
company_id, user_id, organization_id | companyId, userId, organizationId |
registration_number, registration_type | registrationNumber, registrationType |
tin_code | tinCode |
(Single-word fields — id, code, uuid, status, type — are unchanged.)
v1 also exposed both long_id and lhdn_long_id. v2 exposes a single longId — confirm
which value your status-sync consumed.
4. Error shape
v2 errors are a flat envelope: { success: false, error, message, code?, details? }.
errorandmessageare both the human-readable string (messagemirrorserrorfor compatibility, so v1 extraction likedata.message || …keeps working).codeis a machine-readable code (e.g.NOT_FOUND,VALIDATION_ERROR,QUOTA_EXCEEDED).detailson validation errors is an array of{ path, message }.
See Error handling.
5. Request payload renames (invoice create)
| v1 | v2 |
|---|---|
code (number) | invoiceCode (integer ≥ 1) |
issueDateTime (string) | invoiceDateTime { date, time } |
buyer.type | buyer.partyType |
buyer.registration { type, number } | flat buyer.registrationType, buyer.registrationNumber |
buyer.address.line1 / line2 / line3 | address.addressLine0 / addressLine1 / addressLine2 |
buyer.address.city | address.cityName |
buyer.address.zipCode | address.postalZone |
submitImmediately is now optional (defaults to not submitting). type is unchanged and still
includes PENDING_FINALIZE_INVOICE — the non-TIN → consolidation flow is preserved.
6. Endpoint changes
GET /companiesreturns a single company object (ornull), not an array.POST /validation/tinbody is now camelCase:{ registrationType, registrationNumber, tin }.- The old
/api/test/*triggers are gone. UsePOST /companies/consolidate-nowand the per-documentPOST /submitted-documents/{id}/sync-statusinstead. - Pagination params are
?page=&per_page=(per_pagemax 1000, default 10).
Renamed routes
If you port a v1 client by find-and-replace, change these paths:
| v1 | v2 |
|---|---|
GET /invoices/export-all | GET /invoices/export |
GET /adjustment-notes/export-all | GET /adjustment-notes/export |
GET /consolidated-invoices/export-all | GET /consolidated-invoices/export |
GET /submitted-documents/export-all | GET /submitted-documents/export |
POST /consolidated-invoices/:id/adjustment-note | POST /consolidated-invoices/:id/adjustment-notes (plural) |
GET /invoices/:id/submitted-documents/:sid | GET /submitted-documents/:id |
POST /invoices/:id/adjustment-note (the invoice route) stays singular. The nested
…/submitted-documents/:sid read for adjustment notes is also gone. Use
GET /submitted-documents/:id. The list GET …/:id/submitted-documents still works.
List envelope
v1 list responses carried meta: { current_page, last_page, per_page }. v2 returns
{ success, data, pagination }, and pagination has page, perPage, total, totalPages,
hasNextPage, hasPrevPage.
| v1 | v2 |
|---|---|
meta.current_page | pagination.page |
meta.last_page | pagination.totalPages |
meta.per_page | pagination.perPage |
Removed
- Bearer or session auth. Only the
X-Api-Keyheader works.Authorization: Bearer …is not accepted. - Licence-key user and organization endpoints. The v1
/users*,/organizations*and/integrations/*routes (licence-key auth) are gone. Create keys and manage organizations in the dashboard.
Rate limit
Each key is limited to 100 requests per 60 seconds, then 429 RATE_LIMITED. A bulk importer
should use POST /invoices/bulk and back off on 429.
7. Use webhooks instead of polling
v2 pushes document status changes to your server (document.submitted / valid / invalid /
cancelled), signed with HMAC-SHA256 — so you can stop polling. Endpoints are now registrable by
API on /api/v1/webhooks (create, list, update, delete, regenerate secret, delivery log), can be
scoped to live or test, and deliveries carry X-Webhook-Delivery-Id and
X-Webhook-Timestamp headers. Treat each event as a signal and re-read the document. BizCARE also
polls LHDN itself (after each submit and on a half-hourly sweep), so statuses stay current even if
you register no webhook. See Webhooks and the
API reference.
Also new for integrators: idempotent create (Idempotency-Key), 409 CODE_EXISTS with the
holder's id, externalId/externalIdPlatform, list filters, and latestSubmission on invoices —
all described in the API reference. The submitted-documents endpoints no
longer return the raw base64 document or raw LHDN responses; use the /export routes.
8. v2 is metered
Submissions are subject to a plan quota and can fail with 402 QUOTA_EXCEEDED
({ currentUsage, limit, planName }). Make sure your organization is on an adequate plan
before going live — the sandbox environment does not enforce quota, so this only surfaces on
live.