Home>API Reference
</>

API Reference

Integrate EasyInvois directly into your ERP, accounting system, or platform. Programmatically create organisations, manage customers, and submit LHDN e-invoices — no dashboard required.


Overview

The EasyInvois REST API lets you build full e-invoicing workflows on top of our platform. All requests and responses use JSON. There are two levels of access — each with a different scope and a separate set of credentials:

  • 1Platform key — Issued by EasyInvois to your business (contact us to request one). Can only be used to provision new organisations via POST /api/v1/organizations. You get one platform key for your entire business — it does not change per organisation. When you create an organisation, the response immediately returns that organisation's own API key.
  • 2Organisation API key — Scoped to a single organisation. Used for all invoice, customer, and data operations. Can be generated from the organisation's Developer settings in the dashboard, or is returned automatically when an organisation is created via the platform key.

Typical platform onboarding flow

  1. Call POST /api/v1/organizations with your platform key → creates the org, returns an org API key
  2. Store the returned org API key against that merchant in your system
  3. Use the org API key for all subsequent calls — create customers, create invoices, submit to LHDN, etc.

Base URL

The API is hosted on the same domain as the application. Choose the environment that matches your integration:

Production
https://www.easyinvoisapp.com/api/v1
Staging
https://staging.easyinvoisapp.com/api/v1

To call an endpoint, append its path to the base URL. For example:

http
# Create an invoice (production)
POST https://www.easyinvoisapp.com/api/v1/invoices

# List customers (staging)
GET  https://staging.easyinvoisapp.com/api/v1/customers

# Get a specific invoice
GET  https://www.easyinvoisapp.com/api/v1/invoices/f1a2b3c4-...

Authentication

Every request must include an Authorization header. Combine your Client ID and Client Secret with a colon:

http
Authorization: Bearer <client_id>:<client_secret>

Client ID

Public identifier. Safe to log. Shown in your key list in Developer settings.

Client Secret

Shown once at creation. Store it in an environment variable — never hard-code it in source code.

Important: If you lose your Client Secret, you must revoke the key and generate a new one. It cannot be recovered.
A 401 is also returned if your organisation's plan does not include API access — for example after a plan downgrade. Contact support if you believe your plan should include API access.

Rate Limits

Organisation API endpoints are limited to 60 requests per minute per API key. When the limit is exceeded, the server returns 429 Too Many Requests. Wait until the next minute window before retrying.

json
HTTP/1.1 429 Too Many Requests
{
  "status": "error",
  "error":  "Rate limit exceeded",
  "detail": "You have exceeded 60 requests per minute. Please slow down."
}

Error Format

All error responses share the same JSON structure:

json
{
  "status": "error",
  "error":  "Short error title",
  "detail": "A human-readable explanation of what went wrong."
}

Common HTTP status codes used across all endpoints:

HTTP StatusDescription
200Success (GET / update requests)
201Created (resource created successfully)
400Malformed JSON or body is not an object
401Missing or invalid API key / platform key
403Access denied — resource not in your organisation, or limit reached
404Resource not found
409Conflict — duplicate email, TIN, or slug
422Validation error — see detail for the specific field
429Rate limit exceeded
500Unexpected server error
502Upstream service error (e.g. LHDN MyInvois unreachable)

Endpoints

POST/api/v1/organizationsPlatform key

Creates a new organisation and owner account in a single call. The account is immediately active — email is auto-verified and an organisation API key is returned. Use this when onboarding a new merchant onto your platform.

The returned client_secret is shown once only. Store it immediately — it cannot be retrieved again.

Request body

http
POST /api/v1/organizations
Authorization: Bearer ci_platform123...:sk_platform456...
Content-Type: application/json

{
  "email":             "owner@acme.com",
  "password":          "SecurePass123",
  "full_name":         "Ahmad bin Ali",
  "organization_name": "Acme Sdn Bhd",
  "tin":               "C1234567890",
  "brn":               "202301012345",
  "msic_code":         "62010",
  "address_line1":     "123 Jalan Bukit",
  "city":              "Kuala Lumpur",
  "state":             "WP Kuala Lumpur",
  "postal_code":       "50450"
}
FieldTypeRequiredDescription
emailstringRequiredOwner email address.
passwordstringRequiredMin 8 chars. Must contain uppercase, lowercase, and a digit. Max 72 chars.
full_namestringRequiredOwner full name. 2–100 characters.
organization_namestringRequiredOrganisation display name. 2–150 characters.
slugstringOptionalURL-friendly identifier used as the invoice number prefix (e.g. slug "acme" → invoice "ACME-2026-0001"). Recommended: pass explicitly to control the prefix. If omitted, auto-derived from organisation_name by stripping legal entity suffixes (Sdn Bhd, Berhad, Bhd, Enterprise/s, Corporation, Corp, PLT, LLP, & Sons) — e.g. "Acme Sdn Bhd" → "acme", "Ali Brothers Construction" → "ali-brothers-construction". If the derived slug is already taken, a random suffix is appended (e.g. "acme-3f9a"). Lowercase letters, numbers, hyphens only; 2–60 chars.
tinstringRequiredLHDN Tax Identification Number. Format: 1–2 uppercase letters followed by digits (e.g. C1234567890). Required to validate customer TINs and submit invoices to MyInvois.
brnstringRequiredBusiness Registration Number (SSM). Alphanumeric with hyphens (e.g. 202301012345). Required by LHDN.
msic_codestringRequiredMSIC industry activity code. Must be exactly 5 digits (e.g. 62010). Required by LHDN.
address_line1stringRequiredFirst line of business address. Required by LHDN.
address_line2stringOptionalSecond line of business address.
citystringRequiredCity. Required by LHDN.
statestringRequiredState (e.g. WP Kuala Lumpur). Required by LHDN.
postal_codestringRequiredPostal code. Required by LHDN.
sst_registration_nostringOptionalSST registration number. Semicolons are supported for multiple registration numbers (e.g. "A12-3456-78900001; B98-7654-32100002"). Max 50 characters.
tourism_tax_registration_nostringOptionalTourism tax registration number.
All required fields are enforced at creation. There is no update API — all details must be correct when creating the organisation.

Response fields

json
HTTP/1.1 201 Created
{
  "status": "success",
  "data": {
    "organization": {
      "id":         "f1a2b3c4-...",
      "name":       "Acme Sdn Bhd",
      "slug":       "acme",
      "tin":        "C1234567890",
      "brn":        "202501234567",
      "created_at": "2025-04-13T10:00:00Z"
    },
    "user": {
      "id":        "a1b2c3d4-...",
      "email":     "owner@acme.com",
      "full_name": "Ahmad bin Ali"
    },
    "api_key": {
      "client_id":     "ci_abc123...",
      "client_secret": "sk_def456..."
    },
    "warning": "Copy the client_secret now. It will not be shown again."
  }
}

Returns an object with three nested objects: organization, user, and api_key.

organization

FieldTypeDescription
iduuid

Unique identifier of the newly created organisation.

namestring

Organisation display name.

slugstring

URL-friendly identifier used in the dashboard.

tinstring

Organisation TIN registered with LHDN.

brnstring

Business registration number.

created_atdatetime

Organisation creation timestamp.

user

FieldTypeDescription
iduuid

Unique identifier of the owner account.

emailstring

Owner email address.

full_namestring

Owner full name.

api_key

FieldTypeDescription
client_idstring

Public key identifier. Use this as the first part of your Authorization header.

client_secretstring

Secret key. Shown once only — store it immediately. Cannot be retrieved again.

A warning string is also included in data reminding you to save the client_secret.

Error codes

HTTP StatusDescription
400Malformed JSON or body is not an object
401Missing or invalid platform key
403Platform key revoked
403Organisation limit reached for this platform key
409Email already registered
409Slug already taken (when slug is explicitly provided)
422email invalid or missing
422password too short (min 8), too long (max 72), or missing uppercase/lowercase/digit
422full_name missing or out of range (2–100 chars)
422organization_name missing or out of range (2–150 chars)
422slug invalid format
422tin missing or invalid format (must be 1–2 uppercase letters followed by digits)
422brn missing or contains invalid characters
422msic_code missing or not exactly 5 digits
422address_line1, city, state, or postal_code missing
429Rate limit exceeded — too many requests. Retry after 60 seconds.
500Free plan not configured (contact support)
GET/api/v1/customersOrg API key

