LHDN MyInvois 电子发票 API 故障排查:先看证据,不猜错误码
依据 GetPay 实际程序说明 MyInvois 身份验证、UBL 2.1 文件封装、提交防重、状态查询与税务错误证据的排查方法。
核心要点 (TL;DR)
- •先确认失败的是令牌、文件提交、状态查询还是作废/拒收,再查看真实 HTTP 状态与返回内容;GetPay 不虚构通用错误码表。
- •GetPay 只序列化一次 UBL JSON,并以同一份 UTF-8 字节计算 SHA-256 与生成 Base64 文件。
- •令牌申请与状态查询可安全重试;电子发票提交和状态变更只有超时保护,绝不自动重发。
- •本地 B2C 买方没有税号时可使用马来西亚公众通用 TIN;外国通用 TIN 只用于符合条件的境外个人供应商自开票流程。
先确认哪一项税务操作失败
排查 MyInvois 电子发票问题时,应按照证据顺序处理:
- 确认失败点是令牌申请、文件提交、状态查询,还是作废/拒收。
- 保存实际 HTTP 状态和 LHDN 返回内容。
- 对照被计算哈希的 UBL 文件与实际 Base64 编码字节。
- 明确失败操作后,才修改企业资料或 UBL 映射规则。
GetPay 不会把所有失败强行套进自创的错误名称。原生接口客户程序会保留 LHDN 的真实返回证据,同时限制文字长度,避免运行记录无限膨胀。
| GetPay 操作 | 方法与路径 | 重试原则 | 失败时保留的资料 |
|---|---|---|---|
| 申请访问令牌 | POST /connect/token | 退避重试 | 状态与首 200 个字符 |
| 提交电子发票 | POST /api/v1.0/documentsubmissions | 只设超时;不重试 | 状态与首 500 个字符 |
| 查询文件详情 | GET /api/v1.0/documents/{uuid}/details | 退避重试 | 状态与首 300 个字符 |
| 作废或拒收 | PUT /api/v1.0/documents/state/{uuid}/state | 只设超时;不重试 | 状态与首 300 个字符 |
令牌有效期以 expires_in 为准
GetPay 通过客户凭证模式申请 InvoicingAPI 权限令牌。系统不会假设每个令牌都有固定时长,而是采用 LHDN 返回的 expires_in,并在服务器进程内暂存令牌。
只有剩余时间超过 60 秒时,缓存令牌才会继续使用:
const cached = tokenCache.get(key)
if (cached && cached.exp > Date.now() + 60_000) return cached.token
tokenCache.set(key, {
token: json.access_token,
exp: Date.now() + json.expires_in * 1000,
})
令牌申请不会提交会计文件,因此可以采用退避重试。若申请失败,应先检查状态与返回文字,再决定是否更换凭证或调整权限范围。
哈希与 Base64 必须来自同一份 JSON
buildSubmissionDocument() 只建立一次 JSON 字符串,并从同一内容生成两个传输字段:
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")
切勿先为一份 JSON 计算哈希,修改对象后再生成 Base64。即使只相差一个字节,documentHash 也不再对应实际提交的文件。
GetPay 的提交封套包括:
format: "JSON";- Base64
document; - SHA-256 十六进制
documentHash;以及 - 企业内部发票号码
codeNumber。
成功响应预期包含 submissionUid、带 uuid 的已接收文件,以及带 LHDN 实际错误对象的被拒文件。若 LHDN 没有返回代码,GetPay 不会自行补造一个。
网络正常不代表 UBL 一定正确
一般销售电子发票由 GetPay 映射为 01 类别、1.1 版本及 MYR 币值。供应方资料包括 TIN 与商业注册号码;本地 B2C 买方没有 TIN 时,可回退至 EI00000000010。
EI00000000030 并不是所有境外交易的买方备用税号。在 GetPay 中,它只用于自开票个人供应商流程:供应商位于马来西亚境外,而且没有提供马来西亚 TIN。
自开票流程还会预先检查:
- 供应商身份证明种类与号码;
- 国家、州属、邮编与五位 MSIC;
- E.164 格式电话号码;
- 金额为正数且精确至仙;以及
- 所有明细金额必须与付款凭单总额一致。
税额必须准确至每一仙
GetPay 采用 Hamilton 最大余数分配法,使每行税额保持非负,并确保所有行的仙值正好等于整张文件的税额:
const exact = weights.map((weight) => (totalCents * weight) / totalWeight)
exact.forEach((share, index) => {
cents[index] = Math.floor(share)
})
let remainder = totalCents - cents.reduce((sum, value) => sum + value, 0)
如果每行各自四舍五入,小额税款可能被过度分配,最后一行甚至出现负税额。提交前修正计算,比事后猜测验证失败原因更稳妥。
超时表示结果尚未确定
文件提交使用 fetchWithTimeout,不会调用重试函数。提交前,GetPay 先以原子数据库操作把该发票的 LHDN 状态设为 SUBMITTING,防止两个请求同时提交同一张发票。但若网络回复遗失,这项本地保护并不能证明 LHDN 是否已经接收文件。
发生超时后:
- 不要直接重发同一文件;
- 保留本地提交状态与完整负载证据;
- 已取得
uuid时,可通过文件详情接口查询状态; - 未取得
uuid时,应通过营运提交记录进行核对,不能擅自判定成功或失败。
作废与拒收同样是会改变税务状态的操作,所以只设超时上限,不自动重放。
企业排查清单
- 确认环境与凭证来自正确租户的公司记录。
- 记录失败操作、真实 HTTP 状态与返回文字。
- 确认 SHA-256 和 Base64 来自同一 JSON 字符串。
- 按交易流程检查文件类别、版本、TIN 角色、注册方案、MSIC、地址、电话与仙值精度。
- 核对所有行税额总和与文件税额完全一致。
- 不要把“没有收到网络回复”当作“LHDN 没有处理”的证据。
常见问题 (FAQ)
为什么电子发票提交超时后,GetPay 不会自动重试?
即使回复没有传回 GetPay,LHDN 网关仍可能已接收文件。再次发送同一项 POST 请求可能产生重复文件。因此,GetPay 先以原子数据库操作把本地状态设为 SUBMITTING,再发出有超时上限但不会自动重试的提交请求。
所有买方和供应商都使用同一个备用 TIN 吗?
不是。一般本地 B2C 买方没有 TIN 时,可使用 EI00000000010。EI00000000030 只出现在 GetPay 自开票个人供应商映射中,用于没有马来西亚 TIN 的合资格境外供应商。
GetPay 会保留哪些 MyInvois 错误资料?
接口返回非成功状态时,GetPay 会保留操作名称、HTTP 状态以及限定长度的返回内容。成功的提交结果分为 acceptedDocuments 与 rejectedDocuments;被拒文件可包含 LHDN 实际返回的代码和说明。
来源与权威依据
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 外币栏位。
2026 年马来西亚销售与服务税(SST 8%):开票、贷项通知单与 SST-02 申报指南
面向马来西亚企业的 2026 年实务指南:正确区分 8% 与 6% 服务税、开具发票和贷项通知单、记录会计分录,并复核 SST-02 工作底稿。
LHDN MyInvois API 载荷指南:JSON 架构、字段验证与直接传送电子发票
从真实账单字段建立 MyInvois UBL 2.1 JSON,核对买卖双方、税务与总额,再完成哈希、Base64 编码及安全直传。