LHDN MyInvois API 载荷指南:JSON 架构、字段验证与直接传送电子发票
从真实账单字段建立 MyInvois UBL 2.1 JSON,核对买卖双方、税务与总额,再完成哈希、Base64 编码及安全直传。
核心要点 (TL;DR)
- •GetPay 生成 UBL 2.1 发票的 JSON 版本:命名空间、Invoice 数组、供应商与买方、逐项 InvoiceLine、TaxTotal 和 LegalMonetaryTotal 都在同一份文件内。
- •现有普通发票映射固定采用文件类型 01、版本 1.1 和 MYR;只有在没有买方 TIN 时,才以 EI00000000010 作为一般公众的后备 TIN。
- •验证必须按文件流程处理:普通发票确保逐行税额与整张文件税额相符;自开电子发票验证身份和仙位精度;合并 B2C 发票遇到未知税务类型时直接拒绝,不代猜 LHDN 类别。
- •直接传送前只做一次 JSON.stringify,以同一组 UTF-8 字节计算 SHA-256 和 Base64;文件提交 POST 即使超时也不可自动重试。
一份有效的 MyInvois JSON 载荷包含什么?
GetPay 的普通电子发票,是以 JSON 表达的 UBL 2.1 Invoice。根层有 _D、_A、_B 三个命名空间识别码,以及一个 Invoice 数组。数组内第一份发票依序承载文件编号、开具日期与时间、文件类型、币种、供应商、买方、逐项明细、税额总计和法定金额总计。
实际提交前,可按这个次序检查:
- 从租户的账单、项目、供应商和买方记录建立会计文件。
- 确认每个商业识别码放在正确的交易方,并配上正确的
schemeID。 - 逐项核对
InvoiceLine.LineExtensionAmount和该行的TaxTotal。 - 用小计、税额和总额复核
LegalMonetaryTotal。 - 完成后只把对象转成字符串一次;计算哈希后不可再修改。
- 把该字符串包装成提交文件,并以该租户自己的访问令牌传送。
以下是 GetPay 普通发票映射器实际生成的完整结构;数值仅作说明:
{
"_D": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
"_A": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
"_B": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
"Invoice": [
{
"ID": [{ "_": "INV-1001" }],
"IssueDate": [{ "_": "2026-07-30" }],
"IssueTime": [{ "_": "00:00:00Z" }],
"InvoiceTypeCode": [{ "_": "01", "listVersionID": "1.1" }],
"DocumentCurrencyCode": [{ "_": "MYR" }],
"AccountingSupplierParty": [{
"Party": [{
"IndustryClassificationCode": [{ "_": "62010", "name": "Business activity" }],
"PartyIdentification": [
{ "ID": [{ "_": "SUPPLIER_TIN", "schemeID": "TIN" }] },
{ "ID": [{ "_": "SUPPLIER_REGISTRATION", "schemeID": "BRN" }] },
{ "ID": [{ "_": "SUPPLIER_SST", "schemeID": "SST" }] }
],
"PartyName": [{ "Name": [{ "_": "Example Supplier Sdn. Bhd." }] }],
"PostalAddress": [{
"AddressLine": [{ "Line": [{ "_": "Example address" }] }],
"CityName": [{ "_": "Kuala Lumpur" }],
"PostalZone": [{ "_": "50000" }],
"CountrySubentityCode": [{ "_": "14" }],
"Country": [{ "IdentificationCode": [{ "_": "MYS", "listID": "ISO3166-1", "listAgencyID": "6" }] }]
}],
"PartyLegalEntity": [{ "RegistrationName": [{ "_": "Example Supplier Sdn. Bhd." }] }],
"Contact": [{ "Telephone": [{ "_": "NA" }], "ElectronicMail": [{ "_": "NA" }] }]
}]
}],
"AccountingCustomerParty": [{
"Party": [{
"PartyIdentification": [
{ "ID": [{ "_": "BUYER_TIN", "schemeID": "TIN" }] },
{ "ID": [{ "_": "BUYER_REGISTRATION", "schemeID": "BRN" }] }
],
"PartyName": [{ "Name": [{ "_": "Example Buyer" }] }],
"PostalAddress": [{
"AddressLine": [{ "Line": [{ "_": "NA" }] }],
"CityName": [{ "_": "NA" }],
"PostalZone": [{ "_": "00000" }],
"CountrySubentityCode": [{ "_": "00" }],
"Country": [{ "IdentificationCode": [{ "_": "MYS", "listID": "ISO3166-1", "listAgencyID": "6" }] }]
}],
"PartyLegalEntity": [{ "RegistrationName": [{ "_": "Example Buyer" }] }],
"Contact": [{ "Telephone": [{ "_": "NA" }], "ElectronicMail": [{ "_": "buyer@example.com" }] }]
}]
}],
"InvoiceLine": [{
"ID": [{ "_": "1" }],
"InvoicedQuantity": [{ "_": 2, "unitCode": "XUN" }],
"LineExtensionAmount": [{ "_": 200, "currencyID": "MYR" }],
"TaxTotal": [{ "TaxAmount": [{ "_": 12, "currencyID": "MYR" }] }],
"Item": [{ "Description": [{ "_": "Implementation service" }] }],
"Price": [{ "PriceAmount": [{ "_": 100, "currencyID": "MYR" }] }]
}],
"TaxTotal": [{ "TaxAmount": [{ "_": 12, "currencyID": "MYR" }] }],
"LegalMonetaryTotal": [{
"LineExtensionAmount": [{ "_": 200, "currencyID": "MYR" }],
"TaxExclusiveAmount": [{ "_": 200, "currencyID": "MYR" }],
"TaxInclusiveAmount": [{ "_": 212, "currencyID": "MYR" }],
"PayableAmount": [{ "_": 212, "currencyID": "MYR" }]
}]
}
]
}
这段示例展示的是结构,不是一套通用于所有企业的值。例如,供应商有 MSIC 才会出现 IndustryClassificationCode,有 SST 注册号码才会多一个 SST PartyIdentification。示例里的识别码绝不可直接拿去申报。
原始账单字段如何映射到 UBL 2.1?
GetPay 的普通发票映射器把数据来源和 JSON 目的地明确连接:
| 原始值 | UBL JSON 位置 | GetPay 的处理 |
|---|---|---|
invoice.invoice_number | Invoice[0].ID[0]._ | 租户内部文件编号 |
invoice.issue_date | IssueDate[0]._ | 预期格式为 YYYY-MM-DD |
| 固定普通发票类型 | InvoiceTypeCode[0] | _ 为 01;listVersionID 为 1.1 |
| 固定申报币种 | DocumentCurrencyCode[0]._ | MYR |
supplier.tin | 供应商 PartyIdentification | schemeID: "TIN" |
supplier.registration | 供应商 PartyIdentification | schemeID: "BRN" |
supplier.sst_registration | 供应商 PartyIdentification | 可选的 schemeID: "SST" |
supplier.msic_code | IndustryClassificationCode | 可选,并附上行业名称 |
| 买方 TIN | 买方 PartyIdentification | 明确传入值、账单值、一般公众后备值 |
item.quantity | InvoicedQuantity[0]._ | 普通流程采用 unitCode: "XUN" |
item.amount | 该行 LineExtensionAmount[0]._ | 未含税的项目金额 |
item.unit_price | PriceAmount[0]._ | MYR 单价 |
invoice.tax_amount | 文件层 TaxTotal | 同时准确分配到各行 |
invoice.subtotal | 行项目总额及未含税总额 | 两个文件总计都取自小计 |
invoice.total | 含税总额及应付总额 | 两个文件总计都取自总额 |
数组和内层的 _ 值是这个 JSON 表达方式的一部分。把 ID: [{ _: "INV-1001" }] 简化成 ID: "INV-1001",就已经改变架构。同样,schemeID、currencyID、listVersionID、listID 和 listAgencyID 是属性,不是可随意填写的说明文字。
直接提交前应执行哪些字段验证?
验证必须配合文件流程。仅仅证明 JSON 语法正确,并不代表电子发票的业务内容、税务归类和金额都正确。
普通发票映射器固定类型 01、版本 1.1、币种 MYR,供应商国家为 MYS。TIN、BRN、可选 SST 注册号码及可选 MSIC 都按各自的 scheme 放置。买方 TIN 缺失时可以后备为 EI00000000010;若买方是已知注册实体,就应保留对方实际 TIN。
税额必须逐仙对得上。GetPay 先把整张发票税额四舍五入成仙,再按正数行金额采用最大余数法分配。每行税额不会是负数,而且所有行税额的总和必定等于文件税额。金额为零或负数的权重行不会获分配税额;如果所有行都没有正权重,则把整笔税额放在最后一行,避免除以零。
其他文件流程有更严格的防线。个人供应商的自开电子发票要求付款凭单总额为正数,而且各行必须逐仙准确加回总额。它会验证供应商身份种类、已提供 TIN 的格式、身份与国家的关系、地址、E.164 电话格式、五位数 MSIC、马来西亚州属和邮编格式、每行正数金额,以及最多两位小数。它生成的是类型 11,不是 01;交易方也会反转,因为收款人是供应商,租户则是代供应商开票的买方。
合并 B2C 电子发票会把每张来源账单映射成独立一行。来源账单的小计成为该行 LineExtensionAmount,来源税额留在该行 TaxTotal,分类 004 则放进 ItemClassificationCode,并使用 listID: "CLASS"。税务类型映射采取封闭式处理:
service映射为税务类别02;sales映射为01;exempt映射为E;以及- 任何其他
tax_type都会抛出错误,绝不代猜。
这些是映射器采用的 LHDN 税务类别,不是任意内部总账代码。文件层 TaxSubtotal 会按映射后的类别分组汇总,而每一行仍可保留自己的税率百分比。
JSON 应如何计算哈希并转成 Base64?
UBL 对象不会原样直接 POST。buildSubmissionDocument() 只建立一个标准字符串,再由它产生两个完整性字段:
const canonical = JSON.stringify(ublDoc)
const bytes = new TextEncoder().encode(canonical)
const hashBuf = await crypto.subtle.digest("SHA-256", bytes)
const documentHash = [...new Uint8Array(hashBuf)]
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
const document = Buffer.from(canonical, "utf8").toString("base64")
最后的单份提交项目只有四个字段:
{
"format": "JSON",
"document": "JSON_STRING_的_BASE64",
"documentHash": "同一组_UTF8_字节的小写_SHA256_十六进制值",
"codeNumber": "INV-1001"
}
这两个步骤之间,不可重新美化排版、调整键的次序或修改对象。LHDN 收到的是 Base64 解码后的文件,因此 documentHash 必须准确描述同一组字节。codeNumber 是 GetPay 用来追踪的租户账单编号,它不在 Base64 文件里面。
MyInvois API 直接传送流程如何运作?
GetPay 使用每个租户各自的凭证,并选择两个环境之一:sandbox 采用 MyInvois 预生产 API 主机,production 采用正式 API 主机。凭证取自该租户的公司记录,而不是所有公司共用一个运行环境凭证。
直接传送分成六步:
- 以
client_id、client_secret、grant_type: "client_credentials"和scope: "InvoicingAPI"申请访问令牌。 - 采用回应内真正的
expires_in。GetPay 会缓存令牌,但剩余时间不超过 60 秒就重新申请。 - 把一份或多份四字段提交文件放入
{ "documents": [...] }。 - 以
Content-Type: application/json和Authorization: Bearer <token>把 JSON 送往文件提交操作。 - 保存回应的
submissionUid、每份获接受文件的uuid与invoiceCodeNumber,以及被拒文件真正返回的错误代码和说明。 - 以 UUID 读取文件详情,区分
Submitted、Valid、Invalid和Cancelled。
申请令牌和读取文件详情不会建立新申报,因此可以退避重试。文件提交则不可以。如果网络回应遗失,电子发票可能已经存在于 LHDN;自动重发可能建立第二份文件。系统应把结果视为未知,并从已保存的提交证据进行核对。
LHDN XML 发票格式应怎样处理?
UBL 经常以 XML 元素名称来说明,但 GetPay 已实现的传送格式是 JSON。AccountingSupplierParty、InvoiceLine、TaxTotal 和 LegalMonetaryTotal 等概念依然相通,不过序列化规则并不相同。JSON 数组、承载值的 _,以及类似属性的 currencyID,都是这条 SDK 流程实际传送对象的一部分。
因此,不可把一段 XML 放进 document 后标成 JSON,也不可对一种表达方式计算哈希,却把另一种表达方式编码。如果某个整合从 XML 开始,就需要一套另行验证的 XML 至 MyInvois JSON 转换。GetPay 的原生流程直接从会计记录建立 JSON UBL 文件,在直接传送前完成相应流程的验证,从而避免这层转换风险。
提交前检查清单
- 确认目标环境,以及该租户专属的凭证组合。
- 按实际文件流程确认文件类型和版本。
- 复核供应商与买方角色、TIN、注册 scheme、SST 注册、MSIC、地址和联络资料。
- 把每行数量、单价、未含税金额和税额与来源账单逐项比较。
- 确认各行税额加回文件税额,而且小计加税额与总额一致。
- 确认合并发票的税务类型已受支持,绝不代入猜测的类别。
- 只 stringify 一次,再以同一组 UTF-8 字节计算哈希和 Base64。
- 保存
submissionUid、UUID、被拒文件证据和状态回应。 - 文件提交 POST 即使超时,也绝不自动重试。
常见问题 (FAQ)
MyInvois API 接受 JSON,还是只接受 XML?
GetPay 直接提交 UBL 2.1 发票的 JSON 版本。提交封套的 format 是 JSON,document 则是该 JSON 字符串的 Base64 编码。XML 与 JSON 虽表达相近的 UBL 概念,但这条流程不会转换或提交 XML 字符串。
B2C 买方没有 TIN 时,GetPay 会使用哪个号码?
普通发票会依次采用明确传入的买方 TIN、账单内的 customer TIN,最后才使用一般公众 TIN EI00000000010。若已知买方是注册实体,就不应以一般公众 TIN 取代其真实 TIN。
documentHash 与 document 必须有什么关系?
两者必须源自同一次 JSON.stringify。GetPay 以该字符串的 UTF-8 字节计算小写 SHA-256 十六进制摘要,并把完全相同的字符串编码成 Base64 document。
MyInvois 文件提交超时后,可以自动重试吗?
不可以。收不到回应不代表 LHDN 没有接收;文件可能已经入档。GetPay 会限制提交请求的等候时间,但不会自动重试这个会改变状态的 POST。
来源与权威依据
Ready to automate your Malaysian e-invoicing & bookkeeping?
GetPay handles 100% compliant e-invoices, multi-bank reconciliation, and statutory payroll out of the box.
相关文章
多币种开票与外汇损益复式记账:马来西亚企业实务指南
马来西亚企业处理 USD、SGD 外币发票的实务指南:交易日换算、期末未实现外汇损益、收款时已实现损益、复式分录,以及 LHDN MyInvois 外币栏位。
LHDN MyInvois 电子发票 API 故障排查:先看证据,不猜错误码
依据 GetPay 实际程序说明 MyInvois 身份验证、UBL 2.1 文件封装、提交防重、状态查询与税务错误证据的排查方法。