Returns a list of customers belonging to your organisation. Supports search by name or email.

Example request

http
# List latest 50 customers
GET /api/v1/customers
Authorization: Bearer ci_abc123...:sk_def456...

# Search by name or email
GET /api/v1/customers?search=Ahmad
Authorization: Bearer ci_abc123...:sk_def456...

# Custom limit
GET /api/v1/customers?limit=100
Authorization: Bearer ci_abc123...:sk_def456...

Query parameters

FieldTypeRequiredDescription
searchstringOptionalSearch term. Partial match against customer name or email. Returns up to 20 results.
limitintegerOptionalNumber of results when not searching. Range 1–200. Default 50.

Response fields

json
HTTP/1.1 200 OK
{
  "status": "success",
  "data": {
    "customers": [
      {
        "id":                          "c1d2e3f4-...",
        "name":                        "Ahmad bin Ali",
        "email":                       "ahmad@example.com",
        "tin":                         "C1234567890",
        "id_type":                     "NRIC",
        "id_value":                    "900101-14-1234",
        "mobile_prefix_id":            null,
        "phone_number":                null,
        "tin_validation_status":       "valid",
        "tin_validated_at":            "2025-04-13T10:00:00Z",
        "tin_validation_error":        null,
        "tin_last_validated_tin":      "C1234567890",
        "tin_last_validated_id_type":  "NRIC",
        "tin_last_validated_id_value": "900101-14-1234",
        "address_line1":               "123 Jalan Bukit",
        "address_line2":               null,
        "city":                        "Kuala Lumpur",
        "state":                       "WP Kuala Lumpur",
        "postal_code":                 "50450",
        "created_at":                  "2025-04-13T10:00:00Z",
        "updated_at":                  "2025-04-13T10:00:00Z"
      }
    ]
  }
}

Returns customers — an array of objects. Each object contains:

FieldTypeDescription
iduuid

Unique identifier of the customer.

namestring

Customer name.

emailstring | null

Customer email address.

tinstring | null

LHDN Tax Identification Number.

id_typestring | null

Identity document type: NRIC, BRN, PASSPORT, or ARMY.

id_valuestring | null

Identity document number.

mobile_prefix_iduuid | null

UUID of the mobile country prefix. Cross-reference with GET /api/v1/mobile-prefixes.

phone_numberstring | null

Phone number.

tin_validation_statusstring | null

LHDN TIN validation result.

validTIN confirmed by LHDN.
invalidTIN does not match the identity document.
errorValidation failed — see tin_validation_error.
skippedNot attempted — TIN or ID details not provided.
not_checkedTIN present but not yet validated.
tin_validated_atdatetime | null

Timestamp of last LHDN validation.

tin_validation_errorstring | null

Reason for validation failure.

tin_last_validated_tinstring | null

TIN used in last validation.

tin_last_validated_id_typestring | null

id_type used in last validation.

tin_last_validated_id_valuestring | null

id_value used in last validation.

address_line1string | null

First line of address.

address_line2string | null

Second line of address.

citystring | null

City.

statestring | null

State.

postal_codestring | null

Postal code.

created_atdatetime

Creation timestamp.

updated_atdatetime

Last update timestamp.

Error codes

HTTP StatusDescription
401Missing or invalid API key
429Rate limit exceeded
POST/api/v1/customersOrg API key

Creates a new customer. If tin, id_type, and id_value are all provided, the TIN is automatically validated against LHDN in the background — the customer is saved immediately andtin_validation_status in the response reflects the result.

Request body

http
POST /api/v1/customers
Authorization: Bearer ci_abc123...:sk_def456...
Content-Type: application/json

{
  "name":          "Ahmad bin Ali",
  "email":         "ahmad@example.com",
  "tin":           "C1234567890",
  "id_type":       "NRIC",
  "id_value":      "900101-14-1234",
  "address_line1": "123 Jalan Bukit",
  "city":          "Kuala Lumpur",
  "state":         "WP Kuala Lumpur",
  "postal_code":   "50450"
}
FieldTypeRequiredDescription
namestringRequiredCustomer name. Max 200 characters.
emailstringOptionalCustomer email address.
tinstringOptionalLHDN TIN. Max 20 characters. Must be unique within your organisation.
id_typestringOptionalIdentity document type: NRIC, BRN, PASSPORT, or ARMY.
id_valuestringOptionalIdentity document number. Max 50 characters.
mobile_prefix_iduuidOptionalUUID of the mobile country prefix. Get UUIDs from GET /api/v1/mobile-prefixes ↓
phone_numberstringOptionalCustomer phone number. Max 20 characters.
address_line1stringOptionalFirst line of address.
address_line2stringOptionalSecond line of address.
citystringOptionalCity.
statestringOptionalState.
postal_codestringOptionalPostal code.

Response fields

json
HTTP/1.1 201 Created
{
  "status": "success",
  "data": {
    "customer": {
      "id":                          "c1d2e3f4-...",
      "name":                        "Ahmad bin Ali",
      "email":                       "ahmad@example.com",
      "tin":                         "C1234567890",
      "id_type":                     "NRIC",
      "id_value":                    "900101-14-1234",
      "mobile_prefix_id":            null,
      "phone_number":                null,
      "tin_validation_status":       "valid",
      "tin_validated_at":            "2025-04-13T10:00:00Z",
      "tin_validation_error":        null,
      "tin_last_validated_tin":      "C1234567890",
      "tin_last_validated_id_type":  "NRIC",
      "tin_last_validated_id_value": "900101-14-1234",
      "address_line1":               "123 Jalan Bukit",
      "address_line2":               null,
      "city":                        "Kuala Lumpur",
      "state":                       "WP Kuala Lumpur",
      "postal_code":                 "50450",
      "created_at":                  "2025-04-13T10:00:00Z",
      "updated_at":                  "2025-04-13T10:00:00Z"
    }
  }
}
FieldTypeDescription
iduuid

Unique identifier of the customer.

namestring

Customer name.

emailstring | null

Customer email address.

tinstring | null

LHDN Tax Identification Number.

id_typestring | null

Identity document type.

NRICMalaysian identity card
BRNBusiness registration number
PASSPORTPassport
ARMYMilitary identity card
id_valuestring | null

Identity document number.

mobile_prefix_iduuid | null

UUID of the mobile country prefix. Cross-reference with GET /api/v1/mobile-prefixes.

phone_numberstring | null

Phone number.

tin_validation_statusstring | null

Result of LHDN TIN validation.

validLHDN confirmed the TIN matches the identity document.
invalidLHDN says the TIN does not match the identity document.
errorValidation could not be completed — see tin_validation_error.
skippedTIN, id_type, or id_value was not provided — validation was not attempted.
not_checkedTIN exists but has not been validated yet.
tin_validated_atdatetime | null

Timestamp of the last successful LHDN validation call.

tin_validation_errorstring | null

Human-readable reason when tin_validation_status is error or invalid.

tin_last_validated_tinstring | null

The TIN value that was used in the last validation call.

tin_last_validated_id_typestring | null

The id_type used in the last validation call.

tin_last_validated_id_valuestring | null

The id_value used in the last validation call.

address_line1string | null

First line of address.

address_line2string | null

Second line of address.

citystring | null

City.

statestring | null

State.

postal_codestring | null

Postal code.

created_atdatetime

Timestamp when the customer was created.

updated_atdatetime

Timestamp of the last update.

Error codes

HTTP StatusDescription
400Malformed JSON or body is not an object
401Missing or invalid API key
409Duplicate TIN — another customer in your organisation already has this TIN
409Duplicate ID document — same id_type + id_value already exists for this organisation
422name missing
422name exceeds 200 characters
422email invalid format
422id_type invalid (must be NRIC, BRN, PASSPORT, or ARMY)
422mobile_prefix_id is not a valid UUID
422mobile_prefix_id UUID does not exist — call GET /api/v1/mobile-prefixes for valid IDs
429Rate limit exceeded

