e-invoicing2026-07-09

LHDN MyInvois API 载荷指南:JSON 架构、字段验证与直接传送电子发票

从真实账单字段建立 MyInvois UBL 2.1 JSON,核对买卖双方、税务与总额,再完成哈希、Base64 编码及安全直传。

GetPay Engineering Team
更新于: 2026-07-30

核心要点 (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 数组。数组内第一份发票依序承载文件编号、开具日期与时间、文件类型、币种、供应商、买方、逐项明细、税额总计和法定金额总计。

实际提交前,可按这个次序检查:

  1. 从租户的账单、项目、供应商和买方记录建立会计文件。
  2. 确认每个商业识别码放在正确的交易方,并配上正确的 schemeID
  3. 逐项核对 InvoiceLine.LineExtensionAmount 和该行的 TaxTotal
  4. 用小计、税额和总额复核 LegalMonetaryTotal
  5. 完成后只把对象转成字符串一次;计算哈希后不可再修改。
  6. 把该字符串包装成提交文件,并以该租户自己的访问令牌传送。

以下是 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_numberInvoice[0].ID[0]._租户内部文件编号
invoice.issue_dateIssueDate[0]._预期格式为 YYYY-MM-DD
固定普通发票类型InvoiceTypeCode[0]_01listVersionID1.1
固定申报币种DocumentCurrencyCode[0]._MYR
supplier.tin供应商 PartyIdentificationschemeID: "TIN"
supplier.registration供应商 PartyIdentificationschemeID: "BRN"
supplier.sst_registration供应商 PartyIdentification可选的 schemeID: "SST"
supplier.msic_codeIndustryClassificationCode可选,并附上行业名称
买方 TIN买方 PartyIdentification明确传入值、账单值、一般公众后备值
item.quantityInvoicedQuantity[0]._普通流程采用 unitCode: "XUN"
item.amount该行 LineExtensionAmount[0]._未含税的项目金额
item.unit_pricePriceAmount[0]._MYR 单价
invoice.tax_amount文件层 TaxTotal同时准确分配到各行
invoice.subtotal行项目总额及未含税总额两个文件总计都取自小计
invoice.total含税总额及应付总额两个文件总计都取自总额

数组和内层的 _ 值是这个 JSON 表达方式的一部分。把 ID: [{ _: "INV-1001" }] 简化成 ID: "INV-1001",就已经改变架构。同样,schemeIDcurrencyIDlistVersionIDlistIDlistAgencyID 是属性,不是可随意填写的说明文字。

直接提交前应执行哪些字段验证?

验证必须配合文件流程。仅仅证明 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 主机。凭证取自该租户的公司记录,而不是所有公司共用一个运行环境凭证。

直接传送分成六步:

  1. client_idclient_secretgrant_type: "client_credentials"scope: "InvoicingAPI" 申请访问令牌。
  2. 采用回应内真正的 expires_in。GetPay 会缓存令牌,但剩余时间不超过 60 秒就重新申请。
  3. 把一份或多份四字段提交文件放入 { "documents": [...] }
  4. Content-Type: application/jsonAuthorization: Bearer <token> 把 JSON 送往文件提交操作。
  5. 保存回应的 submissionUid、每份获接受文件的 uuidinvoiceCodeNumber,以及被拒文件真正返回的错误代码和说明。
  6. 以 UUID 读取文件详情,区分 SubmittedValidInvalidCancelled

申请令牌和读取文件详情不会建立新申报,因此可以退避重试。文件提交则不可以。如果网络回应遗失,电子发票可能已经存在于 LHDN;自动重发可能建立第二份文件。系统应把结果视为未知,并从已保存的提交证据进行核对。

LHDN XML 发票格式应怎样处理?

UBL 经常以 XML 元素名称来说明,但 GetPay 已实现的传送格式是 JSON。AccountingSupplierPartyInvoiceLineTaxTotalLegalMonetaryTotal 等概念依然相通,不过序列化规则并不相同。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。

来源与权威依据

Direct LHDN MyInvois Submission Engine

Ready to automate your Malaysian e-invoicing & bookkeeping?

GetPay handles 100% compliant e-invoices, multi-bank reconciliation, and statutory payroll out of the box.

Get Started Free

相关文章