My App

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-production
  • biz_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_reasonfailReason
fail_detailsfailDetails
long_idlongId
submission_uidsubmissionUid
is_submitted_to_lhdnisSubmittedToLhdn
invoice_codeinvoiceCode
created_at / updated_atcreatedAt / updatedAt
company_id, user_id, organization_idcompanyId, userId, organizationId
registration_number, registration_typeregistrationNumber, registrationType
tin_codetinCode

(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? }.

  • error and message are both the human-readable string (message mirrors error for compatibility, so v1 extraction like data.message || … keeps working).
  • code is a machine-readable code (e.g. NOT_FOUND, VALIDATION_ERROR, QUOTA_EXCEEDED).
  • details on validation errors is an array of { path, message }.

See Error handling.

5. Request payload renames (invoice create)

v1v2
code (number)invoiceCode (integer ≥ 1)
issueDateTime (string)invoiceDateTime { date, time }
buyer.typebuyer.partyType
buyer.registration { type, number }flat buyer.registrationType, buyer.registrationNumber
buyer.address.line1 / line2 / line3address.addressLine0 / addressLine1 / addressLine2
buyer.address.cityaddress.cityName
buyer.address.zipCodeaddress.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 /companies returns a single company object (or null), not an array.
  • POST /validation/tin body is now camelCase: { registrationType, registrationNumber, tin }.
  • The old /api/test/* triggers are gone. Use POST /companies/consolidate-now and the per-document POST /submitted-documents/{id}/sync-status instead.
  • Pagination params are ?page=&per_page= (per_page max 1000, default 10).

Renamed routes

If you port a v1 client by find-and-replace, change these paths:

v1v2
GET /invoices/export-allGET /invoices/export
GET /adjustment-notes/export-allGET /adjustment-notes/export
GET /consolidated-invoices/export-allGET /consolidated-invoices/export
GET /submitted-documents/export-allGET /submitted-documents/export
POST /consolidated-invoices/:id/adjustment-notePOST /consolidated-invoices/:id/adjustment-notes (plural)
GET /invoices/:id/submitted-documents/:sidGET /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.

v1v2
meta.current_pagepagination.page
meta.last_pagepagination.totalPages
meta.per_pagepagination.perPage

Removed

  • Bearer or session auth. Only the X-Api-Key header 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.

On this page