Create Invoice

POST/api/v1/invoicesOrg API key

Creates a new invoice. Can be saved as a draft or immediately finalized.

  • If customer is provided with tin + id_type + id_value: EasyInvois validates the TIN against LHDN. If validation fails, the invoice is rejected — fix the customer details and retry. If the TIN already exists in your organisation, the existing customer record is reused automatically.
  • If no customer is provided, or customer has no TIN: A buyer_link is returned when the invoice is finalized. Share this URL with your customer so they can fill in their TIN details. The link expires in 72 hours.

See Invoice Scenarios ↓ for example responses for each case.

Request body

http
POST /api/v1/invoices
Authorization: Bearer ci_abc123...:sk_def456...
Content-Type: application/json

{
  "customer": {
    "name":          "Ahmad bin Ali",
    "email":         "ahmad@example.com",
    "tin":           "C1234567890",
    "id_type":       "NRIC",
    "id_value":      "900101-14-1234",
    "phone_number":  "123456789",
    "address_line1": "123 Jalan Bukit",
    "city":          "Kuala Lumpur",
    "state":         "WP Kuala Lumpur",
    "postal_code":   "50450"
  },
  "items": [
    {
      "name":          "Consulting Service",
      "quantity":      1,
      "unit_price":    500.00,
      "tax_rate":      8,
      "tax_type_code": "02",
      "sku":           "CS-3F7A2B"
    }
  ],
  "issue_date": "2025-04-13",
  "due_date":   "2025-04-27",
  "reference":  "PO-20250413"
}
FieldTypeRequiredDescription
customer_iduuidOptionalUUID of an existing customer. Use this OR customer — not both.
customerobjectOptionalCreate a new customer inline instead of referencing an existing one. Use this OR customer_id — not both. If neither is provided, the invoice is created with no customer and buyer_status will be "pending" when finalized.
customer.namestringOptionalCustomer name. Required if providing customer object. Max 200 characters.
customer.emailstringOptionalCustomer email address.
customer.tinstringOptionalLHDN TIN. If provided together with id_type and id_value, LHDN TIN validation is performed and must pass — invoice creation is rejected if validation fails. If the TIN already exists in your organisation, the existing customer is reused.
customer.id_typestringOptionalIdentity document type: NRIC, BRN, PASSPORT, or ARMY. Required if tin is provided.
customer.id_valuestringOptionalIdentity document number. Required if tin is provided.
customer.mobile_prefix_iduuidOptionalUUID of the mobile country prefix. Get UUIDs from GET /api/v1/mobile-prefixes ↓
customer.phone_numberstringOptionalPhone number. Max 20 characters.
customer.address_line1stringOptionalFirst line of address.
customer.address_line2stringOptionalSecond line of address.
customer.citystringOptionalCity.
customer.statestringOptionalState.
customer.postal_codestringOptionalPostal code.
invoice_statusstringOptional"finalized" (default) or "draft". Finalized invoices are ready for LHDN submission. Use "draft" if you want to review before finalizing.
itemsarrayRequiredArray of line items. 1–100 items.
items[].namestringRequiredItem description. Max 500 characters.
items[].quantitynumberRequiredQuantity. Range 0.01–100,000. Max 2 decimal places.
items[].unit_pricenumberRequiredUnit price in RM. Range 0–9,999,999.99. Max 2 decimal places.
items[].tax_ratenumberRequiredTax percentage. Range 0–100. Max 2 decimal places.
items[].tax_type_codestringRequiredLHDN tax type code. Required when tax_rate > 0 — a 422 error is returned if tax_rate is non-zero and tax_type_code resolves to 06. If the SKU matches a saved product, the product's tax type is used as the default before this check. Valid values: 01 Sales Tax, 02 Service Tax, 03 Tourism Tax, 04 High-Value Goods, 05 LVG Sales Tax, 06 Not Applicable (default, only valid when tax_rate = 0), E Exempt.
items[].skustringOptionalYour internal SKU or product code. Optional — if matched, inherits the product's tax type, classification code, and unit code.
items[].classification_codestringOptionalLHDN classification code. Defaults to 022 (Others). See Classification Codes ↓
items[].unit_codestringOptionalLHDN unit of measurement code. Defaults to C62 (Unit / Each). See Unit Codes ↓
issue_datestringOptionalInvoice date in YYYY-MM-DD format (Malaysia time). Defaults to today (MYT).
due_datestringOptionalPayment due date in YYYY-MM-DD format (Malaysia time). Must be on or after issue_date.
referencestringOptionalYour internal reference (e.g. PO number). Max 255 characters.

Response fields

json
HTTP/1.1 201 Created
{
  "status": "success",
  "data": {
    "invoice": {
      "id":                    "i1j2k3l4-...",
      "customer_id":           "c1d2e3f4-...",
      "invoice_number":        "ACME-2025-0001",
      "invoice_status":        "finalized",
      "lhdn_status":           "not_submitted",
      "delivery_status":       "not_sent",
      "buyer_status":          "not_required",
      "tin_validation_status": "valid",
      "tin_validation_error":  null,
      "issue_date":            "2025-04-13",
      "due_date":              "2025-04-27",
      "currency_code":         "MYR",
      "total_amount":          "500.00",
      "tax_amount":            "30.00",
      "grand_total":           "530.00",
      "reference":             "PO-20250413",
      "buyer_info_expires_at": null,
      "created_at":            "2025-04-13T10:00:00Z"
    }
  }
}
FieldTypeDescription
iduuid

Unique identifier of the invoice.

customer_iduuid

UUID of the customer this invoice belongs to.

invoice_numberstring

Auto-generated invoice number (e.g. ACME-2025-0001).

invoice_statusstring

Current invoice status.

draftInvoice saved but not yet finalised. Cannot be submitted to LHDN.
finalizedInvoice is finalised and ready for LHDN submission.
voidInvoice has been cancelled.
lhdn_statusstring

LHDN MyInvois submission status.

not_submittedNot yet sent to LHDN.
submittedSent to LHDN, awaiting validation result.
validLHDN accepted and validated the invoice.
invalidLHDN rejected the invoice — check lhdn_status_reason.
cancellingCancellation has been submitted to LHDN and is awaiting confirmation.
cancelledInvoice was cancelled via LHDN.
errorSubmission failed due to a technical error. Can be resubmitted.
delivery_statusstring

Email delivery status.

not_sentEmail not sent.
sentEmail delivered to customer.
failedEmail delivery failed.
buyer_statusstring

Buyer TIN collection status.

not_requiredCustomer already has a validated TIN — no buyer action needed.
pendingAwaiting buyer to fill in their TIN via the buyer_link.
submittedBuyer has submitted their TIN details.
expiredBuyer info request has expired (72 hours elapsed without submission).
buyer_linkstring | null

URL for the buyer to fill in their TIN. Only present when buyer_status is pending. Share this link or generate a QR code from it. Expires in 72 hours.

tin_validation_statusstring | null

Result of customer TIN validation at invoice creation.

validTIN confirmed by LHDN at time of invoice creation.
invalidTIN did not match — buyer_link generated instead.
errorValidation call failed.
skippedCustomer has no TIN — buyer_link generated instead.
not_checkedDraft invoice — TIN not checked.
tin_validation_errorstring | null

Reason when tin_validation_status is error.

issue_datestring

Invoice date in YYYY-MM-DD format.

due_datestring | null

Payment due date in YYYY-MM-DD format.

currency_codestring

Currency code. Always MYR.

total_amountstring

Pre-tax subtotal in RM (e.g. "500.00").

tax_amountstring

Total tax amount in RM.

grand_totalstring

Total including tax in RM.

referencestring | null

Your internal reference number (e.g. PO number). Null if not provided.

buyer_info_expires_atdatetime | null

When the buyer_link expires. Set to 72 hours after invoice creation when buyer_status is pending.

created_atdatetime

Invoice creation timestamp.

buyer_link — Only present when buyer_status is "pending". You can generate a QR code from this URL and display it to your customer, or send them the link directly. It expires after 72 hours.

Error codes

HTTP StatusDescription
400Malformed JSON or body is not an object
401Missing or invalid API key
403customer_id does not belong to your organisation
422Inline customer TIN validation failed — TIN does not match the provided id_type/id_value (LHDN rejected)
502Inline customer TIN validation error — LHDN could not be reached or returned an error
422Both customer_id and customer provided — use one only
422customer.name missing or exceeds 200 characters
422customer.email invalid format
422customer.id_type invalid (must be NRIC, BRN, PASSPORT, or ARMY)
422customer.mobile_prefix_id is not a valid UUID
403Monthly invoice limit reached — upgrade your plan
422customer_id is not a valid UUID
422invoice_status invalid (must be "draft" or "finalized")
422items is not an array or is empty
422items exceeds maximum of 100
422items[].name missing or exceeds 500 characters
422items[].quantity, unit_price, or tax_rate out of range or has more than 2 decimal places
422items[].tax_type_code missing or not one of: 01, 02, 03, 04, 05, 06, E
422items[].tax_type_code is 06 (Not Applicable) but tax_rate > 0 — must specify a valid tax type
422Grand total exceeds RM 99,999,999.99
422issue_date or due_date invalid (must be YYYY-MM-DD)
422due_date is before issue_date
429Rate limit exceeded

Get Invoice

GET/api/v1/invoices/:idOrg API key

Returns the current status and full metadata of a single invoice by its UUID. Use this endpoint to poll for LHDN submission status after submitting an invoice.

Example request

http
GET /api/v1/invoices/i1j2k3l4-...
Authorization: Bearer ci_abc123...:sk_def456...

Response fields

json
HTTP/1.1 200 OK
{
  "status": "success",
  "data": {
    "invoice": {
      "id":                       "i1j2k3l4-...",
      "customer_id":              "c1d2e3f4-...",
      "invoice_number":           "ACME-2025-0001",
      "invoice_type_code":        "01",
      "original_invoice_id":      null,
      "invoice_status":           "finalized",
      "lhdn_status":              "valid",
      "lhdn_status_reason":       null,
      "cancellation_reason":      null,
      "lhdn_submission_uid":      "sub_abc123...",
      "lhdn_uuid":                "lhdn_uuid_...",
      "lhdn_long_id":             "lhdn_long_id_...",
      "lhdn_date_time_received":  "2025-04-13T10:01:00Z",
      "lhdn_date_time_validated": "2025-04-13T10:02:00Z",
      "lhdn_cancelled_at":        null,
      "delivery_status":          "sent",
      "sent_at":                  null,
      "buyer_status":             "not_required",
      "buyer_info_expires_at":    null,
      "tin_validation_status":    "valid",
      "tin_validation_error":     null,
      "paid_at":                  null,
      "issue_date":               "2025-04-13",
      "due_date":                 "2025-04-27",
      "currency_code":            "MYR",
      "total_amount":             "500.00",
      "tax_amount":               "30.00",
      "grand_total":              "530.00",
      "reference":                "PO-20250413",
      "created_at":               "2025-04-13T10:00:00Z"
    }
  }
}
FieldTypeDescription
iduuid

Unique identifier of the invoice.

customer_iduuid | null

UUID of the associated customer.

invoice_numberstring

Human-readable invoice number (e.g. ACME-2025-0001).

invoice_type_codestring

LHDN invoice type. 01 = Invoice, 02 = Credit Note, 03 = Debit Note.

original_invoice_iduuid | null

For credit/debit notes, the UUID of the original invoice.

invoice_statusstring

Invoice lifecycle status.

draftSaved but not finalised. Cannot be submitted to LHDN.
finalizedFinalised and ready for LHDN submission.
voidCancelled.
lhdn_statusstring

LHDN MyInvois submission status.

not_submittedNot yet sent to LHDN.
submittedSent, awaiting LHDN validation.
validAccepted and validated by LHDN.
invalidRejected by LHDN — see lhdn_status_reason.
cancellingCancellation has been submitted to LHDN and is awaiting confirmation.
cancelledCancelled via LHDN.
errorSubmission failed due to a technical error. Can be resubmitted.
lhdn_status_reasonstring | null

Reason from LHDN when status is invalid or cancelled.

cancellation_reasonstring | null

Reason provided by the user when the invoice was cancelled via the MyInvois API. Null if not cancelled or no reason given.

lhdn_submission_uidstring | null

MyInvois submission UID. Available after submission.

lhdn_uuidstring | null

MyInvois document UUID. Available after validation.

lhdn_long_idstring | null

MyInvois long ID. Use with lhdn_uuid to construct the QR verification URL.

lhdn_date_time_receiveddatetime | null

When MyInvois received the document.

lhdn_date_time_validateddatetime | null

When MyInvois validated the document.

lhdn_cancelled_atdatetime | null

When the invoice was cancelled at LHDN.

delivery_statusstring

Email delivery status.

not_sentEmail not sent.
sentEmail delivered.
failedEmail delivery failed.
buyer_statusstring

Buyer TIN collection status.

not_requiredNo buyer action needed — customer has a validated TIN.
pendingWaiting for buyer to submit their TIN via buyer_link.
submittedBuyer has submitted their TIN.
expiredBuyer info request has expired (72 hours elapsed without submission).
buyer_linkstring | null

URL for buyer to fill in TIN. Only present when buyer_status is pending.

buyer_info_expires_atdatetime | null

When the buyer_link expires (72 hours after finalization). Only set when buyer_status is pending.

tin_validation_statusstring | null

Customer TIN validation result.

validTIN confirmed by LHDN.
invalidTIN did not match the identity document.
errorValidation failed — see tin_validation_error.
skippedNo TIN provided — not validated.
not_checkedTIN present but not yet validated.
tin_validation_errorstring | null

Reason when tin_validation_status is error.

paid_atdatetime | null

When the invoice was marked as paid.

issue_datestring

Invoice date (YYYY-MM-DD).

due_datestring | null

Payment due date (YYYY-MM-DD).

currency_codestring

Currency code. Always MYR.

total_amountstring

Pre-tax subtotal in RM (e.g. "500.00").

tax_amountstring

Total tax in RM.

grand_totalstring

Total including tax in RM.

referencestring | null

Your internal reference.

sent_atstring | null

ISO 8601 timestamp when the invoice was emailed to the customer. null if not yet sent.

created_atdatetime

Invoice creation timestamp.

Error codes

HTTP StatusDescription
401Missing or invalid API key
404Invoice not found or does not belong to your organisation
422id is not a valid UUID
429Rate limit exceeded

Finalize Invoice

PATCH/api/v1/invoices/:idOrg API key

Finalizes a draft invoice. Once finalized, TIN validation runs against LHDN and the invoice is ready for submission. If the customer has no TIN, buyer_link is returned.

Only draft invoices can be finalized. Finalized and voided invoices cannot be changed.

Request body

http
PATCH /api/v1/invoices/i1j2k3l4-...
Authorization: Bearer ci_abc123...:sk_def456...
Content-Type: application/json

{
  "invoice_status": "finalized"
}
FieldTypeRequiredDescription
invoice_statusstringRequiredMust be "finalized". This is the only accepted value.

Response fields

Returns the same fields as Get Invoice.

json
HTTP/1.1 200 OK
{
  "status": "success",
  "data": {
    "invoice": {
      "id":                       "i1j2k3l4-...",
      "customer_id":              "c1d2e3f4-...",
      "invoice_number":           "ACME-2025-0001",
      "invoice_type_code":        "01",
      "original_invoice_id":      null,
      "invoice_status":           "finalized",
      "lhdn_status":              "not_submitted",
      "lhdn_status_reason":       null,
      "lhdn_submission_uid":      null,
      "lhdn_uuid":                null,
      "lhdn_long_id":             null,
      "lhdn_date_time_received":  null,
      "lhdn_date_time_validated": null,
      "lhdn_cancelled_at":        null,
      "delivery_status":          "not_sent",
      "sent_at":                  null,
      "buyer_status":             "not_required",
      "buyer_info_expires_at":    null,
      "tin_validation_status":    "valid",
      "tin_validation_error":     null,
      "paid_at":                  null,
      "issue_date":               "2025-04-13",
      "due_date":                 null,
      "currency_code":            "MYR",
      "total_amount":             "500.00",
      "tax_amount":               "30.00",
      "grand_total":              "530.00",
      "reference":                null,
      "created_at":               "2025-04-13T10:00:00Z"
    }
  }
}

Error codes

HTTP StatusDescription
400Malformed JSON or body is not an object
401Missing or invalid API key
404Invoice not found or does not belong to your organisation
422id is not a valid UUID
422invoice_status is not "finalized"
422Invoice is already finalized or voided
429Rate limit exceeded

Query Invoices

POST/api/v1/invoices/queryOrg API key

Fetches up to 50 invoices by their UUIDs in a single request. Use this for bulk status polling. Invoices not found or not belonging to your organisation are silently omitted. Results are returned in the same order as the input IDs.

This endpoint only returns standard invoices (type 01). Credit notes and debit notes are excluded.

Request body

FieldTypeRequiredDescription
idsstring[]RequiredArray of invoice UUIDs. 1–50 entries. Duplicates are deduplicated.

Example request

http
POST /api/v1/invoices/query
Authorization: Bearer ci_abc123...:sk_def456...
Content-Type: application/json

{
  "ids": [
    "i1j2k3l4-...",
    "m5n6o7p8-..."
  ]
}

Response fields

Returns invoices — an array of full invoice objects, each containing the same fields as Get Invoice. Invoices not found or not belonging to your organisation are silently omitted.

json
HTTP/1.1 200 OK
{
  "status": "success",
  "data": {
    "invoices": [
      {
        "id":                       "i1j2k3l4-...",
        "customer_id":              "c1d2e3f4-...",
        "invoice_number":           "ACME-2025-0001",
        "invoice_type_code":        "01",
        "original_invoice_id":      null,
        "invoice_status":           "finalized",
        "lhdn_status":              "valid",
        "lhdn_status_reason":       null,
        "lhdn_submission_uid":      "sub_abc123...",
        "lhdn_uuid":                "lhdn_uuid_...",
        "lhdn_long_id":             "lhdn_long_id_...",
        "lhdn_date_time_received":  "2025-04-13T10:01:00Z",
        "lhdn_date_time_validated": "2025-04-13T10:02:00Z",
        "lhdn_cancelled_at":        null,
        "delivery_status":          "sent",
        "sent_at":                  null,
        "buyer_status":             "not_required",
        "buyer_info_expires_at":    null,
        "tin_validation_status":    "valid",
        "tin_validation_error":     null,
        "paid_at":                  null,
        "issue_date":               "2025-04-13",
        "due_date":                 "2025-04-27",
        "currency_code":            "MYR",
        "total_amount":             "500.00",
        "tax_amount":               "30.00",
        "grand_total":              "530.00",
        "reference":                "PO-20250413",
        "created_at":               "2025-04-13T10:00:00Z"
      }
    ]
  }
}

Error codes

HTTP StatusDescription
400Malformed JSON or body is not an object
401Missing or invalid API key
422ids missing, empty, or contains no valid UUIDs
422ids exceeds maximum of 50 unique IDs
429Rate limit exceeded

Submit Invoices

POST/api/v1/invoices/submitOrg API key

Submits one or more finalized invoices to LHDN MyInvois in a single batch. After a successful call the invoices move to lhdn_status = "submitted" — LHDN validates asynchronously. Poll GET /api/v1/invoices/:id or listen to theinvoice.lhdn_status_changed webhook for the final result.

Consolidated invoices: Do not pass a consolidated invoice ID to this endpoint. UsePOST /api/v1/invoices/consolidate/:id/submit instead. Child invoices that have been added to a consolidated invoice also cannot be submitted individually — withdraw them first.

Request body

FieldTypeRequiredDescription
invoice_idsuuid[]RequiredArray of invoice UUIDs to submit. Pass one UUID to submit a single invoice, or multiple to submit in a batch. Maximum 100 per request. Duplicates are ignored.

Example request

http
POST /api/v1/invoices/submit
Authorization: Bearer ci_abc123...:sk_def456...
Content-Type: application/json

// Single invoice
{ "invoice_ids": ["i1j2k3l4-..."] }

// Multiple invoices
{ "invoice_ids": ["i1j2k3l4-...", "i5j6k7l8-..."] }

Response fields

http
HTTP/1.1 202 Accepted
{
  "status": "success",
  "data": {
    "status":         "success",
    "submitted": [
      { "id": "2ea78328-...", "invoice_number": "ACME-2026-0001", "lhdn_status": "submitted" },
      { "id": "58a7ec3f-...", "invoice_number": "ACME-2026-0002", "lhdn_status": "submitted" }
    ],
    "failed":         [],
    "submission_id":  "s1u2b3m4-...",
    "submission_uid": "LHDN-UID-12345"
  }
}

When some invoices fail pre-validation and others succeed, status is "partial". When all fail, status is "failed" and the HTTP status is 422.

FieldTypeDescription
statusstring

"success" — all submitted. "partial" — some submitted, some failed. "failed" — none submitted (HTTP 422).

submittedarray

Invoices accepted by LHDN. LHDN validates asynchronously — poll GET /api/v1/invoices/:id for the final valid or invalid result.

submitted[].iduuid

Invoice UUID.

submitted[].invoice_numberstring

Invoice number.

submitted[].lhdn_statusstring

Always "submitted" at this point.

failedarray

Invoices that could not be submitted — either failed pre-validation or were rejected by LHDN.

failed[].iduuid

Invoice UUID.

failed[].invoice_numberstring

Invoice number.

failed[].reasonstring

Why this invoice was not submitted.

submission_iduuid | null

Internal EasyInvois submission UUID. Present when at least one invoice was submitted.

submission_uidstring | null

LHDN submission UID. Present when at least one invoice was submitted.

Error codes

HTTP StatusDescription
400Malformed JSON or body is not an object
401Missing or invalid API key
404One or more invoice IDs not found or do not belong to your organisation
409A submission is already in progress for one or more of these invoices — wait and retry
422invoice_ids missing, empty, or all values are invalid UUIDs
422Batch exceeds 100 invoices after deduplication
422All invoices failed — see failed[] array in response body for per-invoice reasons
422Organisation TIN or BRN not configured
422Document build failed — invoice data is malformed
429Rate limit exceeded (60 requests per minute)
502LHDN upstream error

Submit Single Invoice

POST/api/v1/invoices/:id/submitOrg API key

Submits a single invoice to LHDN by passing its UUID in the URL path. No request body required. Handles regular invoices (type 01) as well as credit notes (type 02) and debit notes (type 03) — for CN/DN the document is submitted to LHDN as an adjustment document. Explicitly rejects consolidated invoices — use POST /api/v1/invoices/consolidate/:id/submit for those.

Example request

http
POST /api/v1/invoices/i1j2k3l4-.../submit
Authorization: Bearer ci_abc123...:sk_def456...

Response fields

http
HTTP/1.1 202 Accepted
{
  "status": "success",
  "data": {
    "submission_id":  "s1u2b3m4-...",
    "submission_uid": "LHDN-UID-12345",
    "invoice_count":  1,
    "lhdn_status":    "submitted"
  }
}
FieldTypeDescription
submission_iduuid

Internal EasyInvois submission UUID.

submission_uidstring

LHDN submission UID returned by MyInvois.

invoice_countnumber

Always 1 for this endpoint.

lhdn_statusstring

Always "submitted" on success. Poll GET /api/v1/invoices/:id for the final valid or invalid result.

Error codes

HTTP StatusDescription
401Missing or invalid API key
404Invoice not found or does not belong to your organisation
409A submission is already in progress for this invoice — wait and retry
422id is not a valid UUID
422Invoice is not finalized (invoice_status must be "finalized")
422Invoice has already been submitted to LHDN
422Invoice is a consolidated invoice — use POST /api/v1/invoices/consolidate/:id/submit instead
422Invoice belongs to a consolidated invoice — withdraw it from the consolidated invoice first
422Customer has an unvalidated TIN
422Organisation TIN or BRN not configured
422Document build failed — invoice data is malformed
422LHDN rejected the submission
429Rate limit exceeded (60 requests per minute)
502LHDN upstream error

Consolidate Invoices

POST/api/v1/invoices/consolidateOrg API key

Groups multiple finalized invoices into a single consolidated invoice for LHDN submission. Consolidated invoices are used for B2C transactions where the buyer has no TIN — instead of submitting each invoice individually, you batch them into one document.

Each original invoice becomes one line item in the consolidated document: the line description is the original invoice number, and the unit price is the original grand total. The original invoices remain visible and are linked to the consolidated invoice — they cannot be submitted individually until withdrawn.

Eligibility rules — every invoice in invoice_ids must satisfy all of the following:
  • invoice_status is finalized
  • lhdn_status is not_submitted, invalid, or error
  • Not already part of another consolidated invoice
  • Customer has no TIN (B2C only — invoices with a validated buyer TIN are not eligible)
  • Same currency as all other invoices in the batch

Request body

FieldTypeRequiredDescription
invoice_idsuuid[]RequiredArray of invoice UUIDs to consolidate. Minimum 1. All must belong to your organisation and satisfy the eligibility rules above.

Example request

http
POST /api/v1/invoices/consolidate
Authorization: Bearer ci_abc123...:sk_def456...
Content-Type: application/json

{
  "invoice_ids": [
    "i1j2k3l4-...",
    "i5j6k7l8-...",
    "i9j0k1l2-..."
  ]
}

Response fields

http
HTTP/1.1 200 OK
{
  "status": "success",
  "data": {
    "consolidated_invoice_id":     "c1o2n3s4-...",
    "consolidated_invoice_number": "CONS-2025-0001",
    "original_invoice_count":      3
  }
}
FieldTypeDescription
consolidated_invoice_iduuid

UUID of the newly created consolidated invoice. Use this with POST /api/v1/invoices/consolidate/:id/submit to submit to LHDN.

consolidated_invoice_numberstring

Auto-generated consolidated invoice number in the format CONS-YYYY-NNNN (e.g. CONS-2025-0001). Separate sequence from regular invoices.

original_invoice_countnumber

Number of original invoices grouped into this consolidated invoice.

Error codes

HTTP StatusDescription
400Malformed JSON or body is not an object
401Missing or invalid API key
404One or more invoice IDs not found or do not belong to your organisation
422invoice_ids is missing or empty
422One or more invoice_ids is not a valid UUID
422One or more invoices is not finalized
422One or more invoices has already been submitted to LHDN
422One or more invoices is already part of another consolidated invoice
422One or more invoices has a customer with a TIN — only invoices without a buyer TIN can be consolidated
422Invoices have mixed currencies — all must share the same currency
429Rate limit exceeded (60 requests per minute)

Submit Consolidated Invoice

POST/api/v1/invoices/consolidate/:id/submitOrg API key

Submits a consolidated invoice to LHDN. Only accepts a consolidated invoice — regular invoices are rejected. After a successful call the consolidated invoice moves to lhdn_status = "submitted". Poll GET /api/v1/invoices/:id or listen to the invoice.lhdn_status_changed webhook for the final result.

No request body required — the consolidated invoice ID is passed in the URL path.

Example request

http
POST /api/v1/invoices/consolidate/c1o2n3s4-.../submit
Authorization: Bearer ci_abc123...:sk_def456...

Response fields

http
HTTP/1.1 202 Accepted
{
  "status": "success",
  "data": {
    "submission_id":  "s1u2b3m4-...",
    "submission_uid": "LHDN-UID-12345",
    "invoice_count":  1,
    "lhdn_status":    "submitted"
  }
}
FieldTypeDescription
submission_iduuid

Internal EasyInvois submission UUID.

submission_uidstring

LHDN submission UID returned by MyInvois.

invoice_countnumber

Always 1 — one consolidated document submitted.

lhdn_statusstring

Always "submitted" on success. Poll GET /api/v1/invoices/:id for the final valid or invalid result.

Error codes

HTTP StatusDescription
401Missing or invalid API key
404Consolidated invoice not found or does not belong to your organisation
409A submission is already in progress for this invoice — wait and retry
422id is not a valid UUID
422Invoice is not a consolidated invoice — use POST /api/v1/invoices/:id/submit or POST /api/v1/invoices/submit instead
422Consolidated invoice has already been submitted to LHDN
422Organisation TIN or BRN not configured
422Document build failed — consolidated invoice data is malformed
422LHDN rejected the submission
429Rate limit exceeded (60 requests per minute)
502LHDN upstream error

Create Credit Note

POST/api/v1/invoices/:id/credit-noteOrg API key

Creates a Credit Note (LHDN type 02) against an existing LHDN-validated invoice. The credit note is created as finalized and pre-filled with all line items from the original invoice. Call POST /api/v1/invoices/:cn_id/submit to submit it to LHDN immediately.

The original invoice must have lhdn_status = "valid" and must not be voided. Self-billed invoices (type 11) are not supported.

Path parameters

FieldTypeRequiredDescription
iduuidRequiredEasyInvois UUID of the original invoice to credit. This is the id field returned when you created the invoice — not the LHDN UUID.

Example request

bash
POST /api/v1/invoices/8f3a1c2d-.../credit-note
Authorization: Bearer ci_live_...:cs_live_...

Response fields

HTTP 201 — returns the newly created credit note.

FieldTypeDescription
iduuid

UUID of the newly created credit note.

invoice_numberstring

Auto-generated credit note number (e.g. CN-2025-0001).

Error codes

HTTP StatusDescription
401Missing or invalid API key
404Original invoice not found or does not belong to your organisation
422id in URL is not a valid UUID
422Original invoice is not LHDN-validated (lhdn_status must be "valid")
422Original invoice is voided
422Original invoice has no LHDN UUID
422Original invoice has no line items
422Self-billed invoice adjustments are not supported
429Rate limit exceeded (60 requests per minute)

Create Debit Note

POST/api/v1/invoices/:id/debit-noteOrg API key

Creates a Debit Note (LHDN type 03) against an existing LHDN-validated invoice. Identical behaviour to Create Credit Note above, but issued when additional charges need to be applied to the buyer. The debit note is created as finalized and pre-filled with all line items from the original invoice. Call POST /api/v1/invoices/:dn_id/submit to submit it to LHDN immediately.

Path parameters

FieldTypeRequiredDescription
iduuidRequiredEasyInvois UUID of the original invoice to debit. This is the id field returned when you created the invoice — not the LHDN UUID.

Example request

bash
POST /api/v1/invoices/8f3a1c2d-.../debit-note
Authorization: Bearer ci_live_...:cs_live_...

Response fields

HTTP 201 — returns the newly created debit note.

FieldTypeDescription
iduuid

UUID of the newly created debit note.

invoice_numberstring

Auto-generated debit note number (e.g. DN-2025-0001).

Error codes

HTTP StatusDescription
401Missing or invalid API key
404Original invoice not found or does not belong to your organisation
422id in URL is not a valid UUID
422Original invoice is not LHDN-validated (lhdn_status must be "valid")
422Original invoice is voided
422Original invoice has no LHDN UUID
422Original invoice has no line items
422Self-billed invoice adjustments are not supported
429Rate limit exceeded (60 requests per minute)

Cancel Invoice

POST/api/v1/invoices/:id/cancelOrg API key

Cancels an invoice. Behaviour depends on the invoice's LHDN status:

  • Not submitted to LHDN (lhdn_status: "not_submitted") — voided locally immediately. No LHDN call is made.
  • LHDN-validated (lhdn_status: "valid") — cancels via the MyInvois API. LHDN only allows cancellation within 72 hours of validation.
Cancellation is permanent and cannot be undone.

Request body

http
POST /api/v1/invoices/i1j2k3l4-.../cancel
Authorization: Bearer ci_abc123...:sk_def456...
Content-Type: application/json

{
  "reason": "Wrong amount entered"
}
FieldTypeRequiredDescription
reasonstringRequiredReason for cancellation. Sent to LHDN when cancelling a validated invoice. Max 255 characters.

Response fields

json
HTTP/1.1 200 OK
{
  "status": "success",
  "data": {
    "invoice": {
      "id":                   "i1j2k3l4-...",
      "invoice_number":       "ACME-2025-0001",
      "invoice_status":       "void",
      "lhdn_status":          "cancelled",
      "cancellation_reason":  "Wrong amount entered",
      "lhdn_cancelled_at":    "2025-04-13T12:00:00Z"
    }
  }
}

Returns invoice with the updated fields after cancellation.

FieldTypeDescription
iduuid

Unique identifier of the invoice.

invoice_numberstring

Human-readable invoice number.

invoice_statusstring

Always void after successful cancellation.

voidInvoice has been cancelled.
lhdn_statusstring

LHDN status after cancellation.

cancelledLHDN confirmed the cancellation (was previously valid).
not_submittedInvoice was voided locally without submitting to LHDN.
cancellation_reasonstring | null

The cancellation reason provided in the request body, if any.

lhdn_cancelled_atdatetime | null

Timestamp when LHDN recorded the cancellation. Null if not submitted to LHDN.

Error codes

HTTP StatusDescription
400Malformed JSON or body is not an object
401Missing or invalid API key
404Invoice not found or does not belong to your organisation
409Invoice status changed during the request — retry
409Cancellation in progress — a cancellation is already in progress for this invoice
422id is not a valid UUID
422reason is required
422reason exceeds 255 characters
422Invoice is already voided
422Invoice lhdn_status cannot be cancelled (must be "valid" or "not_submitted")
422Cancellation window expired (72 hours from LHDN validation)
422Organisation TIN not configured
422Missing LHDN UUID — the invoice does not have an LHDN UUID
422Missing validation timestamp — the invoice does not have a validation timestamp
429Rate limit exceeded
502LHDN returned an error during cancellation

Invoice Scenarios

Common scenarios when creating invoices via POST /api/v1/invoices.

1. Customer with valid TIN — invoice ready to submit

When you pass tin + id_type + id_value and LHDN validates successfully, the invoice is immediately ready for LHDN submission.

json
// Response — HTTP 201
{
  "status": "success",
  "data": {
    "invoice": {
      "invoice_number":        "ACME-2026-0001",
      "invoice_status":        "finalized",
      "lhdn_status":           "not_submitted",
      "buyer_status":          "not_required",
      "tin_validation_status": "valid",
      "tin_validation_error":  null,
      "buyer_link":            null
    }
  }
}

2. No customer provided — buyer fills in TIN via link

When no customer is provided (or customer has no TIN), a buyer_link is returned. Share this URL with your customer so they can fill in their TIN. The link expires in 72 hours.

json
// Response — HTTP 201
{
  "status": "success",
  "data": {
    "invoice": {
      "invoice_number":        "ACME-2026-0002",
      "invoice_status":        "finalized",
      "lhdn_status":           "not_submitted",
      "buyer_status":          "pending",
      "tin_validation_status": "skipped",
      "tin_validation_error":  null,
      "buyer_link":            "https://app.easyinvoisapp.com/buyer/a1b2c3...",
      "buyer_info_expires_at": "2026-06-25T10:00:00Z"
    }
  }
}

3. TIN does not match identity document — invoice rejected

When the TIN and id_type/id_value combination is rejected by LHDN (e.g. NRIC number doesn't match the TIN), the invoice is not created. Fix the customer details and retry.

json
// Response — HTTP 422
{
  "status": "error",
  "title": "Invalid customer TIN",
  "description": "The customer TIN does not match the provided identity document."
}

4. TIN already exists — existing customer reused

If the TIN you provide already belongs to a customer in your organisation, that customer is reused automatically. No duplicate is created.

json
// Response — HTTP 201
{
  "status": "success",
  "data": {
    "invoice": {
      "invoice_number":        "ACME-2026-0003",
      "customer_id":           "c1d2e3f4-...",
      "invoice_status":        "finalized",
      "lhdn_status":           "not_submitted",
      "buyer_status":          "not_required",
      "tin_validation_status": "valid",
      "tin_validation_error":  null
    }
  }
}

5. LHDN credentials error — invoice rejected

If your MyInvois intermediary credentials are invalid or not authorised for the customer's TIN, LHDN validation fails and the invoice is not created.

json
// Response — HTTP 502
{
  "status": "error",
  "title": "TIN validation failed",
  "description": "LHDN rejected your intermediary credentials. Please check your MyInvois Client ID and Client Secret in Organisation Settings."
}

Webhooks

Webhooks

EasyInvois can push real-time event notifications to your server whenever an invoice status changes. Instead of polling GET /api/v1/invoices/:id, register a webhook URL and EasyInvois will POST a signed JSON payload to it automatically.

There are two webhook scopes — you may use one or both simultaneously:

Organisation webhook

Fires for invoices belonging to your organisation. Configure your webhook URL in the dashboard settings using your organisation API key.

Platform webhook

Fires for all organisations under your platform. Configure your platform webhook URL in the dashboard settings using your platform key.

Keep your webhook secret safe. When you configure your webhook in the dashboard we will provide a signing secret (whs_...). Store it securely — it is required to verify that incoming webhook requests genuinely came from EasyInvois. If the secret is compromised, contact us to rotate it.

Events & Payloads

Every webhook POST uses the same outer envelope structure. The data object contains the event-specific fields and differs per event type.

Envelope fields (all events)

FieldTypeDescription
eventstringEvent type identifier. Tells you what happened. See event types below.
created_atdatetimeISO 8601 UTC timestamp of when the event was dispatched by EasyInvois.
dataobjectEvent-specific payload. Fields inside vary by event type — see each event below.

invoice.lhdn_status_changed

Fired whenever LHDN updates the status of an invoice — when it is accepted (valid), rejected (invalid), or cancelled (cancelled). The data fields present depend on which status was set — see the table below.

FieldTypePresent whenDescription
invoice_iduuidalwaysUUID of the invoice whose LHDN status changed.
invoice_numberstringalwaysHuman-readable invoice number (e.g. ACME-2025-0001).
organization_iduuidalwaysUUID of the organisation that owns the invoice.
lhdn_statusstringalwaysNew LHDN status. One of: "valid", "invalid", "cancelled".
lhdn_uuidstringvalid onlyUnique document UUID assigned by LHDN upon validation. Use this to look up the invoice on the MyInvois portal.
lhdn_long_idstringvalid onlyLHDN long ID string — a human-readable reference for the validated document on the MyInvois portal.
lhdn_date_time_validateddatetimevalid onlyISO 8601 UTC timestamp of when LHDN validated the invoice. The 72-hour cancellation window starts from this time.
lhdn_status_reasonstringinvalid onlyLHDN rejection reason returned by MyInvois when the invoice is invalid.
cancellation_reasonstringcancelled onlyThe reason provided by the user when the invoice was cancelled via the MyInvois API.
lhdn_cancelled_atdatetimecancelled onlyISO 8601 UTC timestamp of when the invoice was cancelled.
json
// Example: lhdn_status = "valid"
{
  "event": "invoice.lhdn_status_changed",
  "created_at": "2025-04-13T10:30:45.123Z",
  "data": {
    "invoice_id":               "i1j2k3l4-...",
    "invoice_number":           "ACME-2025-0001",
    "organization_id":          "o9p8q7r6-...",
    "lhdn_status":              "valid",
    "lhdn_uuid":                "f1e2d3c4-...",
    "lhdn_long_id":             "LHDN_LONG_ID_STRING",
    "lhdn_date_time_validated": "2025-04-13T10:30:00.000Z"
  }
}

// Example: lhdn_status = "invalid"
{
  "event": "invoice.lhdn_status_changed",
  "created_at": "2025-04-13T10:31:00.000Z",
  "data": {
    "invoice_id":         "i1j2k3l4-...",
    "invoice_number":     "ACME-2025-0001",
    "organization_id":    "o9p8q7r6-...",
    "lhdn_status":        "invalid",
    "lhdn_status_reason": "TIN does not match the provided ID document"
  }
}

// Example: lhdn_status = "cancelled"
{
  "event": "invoice.lhdn_status_changed",
  "created_at": "2025-04-13T10:35:00.000Z",
  "data": {
    "invoice_id":          "i1j2k3l4-...",
    "invoice_number":      "ACME-2025-0001",
    "organization_id":     "o9p8q7r6-...",
    "lhdn_status":         "cancelled",
    "cancellation_reason": "Wrong amount entered",
    "lhdn_cancelled_at":   "2025-04-13T10:35:00.000Z"
  }
}

invoice.buyer_submitted

Fired when a buyer successfully submits their identity (TIN + ID document) via the buyer link and LHDN validates their TIN. Use this event to know when it is safe to finalize and submit the invoice to LHDN — the buyer's TIN is now confirmed and linked to the invoice.

FieldTypeDescription
invoice_iduuidUUID of the invoice that the buyer submitted their details for.
invoice_numberstringHuman-readable invoice number (e.g. ACME-2025-0001).
organization_iduuidUUID of the organisation that issued the invoice.
buyer_statusstringAlways "submitted". Confirms the buyer has completed the TIN verification step.
id_typestringType of identity document the buyer verified with. One of: "NRIC", "BRN", "PASSPORT", "ARMY".
json
{
  "event": "invoice.buyer_submitted",
  "created_at": "2025-04-13T10:28:00.000Z",
  "data": {
    "invoice_id":      "i1j2k3l4-...",
    "invoice_number":  "ACME-2025-0001",
    "organization_id": "o9p8q7r6-...",
    "buyer_status":    "submitted",
    "id_type":         "NRIC"
  }
}
Privacy note: The buyer's TIN and ID number are not included in the webhook payload. To retrieve the verified TIN and customer details, call GET /api/v1/invoices/:id — the invoice will have a linked customer record with the validated information.

Signature Verification

Every webhook request is signed with HMAC-SHA256 using your webhook secret. Always verify the signature before processing the payload to ensure the request came from EasyInvois and was not tampered with.

Request headers

HeaderValue
Content-Typeapplication/json
X-EasyInvois-EventEvent type, e.g. invoice.lhdn_status_changed
X-EasyInvois-TimestampUnix timestamp in seconds (integer as string)
X-EasyInvois-Signaturesha256=<hex digest>

How to verify

Compute HMAC-SHA256(secret, "{timestamp}.{rawBody}") and compare the hex digest to the value in X-EasyInvois-Signature (after stripping the sha256= prefix). Also reject requests where the timestamp is more than 5 minutes old to prevent replay attacks.

typescript
// Node.js / TypeScript example
import { createHmac, timingSafeEqual } from 'crypto'

function verifyWebhook(
  rawBody: string,       // raw request body string (do NOT parse first)
  timestamp: string,     // X-EasyInvois-Timestamp header
  signature: string,     // X-EasyInvois-Signature header
  secret: string,        // your webhook_secret (whs_...)
): boolean {
  // 1. Reject stale requests (> 5 minutes old)
  const age = Math.abs(Date.now() / 1000 - Number(timestamp))
  if (age > 300) return false

  // 2. Compute expected signature
  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')

  // 3. Constant-time comparison
  const sigHex = signature.replace(/^sha256=/, '')
  return timingSafeEqual(Buffer.from(sigHex), Buffer.from(expected))
}
Important: Read the raw request body as a string before parsing JSON. Parsing and re-serialising will change whitespace and break the signature check.

Delivery & Retries

EasyInvois attempts delivery immediately after an event fires. If your endpoint does not respond with HTTP 2xx within 10 seconds, the delivery is retried with exponential backoff up to 4 times before being abandoned.

HTTP 4xx responses are treated as permanent failures and are not retried — they indicate a configuration problem on your side (wrong URL, auth failure, etc.). HTTP 5xx responses and timeouts are retried.

Retry schedule

AttemptDelay after failure
1st retry1 minute
2nd retry10 minutes
3rd retry1 hour
4th retry6 hours
AbandonedNo further attempts
Make your endpoint idempotent. Retries deliver the same invoice_id and event type more than once if earlier attempts timed out. Use the invoice_id + event combination to deduplicate processing on your side.

Reference

GET/api/v1/mobile-prefixesNo auth required

Returns all supported mobile country prefixes. Use the id from this response as the mobile_prefix_id when creating customers. No authentication is required — call this once and cache the result.

json
HTTP/1.1 200 OK
{
  "status": "success",
  "data": {
    "mobile_prefixes": [
      {
        "id":           "uuid-...",
        "country_code": "MY",
        "dial_code":    "+60",
        "country_name": "Malaysia"
      },
      {
        "id":           "uuid-...",
        "country_code": "SG",
        "dial_code":    "+65",
        "country_name": "Singapore"
      }
    ]
  }
}

Supported countries:

Countrycountry_codedial_code
MalaysiaMY+60
SingaporeSG+65
BruneiBN+673
IndonesiaID+62
ThailandTH+66
PhilippinesPH+63
VietnamVN+84
MyanmarMM+95
ChinaCN+86
Hong KongHK+852
TaiwanTW+886
JapanJP+81
South KoreaKR+82
IndiaIN+91
AustraliaAU+61
New ZealandNZ+64
United StatesUS+1
United KingdomGB+44
UAEAE+971
Saudi ArabiaSA+966
PakistanPK+92
BangladeshBD+880
The id (UUID) for each prefix differs between production and staging environments. Always call this endpoint to get the correct UUIDs — do not hardcode them.

Classification Codes

LHDN MyInvois classification codes for items[].classification_code. Default is 022 (Others).

CodeDescription
001Breastfeeding equipment
002Child care centres and kindergartens fees
003Computer, smartphone or tablet
004Consolidated e-Invoice
005Construction materials
006Disbursement
007Donation
008e-Commerce - e-Invoice to buyer / purchaser
009e-Commerce - Self-billed e-Invoice to seller, logistics, etc.
010Education fees
011Goods on consignment (Consignor)
012Goods on consignment (Consignee)
013Gym membership
014Insurance - Education and medical benefits
015Insurance - Takaful or life insurance
016Interest and financing expenses
017Internet subscription
018Land and building
019Medical examination for learning disabilities and early intervention or rehabilitation treatments of learning disabilities
020Medical examination or vaccination expenses
021Medical expenses for serious diseases
022Others (default)
023Petroleum operations
024Private retirement scheme or deferred annuity scheme
025Motor vehicle
026Subscription of books / journals / magazines / newspapers / other similar publications
027Reimbursement
028Rental of motor vehicle
029EV charging facilities (Installation, rental, sale / purchase or subscription fees)
030Repair and maintenance
031Research and development
032Foreign income
033Self-billed - Betting and gaming
034Self-billed - Importation of goods
035Self-billed - Importation of services
036Self-billed - Others
037Self-billed - Monetary payment to agents, dealers or distributors
038Sports equipment, rental / entry fees for sports facilities, registration in sports competition or sports training fees
039Supporting equipment for disabled person
040Voluntary contribution to approved provident fund
041Dental examination or treatment
042Fertility treatment
043Treatment and home care nursing, daycare centres and residential care centers
044Vouchers, gift cards, loyalty points, etc
045Self-billed - Non-monetary payment to agents, dealers or distributors

Unit Codes

UN/CEFACT unit of measure codes for items[].unit_code. Default is C62 (Unit / Each).

CodeDescription
C62Unit / Each (default)
HURHour
DAYDay
MONMonth
ANNYear / Annual
KGMKilogram
GRMGram
TNETonne
MTRMetre
CMTCentimetre
MMTMillimetre
KMTKilometre
MTKSquare metre
MTQCubic metre
LTRLitre
MLTMillilitre
SETSet
PRPair
DZNDozen
BXBox
CTCarton
PKPackage
RLRoll
For questions or to request a platform key, contact us at contact@easysyncsolutions.com.