开发者指南

商户 API 接入文档

从安全认证到支付结果通知,了解如何将代收与代付能力接入您的业务。

协议 v1JSON · RSA · AES-GCMIDR

开始接入

通过安全接口创建代收、代付订单,并通过查单或异步回调确认支付结果。

接入前请向平台获取

API Key、平台 RSA 公钥、接口环境地址和已开通的支付方式编号;向平台登记商户 RSA 公钥、服务器公网出口 IP 及代收 / 代付回调地址。商户私钥由商户自行保管。

接口环境

生产环境https://openapi.hzpay.arthapay24.com
测试环境https://sandbox-openapi.hzpay.arthapay24.com

以上地址来自当前接口配置,具体开通情况和地址以平台提供的信息为准。文档中的订单号、金额和支付方式编号仅作示例。

回调服务器出口 IP

平台发送商户回调时使用以下 IP;如您的回调接口配置了来源 IP 白名单,请添加这两个地址。

回调出口 IP16.79.84.220
回调出口 IP16.78.208.187

接入流程

  1. 向平台登记商户 RSA 公钥,获取商户 API Key、平台 RSA 公钥,并登记代收/代付回调地址和请求出口 IP。建议使用 HTTPS 回调地址,生产签名应在商户服务端完成。
  2. 在商户服务端按业务参数组装代收 / 代付明文 JSON。
  3. 生成 AES Key 与 IV,使用 AES-GCM 加密明文、平台公钥加密 AES Key、商户私钥签名。
  4. 以固定信封 body 调用对应 /merchant-api 接口,保存响应中的 platformOrderId;代收响应还会返回 cashierUrl。
  5. 代收引导付款人打开 cashierUrl 完成支付;代付通过回调或查单获取后续结果。
  6. 通过异步回调或按固定间隔主动查单,确认订单最终状态,不要用 HTTP 200 推断成功。
HTTP 200 或接口 success=true 只代表接口请求处理结果,不能作为付款成功的依据。请根据订单状态及回调确认结果。

签名与加密

所有商户接口使用 POST、JSON 加密信封和相同的安全请求头。普通接口同步响应为明文 JSON。

请求头

Header格式说明
Content-Typeapplication/json请求体固定为 JSON envelope。
X-Api-Key平台分配商户 API Key,用于识别商户身份。
X-TimestampUnix timestamp推荐秒级时间戳,服务端兼容毫秒,默认允许 300 秒偏差。
X-Nonce随机字符串同一商户 5 分钟内不可重复,长度不超过 128。
X-Encrypt-KeyBase64使用平台 RSA 公钥加密 AES Key 原始字节。
X-IVBase64AES-GCM IV,推荐 12 字节随机数。
X-SignBase64商户 RSA 私钥对签名串执行 SHA256withRSA。

构造请求

  1. 将业务 JSON 序列化为 UTF-8,随机生成 AES Key(16 / 24 / 32 字节,推荐 32 字节)及 12 字节 IV。
  2. 使用 AES-GCM 加密,认证标签为 128 bit。将密文和认证标签拼接后 Base64 编码,得到 cipherText。
  3. 使用平台 RSA 公钥,以 RSA/ECB/PKCS1Padding 加密 AES Key 原始字节并 Base64 编码,得到 X-Encrypt-Key;IV Base64 编码后放入 X-IV。
  4. 生成时间戳和唯一 nonce,以商户 RSA 私钥执行 SHA256withRSA 签名,再将签名字节 Base64 编码,放入 X-Sign。
  5. 将加密信封和所有请求头发送至接口。每次请求及重试均使用新的 AES Key、IV、时间戳和 nonce。
签名原文 · UTF-8X-Api-Key + X-Timestamp + X-Nonce + cipherText四段原始字符串直接拼接,不加分隔符、空格或换行。
请求体信封
{
  "cipherText": "BASE64_AES_GCM_CIPHER_TEXT"
}

时间戳推荐 Unix 秒,服务端兼容毫秒;默认允许 300 秒偏差。nonce 长度不超过 128 字符,默认 300 秒内同一商户不可重复。

接口清单

以下路径均拼接在平台提供的环境地址之后。支付方式编号及开通情况请联系平台获取。

方法路径用途状态
POST/merchant-api/collect/orders代收下单可对接
POST/merchant-api/collect/orders/{platformOrderId}/query按平台代收单号查单可对接
POST/merchant-api/collect/orders/merchant/{merchantOrderId}/query按商户代收单号查单(IDR)可对接
POST/merchant-api/disburse/orders代付下单可对接
POST/merchant-api/disburse/orders/{platformOrderId}/query按平台代付单号查单可对接
POST/merchant-api/disburse/orders/merchant/{merchantOrderId}/query按商户代付单号查单(IDR)可对接
POST/merchant-api/accounts/balance余额查询可对接

支付方式编码

下单时在 method 中填写对应的支付方式编码,使用 JSON 整数。编码清单仅供选择,实际可用方式和限额以平台为商户开通的配置为准。

共 270 条编码

代收

钱包 8 条

支付方式支付编码
OVO202001
DANA202002
SHOPEEPAY202003
LINKAJA202004
GOPAY201017
DANA_FAKE202005
DANA_T+1202006
DANA_D0202007

银行 15 条

支付方式支付编码
BCA201001
MANDIRI201002
BSI201003
BRI201004
BNI201005
PERMATA201006
CIMB201007
DANAMON201008
OCBC201009
BTPN201010
SAHABAT_SAMPOERNA201011
SINARMAS201012
MAYBANK201013
SYARIAH201014
BNC201016

QRIS 1 条

支付方式支付编码
QRIS203001

代付

钱包 7 条

支付方式支付编码
OVO205001
DANA205002
GOPAY205003
SHOPEEPAY205004
LINKAJA205005
ASTRAPAY205006
ISAKU205007

银行 239 条

支付方式支付编码
BANK BCA204001
BANK BRI204002
BANK MANDIRI204003
BANK BNI204004
BANK DANAMON INDONESIA204005
BANK PERMATA204006
MAYBANK204007
BANK PANIN204008
BANK CIMB NIAGA204009
BANK UOB BUANA INDONESIA204010
BANK OCBC NISP204011
CITIBANK204012
BANK CCB INDONESIA204013
BANK ARTHA GRAHA204014
BANK MUFG204015
BANK DBS INDONESIA204016
BANK STANDARD CHARTERED204017
BANK CAPITAL204018
BANK ANZ INDONESIA204019
THE BANK OF CHINA204020
BANK BUMI ARTA204021
BANK HSBC204022
BANK JTRUST INDONESIA204023
BANK MAYAPADA INTERNATIONAL204024
BANK JABAR204025
BPD DKI JAKARTA204026
BPD DIY204027
BANK JATENG204028
BANK JATIM204029
BPD JAMBI204030
BPD ACEH204031
BANK SUMUT204032
BPD SUMATERA BARAT204033
BPD RIAU204034
BPD SUMSEL BABEL204035
BPD LAMPUNG204036
BPD KALSEL204037
BPD KALBAR204038
BPD KALTIMTARA204039
BPD KALTENG204040
BANK SULSELBAR204041
BANK SULUTGO204042
BPD NUSA TENGGARA BARAT204043
BPD BALI204044
Bank NTT204045
BPD MALUKU204046
BPD PAPUA204047
BPD BENGKULU204048
BPD SULAWESI TENGAH204049
BPD SULAWESI TENGGARA204050
BPD BANTEN204051
BANK OF INDIA INDONESIA204052
BANK MUAMALAT INDONESIA204053
BANK MESTIKA DHARMA204054
BANK SHINHAN INDONESIA204055
BANK SINARMAS204056
BANK MASPION INDONESIA204057
BANK GANESHA204058
BANK ICBC204059
BANK QNB KESAWAN204060
BANK BTN204061
BANK WOORI SAUDARA204062
BANK BTPN204063
BANK VICTORIA SYARIAH204064
BANK JABAR BANTEN SYARIAH204065
BANK MEGA204066
BANK KB BUKOPIN204067
BANK BSI (BANK SYARIAH INDONESIA)204068
BANK JASA JAKARTA204069
BANK KEB HANA204070
BANK MNC INTERNASIONAL204071
BANK NEO COMMERCE204072
BANK RAYA INDONESIA204073
BANK SBI INDONESIA204074
BANK DIGITAL BCA204075
BANK NATIONAL NOBU204076
BANK MEGA SYARIAH204077
BANK INA PERDANA204078
BANK PANIN DUBAI SYARIAH204079
BANK PRIMA MASTER204080
BANK BUKOPIN SYARIAH204081
BANK MUTIARA204129
BANK SAHABAT SAMPOERNA204082
BANK OKE INDONESIA204083
BANK SEABANK INDONESIA204084
BANK BCA SYARIAH204085
BANK JAGO204086
BANK BTPN SYARIAH204087
BANK MULTI ARTA SENTOSA204088
BANK MAYORA(Hibank)204089
BANK INDEX SELINDO204090
BANK MANDIRI TASPEN POS(Bank MANTAP)204091
BANK VICTORIA INTERNASIONAL204092
BANK ALLO204093
BANK IBK INDONESIA204094
BANK ALADIN SYARIAH204095
BANK CTBC INDONESIA204096
BANK COMMONWEALTH204097
BANK AMAR INDONESIA204098
BANK Andara204099
BANK Anglomas International204100
ARTAJASA PEMBAYARAN ELEKTRONIK204101
BANK Arta Niaga Kencana204102
BANK Bisnis Internasional204103
BANK Bangkok204104
Bank BNI Syariah204105
Bank BRI Syariah204106
BANK OF AMERICA204107
BPR KS(KARYAJATNIKA SADAYA)204108
Bank Tabungan Negara (BTN) UUS204109
Centratama Nasional Bank(CNB)204110
BANK CIMB Niaga UUS204111
BANK PEMBANGUNAN DAERAH DIY UNIT USAHA SYARIAH204112
BANK Danamon UUS204113
BANK Deutsche204114
BANK DKI UUS204115
BANK EKA204116
BANK Fama International204117
BANK Himpunan Saudara 1906204118
Hongkong and Shanghai Bank Corporation (HSBC)204119
BPD JAWA TENGAH UNIT USAHA SYARIAH204120
BANK Jatim UUS204121
JPMORGAN CHASE BANK204122
BPD Kalimantan Barat UUS204123
BPD Kalimantan Selatan UUS204124
BPD Kalimantan Timur UUS204125
BANK METRO EXPRESS204126
BANK Mitra Niaga204127
BANK Mizuho Indonesia204128
BPD Nusa Tenggara Barat (NTB) UUS204130
Bank Nusantara Parahyangan204176
Bank OCBC NISP UUS204131
Bank Permata UUS204132
BANK PUNDI INDONESIA204133
Rabobank International Indonesia204134
Bank Resona Perdania204135
BPD Riau Dan Kepri UUS204136
Bank Sulselbar UUS204137
BPD Sumatera Barat UUS204138
BANK NAGARI204139
Bank Sumsel Dan Babel UUS204140
Bank Sumut UUS204141
Bank Sumitomo Mitsui Indonesia204142
BANK BNP PARIBAS INDONESIA204143
BPR SUPRA ARTAPERSADA204144
Bank Syariah Mandiri204145
Bank BJB204146
Bank Tabungan Pensiunan Nasional204147
BPD Kalimantan Timur204148
BPD Riau Dan Kepri204149
BPD Kalimantan Barat204150
Hongkong and Shanghai Bank Corporation (HSBC) UUS204151
BPD Aceh UUS204152
BPD Nusa Tenggara Timur204153
BPD Jambi UUS204154
BPD Jawa Tengah204155
Bank Maybank Syariah Indonesia204156
Bank Agris204157
Bank Chinatrust Indonesia204158
Bank of America Merill-Lynch204159
Bank of Tokyo Mitsubishi UFJ204160
Bank Dinar Indonesia204177
BPD Jawa Timur UUS204161
Bank antardaerah204162
Bank harmoni international204163
Bank woori indonesia204164
Bank ekspor indonesia204165
Bank BJB Syariah204166
Bank BRI Agroniaga204167
BANK SINARMAS UUS204168
Virtual Account Bank Mandiri204169
Virtual Account Bank BRI204170
Virtual Account Bank CIMB204171
Virtual Account Bank Permata204172
Virtual Account Bank BCA204173
Virtual Account Bank BNI204174
Virtual Account Bank Danamon204175
BANK NANO SYARIAH204189
BANK NAGARI SYARIAH204178
BANK JAGO SYARIAH204179
BANK EKONOMI RAHARJA204180
BANK ROYAL INDONESIA204181
BANK YUDHA BAKTI (BNC)204182
BANK INTERIM INDONESIA204183
BANK PIKKO204184
BANK KESEJAHTERAAN204185
BANK SEABANK204186
BANK HANA204187
BANK MEGA TBK.204188
BANK UOB INDONESIA204191
BCA204190
BANK JENIUS204215
BANK Syariah Indonesia (eks BRI Syariah)204192
Bank Sulteng (Sulawesi Tengah)204193
Bank Sultra204194
Bank ING Indonesia204195
Bank Credit Agricole Indosuez204196
Bank ABN Amro204197
Bank Keppel Tatlee Buana204198
Bank Haga204199
Bank IFI204200
Bank Hagakita204201
Bank Windu Kentjana204202
Bank Swaguna204203
Bank Sri Partha204204
Bank Bintang Manunggal204205
Bank Harfa204207
Bank Persyarikatan Indonesia204208
Bank Akita204209
Bank Liman International204210
Bank Artos IND204211
Bank Purba Danarta204212
Bank Harda204213
Bank Merincorp204214
Citibank N.A204216
Bank Jawa Barat204218
Bank Syariah Indonesia (Eks BNI SYARIAH)204219
Bank Syariah Indonesia (Eks BSM)204220
Bank KB Bukopin Syariah204221
Bank Lippo204222
Indosat Dompetku204223
Telkomsel Tcash204224
Digibank204225
TMRW by UOB Indonesia204226
BANK NATIONALNOBU204227
KUSTODIAN SENTRAL EFEK INDONESIA204228
BANK RAKYAT INDONESIA (PERSERO)204229
BANK CENTRAL ASIA204230
BPD KALIMANTAN TENGAH204231
BPD KALIMANTAN SELATAN204332
BANK TABUNGAN PENSIUNAN NASIONAL Syariah204333
BANK NEGARA INDONESIA (PERSERO)204334
BANK TABUNGAN NEGARA (PERSERO)204335
BANK BPD SULAWESI SELATAN204336
BANK INDONESIA204337
BPD JAWA TIMUR204338
BPD DAERAH ISTIMEWA YOGYAKARTA204339
BANK ROYAL OF SCOTLAND(RBS)204340
BANK BSG( Bank sulawesi Utara dan Gorontalo)204341

代收下单

POST/merchant-api/collect/orders

币种固定为 IDR,不传币种字段;仅使用表中正式字段名。回调地址由平台预先配置,不能在下单时传入 notifyUrl 或 callbackUrl。

字段类型要求说明
merchantOrderIdString必填商户侧唯一订单号。
transactionAmountDecimal必填订单金额,必须大于 0,最多 4 位小数。
methodInteger必填填写一个已开通的支付方式正整数编号;使用 JSON 整数,不接受字符串、小数或列表。
payerNameString必填付款人姓名,不能为空。
mobileString必填付款人手机号,不能为空。
contactEmailString必填付款人邮箱,不能为空。
redirectUrlString可选支付完成后的商户跳转地址。
业务明文示例 · 加密后发送
{
  "merchantOrderId": "MCH-PAYIN-202607130001",
  "transactionAmount": 10000.00,
  "method": 203001,
  "payerName": "Example Payer",
  "mobile": "6281234567890",
  "contactEmail": "buyer@example.com",
  "redirectUrl": "https://merchant.example.com/return"
}
同步响应示例
{
  "success": true,
  "code": "SUCCESS",
  "message": "success",
  "data": {
    "platformOrderId": "PI2607XXXXXXXXXXXX",
    "merchantOrderId": "MCH-PAYIN-202607130001",
    "transactionAmount": 10000.00,
    "currencyCode": "IDR",
    "status": "PENDING",
    "data": "",
    "cashierUrl": "https://pay.example.com/view/qris/CASHIER_TOKEN_EXAMPLE",
    "expireAt": "2026-07-13T12:30:00"
  },
  "traceId": "TRACE-EXAMPLE"
}

请使用响应中的完整 cashierUrl 引导付款人支付,不要自行拼接。data 为附加支付信息,可能为空;expireAt 为收银台过期时间。

代付下单

POST/merchant-api/disburse/orders

币种固定为 IDR;收款账号去除空格和连字符后须为 ASCII 数字,最多 128 位,保留前导零。回调地址由平台预先配置。

字段类型要求说明
merchantOrderIdString必填商户侧唯一订单号。
methodInteger必填填写一个已开通的代付支付方式正整数编号;使用 JSON 整数,不接受字符串、小数或列表。
transactionAmountDecimal必填代付金额,必须大于 0,最多 4 位小数。
beneficiaryNameString必填收款人姓名,不能为空。
beneficiaryMobileString必填收款人手机号,不能为空。
beneficiaryEmailString必填收款人邮箱,不能为空。
recipientAccountNoString必填收款账号,不能为空;去除空格和连字符后须为纯数字,最多 128 位。
业务明文示例 · 加密后发送
{
  "merchantOrderId": "MCH-PAYOUT-202607130001",
  "method": 204001,
  "transactionAmount": 10000.00,
  "beneficiaryName": "Example Recipient",
  "beneficiaryMobile": "6281234567890",
  "beneficiaryEmail": "receiver@example.com",
  "recipientAccountNo": "1234567890"
}

响应 data 字段

字段类型说明
merchantOrderIdString商户订单号。
platformOrderIdString平台代付订单号,后续优先用它查单。
transactionAmountDecimal代付订单金额。
currencyCodeString订单币种。
paymentMethodNoInteger本单代付支付方式编号。
feeAmountDecimal商户手续费;尚无费用时返回 0.0000。
orderStatusString下单时的订单状态;代付下单响应使用此字段,不使用 status。
statusReasonString平台统一状态说明:FAILED 为 payment failed,REVERSED 为 payment was reversed,PENDING_REVIEW 为 payment is under review,其余为空字符串。
createTimeString创建时间,格式 yyyy-MM-dd HH:mm:ss。
completeTimeString 或 null完成时间;尚未完成时可为 null。
同步响应示例
{
  "success": true,
  "code": "SUCCESS",
  "message": "success",
  "data": {
    "merchantOrderId": "MCH-PAYOUT-202607130001",
    "platformOrderId": "PO2607XXXXXXXXXXXX",
    "transactionAmount": 10000.00,
    "currencyCode": "IDR",
    "paymentMethodNo": 204001,
    "feeAmount": 0.0000,
    "orderStatus": "ACCEPT",
    "statusReason": "",
    "createTime": "2026-07-13 12:00:00",
    "completeTime": null
  },
  "traceId": "TRACE-EXAMPLE"
}

下单受理不代表出款成功,请通过回调或查单确认。下单响应状态字段是 orderStatus,查单响应使用 status。

订单查询

可按平台订单号或商户订单号查询,四种路径均见接口清单。按商户订单号查询当前使用 IDR。路径参数需正确进行 URL 编码。

查单业务明文为 {},仍须按安全协议加密并附带完整请求头;不可直接发送明文空对象。

代收查询响应 data

字段类型说明
merchantOrderIdString商户订单号。
platformOrderIdString平台代收订单号。
orderAmountDecimal代收订单金额。
completeAmountDecimal已支付金额。
statusString当前订单状态。
payMethodInteger本单支付方式编号。
createTimeString创建时间,格式 yyyy-MM-dd HH:mm:ss。
completeTimeString 或 null完成时间;尚未完成时可为 null。
serviceFeeDecimal 或 null商户手续费,尚未产生时可为 null。
additionalInfoString固定为空字符串。
代收查询响应示例
{
  "success": true,
  "code": "SUCCESS",
  "message": "success",
  "data": {
    "merchantOrderId": "MCH-PAYIN-202607130001",
    "platformOrderId": "PI2607XXXXXXXXXXXX",
    "orderAmount": 10000.00,
    "completeAmount": 10000.00,
    "status": "SUCCESS",
    "payMethod": 203001,
    "createTime": "2026-07-13 12:00:00",
    "completeTime": "2026-07-13 12:10:00",
    "serviceFee": 100.00,
    "additionalInfo": ""
  },
  "traceId": "TRACE-EXAMPLE"
}

代付查询响应 data

字段类型说明
merchantOrderIdString商户订单号。
platformOrderIdString平台代付订单号。
statusString当前订单状态。
statusReasonString平台统一状态说明:FAILED 为 payment failed,REVERSED 为 payment was reversed,PENDING_REVIEW 为 payment is under review,其余为空字符串。
currencyString订单币种。
orderAmountDecimal代付订单金额。
createTimeString创建时间,格式 yyyy-MM-dd HH:mm:ss。
completeTimeString 或 null完成时间;尚未完成时可为 null。
serviceFeeDecimal商户手续费;尚无费用时返回 0.0000。
additionalInfoString固定为空字符串。
代付查询响应示例
{
  "success": true,
  "code": "SUCCESS",
  "message": "success",
  "data": {
    "merchantOrderId": "MCH-PAYOUT-202607130001",
    "platformOrderId": "PO2607XXXXXXXXXXXX",
    "status": "SUCCESS",
    "statusReason": "",
    "currency": "IDR",
    "orderAmount": 10000.00,
    "createTime": "2026-07-13 12:00:00",
    "completeTime": "2026-07-13 12:15:00",
    "serviceFee": 100.0000,
    "additionalInfo": ""
  },
  "traceId": "TRACE-EXAMPLE"
}

余额查询

POST/merchant-api/accounts/balance
字段类型要求说明
currencyString可选币种,默认 IDR,大小写不敏感。
业务明文示例 · 加密后发送
{
  "currency": "IDR"
}

响应 data 字段

字段类型说明
currencyString查询币种,与请求归一化后一致。
availableBalanceInteger可用余额,可用于发起代付与提现。
pendingSettleBalanceInteger待结算余额,尚未结算进可用余额。
frozenBalanceInteger当前不可用的冻结余额。

余额以整数 IDR 返回,如存在小数则向下取整。该接口为只读查询。

异步回调

平台向预先登记的地址发送 POST 请求。建议使用 HTTPS。回调采用加密信封,密钥职责与商户请求相反。

回调请求头

Header说明
X-Api-Key商户 API Key,用于识别平台投递的目标商户。
X-Timestamp本次回调的 Unix 时间戳。
X-Nonce本次回调的随机字符串;每次重试均重新生成。
X-Encrypt-KeyBase64:平台用已登记的商户 RSA 公钥加密的一次性 AES Key。
X-IVBase64:本次 AES-GCM 加密使用的 IV。
X-SignBase64:平台 RSA 私钥对规范串执行 SHA256withRSA 的签名。
X-Key-Version可选;平台签名密钥版本,供商户选择对应的平台公钥。
回调请求体
{
  "cipherText": "BASE64_AES_GCM_CIPHER_TEXT"
}

处理流程

  1. 核对 API Key,使用平台 RSA 公钥验证 X-Sign。签名原文仍为 API Key、时间戳、nonce、cipherText 四段直接拼接。
  2. 使用商户 RSA 私钥按 RSA/ECB/PKCS1Padding 解密 X-Encrypt-Key;使用解密得到的 AES Key 和 IV 进行 AES-GCM 解密。
  3. 验签和解密成功后,以 eventNo 对业务事件去重;同一事件重试时 eventNo 不变。重复事件不得重复处理,但应返回成功确认。
  4. 处理完成后严格返回 HTTP 200,正文为纯文本 SUCCESS,不能带空格、换行或 JSON 包装。

失败时默认总共尝试 5 次;首次失败后的退避间隔为 30、60、120、240 秒,并受平台最大退避配置限制。

回调明文字段

字段类型说明
eventNoString回调事件号;同一事件重试时保持一致,商户以此去重。
merchantIdLong平台商户 ID。
platformOrderNoString平台订单号。
merchantOrderNoString 或 null商户订单号。
callbackTypeString事件类型,如 PAYIN_SUCCESS、PAYIN_REFUND、PAYOUT_SUCCESS、PAYOUT_FAILED、PAYOUT_REVERSED。
statusString事件对应的结果状态。
amountDecimal 或 null订单金额。
paidAmountDecimal 或 null代收实付金额。
refundAmountDecimal 或 null代收退款事件的退款金额。
merchantFeeAmountDecimal 或 null商户手续费。
refundNoString 或 null代收退款事件的退款号。
currencyString 或 null订单币种。
timestampString事件生成时间,ISO-8601 格式。
traceIdString链路追踪号。
代收成功回调明文示例
{
  "eventNo": "EVENT-EXAMPLE",
  "merchantId": 12,
  "platformOrderNo": "PI2607XXXXXXXXXXXX",
  "merchantOrderNo": "MCH-PAYIN-202607130001",
  "callbackType": "PAYIN_SUCCESS",
  "status": "SUCCESS",
  "amount": 10000.00,
  "paidAmount": 10000.00,
  "refundAmount": null,
  "merchantFeeAmount": 100.00,
  "refundNo": null,
  "currency": "IDR",
  "timestamp": "2026-07-13T12:10:00Z",
  "traceId": "TRACE-EXAMPLE"
}

订单状态

代收下单及查询、代付查询使用 status;代付下单使用 orderStatus。

状态适用方向含义
PENDING_REVIEW代付订单等待审核,请继续查单或等待回调。
ACCEPT代收 / 代付订单已受理,结果尚未确定。
CHECKED代付收款账号已核验,出款仍在处理中。
BOOKED代付出款准备中,结果尚未确定。
PENDING代收 / 代付订单处理中,请继续查单或等待回调。
SUCCESS代收 / 代付当前查询结果为成功;后续发生退款或冲正时可能转为 REFUND 或 REVERSED。
FAILED代收 / 代付订单失败,请按失败结果处理。
REFUND代收此前成功的代收订单已退款。
REVERSED代付此前成功的代付订单已冲正。

错误排查

可预期的认证、参数和业务错误通常返回 HTTP 200,需读取 success、code 和 message。保留 traceId 供平台排查。

错误响应示例
{
  "success": false,
  "code": "UNAUTHORIZED",
  "message": "signature verification failed",
  "data": null,
  "traceId": "TRACE-EXAMPLE"
}
超时、服务错误或查单异常不能直接认定付款失败。先以原商户订单号查询;结果仍不明确时联系平台,避免更换订单号重复发起付款。确认可重试时保持原订单号和业务内容,重新生成安全参数。

错误码

code含义处理建议
BAD_REQUEST请求字段、金额、支付方式或商户余额不符合要求。按 message 修正请求;无法定位时提供 traceId 联系平台。
UNAUTHORIZED商户身份验证未通过。核对 API Key、时间戳、nonce、签名和加密参数。
FORBIDDEN商户状态、IP 授权或订单访问权限不符合要求,或付款请求被拒绝。按 message 核对商户授权;联系平台确认拒绝原因。
NOT_FOUND订单未找到,或接口路径不存在。核对环境、路径和订单号;查单未找到订单为 HTTP 200,路径不存在为 HTTP 404。
METHOD_NOT_ALLOWEDHTTP 方法不受接口支持。HTTP 405;按接口清单使用 POST。
IDEMPOTENCY_CONFLICT同一商户订单号的请求内容不一致。先查原单,保持同一订单号的业务内容一致。
CONFLICT订单结果暂未就绪。稍后查单,无法确认时联系平台,不另建重复付款。
TOO_MANY_REQUESTS请求频率超限。降低频率并退避重试,每次使用新的时间戳、nonce 和签名。
PAYMENT_SERVICE_UNAVAILABLE支付服务暂不可用。先查单确认是否已受理,无法确认时联系平台。
INTERNAL_ERROR服务异常。保留 traceId 并联系平台;不能据此认定支付失败。
展开认证与加密错误
codemessage原因处理建议
UNAUTHORIZED{header} is required缺少必需的请求头,或请求头为空。补齐 X-Api-Key、X-Timestamp、X-Nonce、X-Sign、X-Encrypt-Key、X-IV;{header} 会替换为缺失的请求头名。
UNAUTHORIZEDmerchant not foundAPI Key 未匹配到商户。核对接入环境和 API Key;无法确认时联系平台核查。
FORBIDDENmerchant is disabled商户已停用。联系平台确认商户状态,恢复启用后再请求。
FORBIDDENmerchant API access is disabled商户 API 接入已关闭。联系平台启用 API 接入后重试。
FORBIDDENmerchant ip whitelist is required商户尚未配置请求 IP 白名单。向平台登记商户服务器的公网出口 IP 或 CIDR。
FORBIDDENrequest ip is not allowed本次请求的真实 IP 未命中白名单。核对商户服务器的公网出口 IP,联系平台登记或更新白名单。
BAD_REQUESTtimestamp is invalidX-Timestamp 无法解析为整数时间戳。按协议传入 Unix 秒级整数字符串,并用同一字符串参与签名。
UNAUTHORIZEDtimestamp is expired请求时间超出平台允许窗口,默认 300 秒。校准服务器时钟,重新生成时间戳、nonce 和签名。
BAD_REQUESTnonce is too longX-Nonce 长度超过 128。使用长度不超过 128 的随机字符串。
UNAUTHORIZEDnonce replay detected同一商户在有效期内重复使用 nonce。每次请求及重试生成新的随机 nonce,并重新签名。
BAD_REQUESTrequest body is requiredHTTP 请求体为空。发送包含 cipherText 的 JSON 加密信封。
BAD_REQUESTencrypted body is invalid请求体不是可解析的加密信封 JSON。检查 Content-Type 和 JSON 格式,发送 {"cipherText":"..."}。
BAD_REQUESTcipherText is required加密信封缺少 cipherText 或该字段为空。将 AES-GCM 密文和认证 Tag 拼接后 Base64 编码,放入 cipherText。
UNAUTHORIZEDmerchant public key is required平台未登记商户验签公钥。向平台登记与商户签名私钥配对的 RSA 公钥。
UNAUTHORIZEDmerchant public key is invalid已登记的商户 RSA 公钥无法解析。联系平台核对已登记公钥的 PEM、Base64 和 RSA 格式。
UNAUTHORIZEDsignature is invalidX-Sign 无法按签名格式解析或验证。检查签名的 Base64 格式、RSA 密钥和 SHA256withRSA 算法。
UNAUTHORIZEDsignature verification failed签名原文不一致,或商户私钥与登记公钥不匹配。只签 X-Api-Key + X-Timestamp + X-Nonce + cipherText;不添加分隔符,不签信封、IV 或明文;重试使用新 nonce。
UNAUTHORIZEDencrypt key is invalidX-Encrypt-Key 无法被平台私钥解密。用该商户的平台 RSA 公钥和 RSA/ECB/PKCS1Padding 加密原始 AES Key 字节,结果使用 Base64。
UNAUTHORIZEDpayload decrypt failedAES-GCM 密文、AES Key、IV 或认证 Tag 不匹配。核对本次 AES Key 和 Base64 IV;cipherText 必须是 Base64(ciphertext || authTag),Tag 为 128 bit。
UNAUTHORIZEDmerchant authentication failed商户身份验证未通过。使用正确的商户 API 地址及完整加密签名请求;仍出现时提供 traceId 联系平台。
UNAUTHORIZEDunauthorized身份验证未通过。核对 API Key、请求头和签名;仍失败时联系平台。
FORBIDDENforbidden当前请求未获授权。核对商户状态及授权范围,联系平台确认。
展开请求参数与业务错误
codemessage原因处理建议
BAD_REQUESTrequest body is invalid解密后的业务 JSON 格式、字段名或字段类型不正确,包括旧别名、未定义字段,以及 method 为字符串、小数、列表或超出 Integer 范围。仅使用文档中的正式字段名;检查 JSON 语法和参数类型,金额使用数值,method 使用单一 JSON 整数。
BAD_REQUESTmerchantOrderId must not be blank下单缺少商户订单号,或订单号为空白。填写唯一的 merchantOrderId;同一笔业务重试保持订单号和业务内容一致。
BAD_REQUESTpayerName must not be blank代收下单缺少付款人姓名,或姓名为空白。填写非空的 payerName。
BAD_REQUESTmobile must not be blank代收下单缺少付款人手机号,或手机号为空白。填写非空的 mobile。
BAD_REQUESTcontactEmail must not be blank代收下单缺少付款人邮箱,或邮箱为空白。填写非空的 contactEmail。
BAD_REQUESTbeneficiaryName must not be blank代付下单缺少收款人姓名,或姓名为空白。填写非空的 beneficiaryName。
BAD_REQUESTbeneficiaryMobile must not be blank代付下单缺少收款人手机号,或手机号为空白。填写非空的 beneficiaryMobile。
BAD_REQUESTbeneficiaryEmail must not be blank代付下单缺少收款人邮箱,或邮箱为空白。填写非空的 beneficiaryEmail。
BAD_REQUESTpayment method must be exact代收或代付下单的 method 缺失、为 null、为零或负数。填写 method,使用可用支付方式页面中已开通的一个正整数编号。
BAD_REQUESTamount must be greater than zerotransactionAmount 缺失、为零或负数。填写大于 0 的订单金额。
BAD_REQUESTamount scale must be less than or equal to 4订单金额超过 4 位小数。按业务规则确定金额后,最多保留 4 位小数再提交。
BAD_REQUESTrecipientAccountNo is required代付下单未提供收款账号,或账号为空白。填写非空的 recipientAccountNo。
BAD_REQUESTrecipientAccountNo is invalid收款账号去除空格和连字符后不是纯数字,或超过 128 位。核对 recipientAccountNo,提交有效的数字账号。
BAD_REQUESTmerchant is not available商户主体不存在或未处于可用状态。联系平台核对商户状态。
BAD_REQUESTmerchant currency is not supported订单币种与商户配置的币种不一致。联系平台核对币种配置;代收固定 IDR,无需传币种。
BAD_REQUESTpayin is not enabled for merchant商户没有已启用的 IDR 代收配置。联系平台开通或启用代收配置后再请求。
BAD_REQUESTpayout is not enabled for merchant商户没有对应币种的已启用代付配置。联系平台开通或启用该币种的代付配置。
BAD_REQUESTpayin callback URL is not configured平台未配置商户代收回调地址。联系平台登记代收回调地址;下单请求不传 notifyUrl 或 callbackUrl。
BAD_REQUESTpayout callback URL is not configured平台未配置商户代付回调地址。联系平台登记代付回调地址;下单请求不传 notifyUrl 或 callbackUrl。
BAD_REQUESTpayin amount is outside merchant range代收金额超出商户代收单笔限额。核对代收最小、最大单笔金额,必要时联系平台调整限额。
BAD_REQUESTpayout amount is outside merchant range代付金额超出商户代付单笔限额。核对代付最小、最大单笔金额,必要时联系平台调整限额。
BAD_REQUESTpayment method is not available支付方式不存在、已停用,或不支持本次币种或订单方向。从可用支付方式页面选取对应方向和币种的有效方式。
BAD_REQUESTpayment method is disabled for merchant商户的具体支付方式已被停用。联系平台确认授权状态,启用后再请求。
BAD_REQUESTpayment method is not enabled for merchant商户未开通该支付方式,或本次金额不在允许范围内。核对支付方式授权及单笔限额,联系平台确认配置。
BAD_REQUESTamount is outside merchant payment method range金额超出商户具体支付方式的单笔限额。核对可用支付方式页面中的该方式最小、最大金额。
BAD_REQUESTmerchant balance account is not available代付预检查未找到已启用的可用余额账户。联系平台核对商户该币种的账户状态。
BAD_REQUESTmerchant available balance is insufficient商户可用余额不足以覆盖代付本金和商户手续费。查询余额并补足本金及手续费后再请求;超时重试前先查单。
BAD_REQUESTinvalid request请求不符合接口要求。核对请求字段和类型;无法定位时提供 traceId 联系平台。
codemessage原因处理建议
FORBIDDENpayment request was rejected当前付款请求未获准处理。提供 traceId 联系平台核实,不要更换订单号反复发起同一笔付款。
NOT_FOUNDorder not found当前查询条件未找到本商户订单。核对接入环境和订单号;下单超时后先查单,仍无法确认时联系平台。
FORBIDDENorder does not belong to merchant订单不属于当前商户。核对商户身份和订单号,仅查询本商户订单。
IDEMPOTENCY_CONFLICTidempotency conflict同一商户订单号的业务内容与首次请求不同。先查原单,保持同一订单号的业务内容一致。
CONFLICTorder is not ready订单结果暂未就绪。稍后按原订单号查单;持续无法确认时提供 traceId 联系平台,不另建同一笔业务订单。
TOO_MANY_REQUESTSrate limit exceeded请求频率超过平台限制。降低频率并退避重试,使用新的时间戳、nonce 和签名。
TOO_MANY_REQUESTStoo many requests请求频率超过平台限制。降低频率并退避重试,使用新的时间戳、nonce 和签名。
PAYMENT_SERVICE_UNAVAILABLEpayment service is temporarily unavailable支付服务暂不可用。先查单确认是否受理;无法确认时提供 traceId 联系平台,避免重复付款。
INTERNAL_ERRORinternal server error服务发生异常。保留 traceId 联系平台;先查单确认状态,不能据此认定订单支付失败。

代码示例

以下示例在商户服务端生成请求头和加密信封。请自行接入 HTTP 客户端、密钥管理、错误处理和重试逻辑,切勿将私钥或 AES Key 写入日志。

Node.js / TypeScript
生成签名与加密信封
import * as crypto from 'node:crypto'

type ArthaPayRequest = {
  headers: Record<string, string>
  body: string
}

export function buildArthaPayRequest(
  apiKey: string,
  businessPayload: unknown,
  platformPublicKeyPem: string,
  merchantPrivateKeyPem: string,
): ArthaPayRequest {
  // 业务参数先序列化为 JSON;明文不直接参与签名。
  const plainJson = JSON.stringify(businessPayload)
  // 每次请求(包括重试)都生成新的 AES Key 和 IV。
  const aesKey = crypto.randomBytes(32)
  const iv = crypto.randomBytes(12)

  const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv)
  const encrypted = Buffer.concat([
    cipher.update(plainJson, 'utf8'),
    cipher.final(),
  ])
  // 将密文与 128 位 GCM 认证标签拼接,再 Base64 编码。
  const cipherText = Buffer.concat([encrypted, cipher.getAuthTag()]).toString('base64')

  // 使用平台 RSA 公钥和 PKCS#1 v1.5 加密 AES Key 原始字节,作为 X-Encrypt-Key。
  const encryptKey = crypto.publicEncrypt(
    {
      key: platformPublicKeyPem,
      padding: crypto.constants.RSA_PKCS1_PADDING,
    },
    aesKey,
  ).toString('base64')

  // 推荐秒级 Unix 时间戳;nonce 每次请求均须不同。
  const timestamp = Math.floor(Date.now() / 1000).toString()
  const nonce = crypto.randomBytes(16).toString('hex')
  // 按顺序直接拼接四段原始字符串,不添加分隔符或空格;时间戳须与请求头完全一致。
  // 不要把业务明文、整个信封、X-IV 或 X-Encrypt-Key 加入签名原文。
  const signPayload = apiKey + timestamp + nonce + cipherText
  // 使用商户 RSA 私钥对 UTF-8 签名原文执行 SHA256withRSA,并将结果 Base64 编码。
  const sign = crypto.sign(
    'RSA-SHA256',
    Buffer.from(signPayload, 'utf8'),
    merchantPrivateKeyPem,
  ).toString('base64')

  return {
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': apiKey,
      'X-Timestamp': timestamp,
      'X-Nonce': nonce,
      'X-Encrypt-Key': encryptKey,
      'X-IV': iv.toString('base64'),
      'X-Sign': sign,
    },
    body: JSON.stringify({ cipherText }),
  }
}
Go · 标准库

需要 Go 1.18 或以上版本,使用标准库即可。查单时可传入 map[string]any{},以生成业务明文 {}。

生成签名与加密信封
package arthapay

import (
	"crypto"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"errors"
	"strconv"
	"time"
)

type RequestData struct {
	Headers map[string]string
	Body    string
}

func BuildArthaPayRequest(
	apiKey string,
	businessPayload any,
	platformPublicKeyPEM string,
	merchantPrivateKeyPEM string,
) (*RequestData, error) {
	// 业务参数序列化为 JSON;查单可传入 map[string]any{},生成 {}。
	plainJSON, err := json.Marshal(businessPayload)
	if err != nil {
		return nil, err
	}

	// 每次请求(包括重试)生成新的 32 字节 AES Key 和 12 字节 IV。
	aesKey := make([]byte, 32)
	if _, err = rand.Read(aesKey); err != nil {
		return nil, err
	}
	iv := make([]byte, 12)
	if _, err = rand.Read(iv); err != nil {
		return nil, err
	}

	// AES-GCM 输出为密文与 128 位认证标签的拼接,再进行 Base64 编码。
	cipherText, err := aesGcmEncryptToBase64(plainJSON, aesKey, iv)
	if err != nil {
		return nil, err
	}

	// 使用平台 RSA 公钥及 PKCS#1 v1.5 加密 AES Key 原始字节。
	publicKey, err := readPublicKey(platformPublicKeyPEM)
	if err != nil {
		return nil, err
	}
	encryptKeyBytes, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, aesKey)
	if err != nil {
		return nil, err
	}
	encryptKey := base64.StdEncoding.EncodeToString(encryptKeyBytes)

	// 使用秒级 Unix 时间戳,每次请求生成新的 nonce。
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	nonceBytes := make([]byte, 16)
	if _, err = rand.Read(nonceBytes); err != nil {
		return nil, err
	}
	nonce := hex.EncodeToString(nonceBytes)

	// 按顺序直接拼接四段原始字符串,不加分隔符;时间戳须与请求头完全一致。
	// 业务明文、整个信封、X-IV 和 X-Encrypt-Key 不参与签名。
	signPayload := apiKey + timestamp + nonce + cipherText
	privateKey, err := readPrivateKey(merchantPrivateKeyPEM)
	if err != nil {
		return nil, err
	}
	// 使用商户 RSA 私钥执行 SHA256withRSA,再将签名字节 Base64 编码。
	hashed := sha256.Sum256([]byte(signPayload))
	signBytes, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
	if err != nil {
		return nil, err
	}
	sign := base64.StdEncoding.EncodeToString(signBytes)

	body, err := json.Marshal(map[string]string{"cipherText": cipherText})
	if err != nil {
		return nil, err
	}

	return &RequestData{
		Headers: map[string]string{
			"Content-Type":  "application/json",
			"X-Api-Key":     apiKey,
			"X-Timestamp":   timestamp,
			"X-Nonce":       nonce,
			"X-Encrypt-Key": encryptKey,
			"X-IV":          base64.StdEncoding.EncodeToString(iv),
			"X-Sign":        sign,
		},
		Body: string(body),
	}, nil
}

func aesGcmEncryptToBase64(plainJSON, aesKey, iv []byte) (string, error) {
	block, err := aes.NewCipher(aesKey)
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	sealed := gcm.Seal(nil, iv, plainJSON, nil)
	return base64.StdEncoding.EncodeToString(sealed), nil
}

// 公钥使用 X.509 SubjectPublicKeyInfo PEM(BEGIN PUBLIC KEY)。
func readPublicKey(pemStr string) (*rsa.PublicKey, error) {
	block, _ := pem.Decode([]byte(pemStr))
	if block == nil {
		return nil, errors.New("invalid public key PEM")
	}
	parsed, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	publicKey, ok := parsed.(*rsa.PublicKey)
	if !ok {
		return nil, errors.New("not an RSA public key")
	}
	return publicKey, nil
}

// 私钥使用 PKCS#8 PEM(BEGIN PRIVATE KEY)。
func readPrivateKey(pemStr string) (*rsa.PrivateKey, error) {
	block, _ := pem.Decode([]byte(pemStr))
	if block == nil {
		return nil, errors.New("invalid private key PEM")
	}
	parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	privateKey, ok := parsed.(*rsa.PrivateKey)
	if !ok {
		return nil, errors.New("not an RSA private key")
	}
	return privateKey, nil
}
PHP · OpenSSL

需要 PHP 7.3 或以上版本,并启用 OpenSSL 扩展。查单时请传入 (object) [],以生成业务明文 {}。

生成签名与加密信封
<?php
// 需要 PHP 7.3+,并启用 OpenSSL 扩展。
function buildArthaPayRequest(
    string $apiKey,
    $businessPayload,
    string $platformPublicKeyPem,
    string $merchantPrivateKeyPem
): array {
    // 业务参数先序列化为 JSON;查单时使用 (object) [],确保明文为 {}。
    $plainJson = json_encode($businessPayload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);

    // 每次请求(包括重试)生成新的 32 字节 AES Key 和 12 字节 IV。
    $aesKey = random_bytes(32);
    $iv = random_bytes(12);

    // 使用 AES-256-GCM 加密;认证标签长度为 16 字节(128 位)。
    $tag = '';
    $encrypted = openssl_encrypt(
        $plainJson,
        'aes-256-gcm',
        $aesKey,
        OPENSSL_RAW_DATA,
        $iv,
        $tag,
        '',
        16
    );
    if ($encrypted === false || strlen($tag) !== 16) {
        throw new RuntimeException('AES-GCM encryption failed');
    }

    // 将密文与认证标签拼接,再 Base64 编码,作为请求体 cipherText。
    $cipherText = base64_encode($encrypted . $tag);

    // 使用平台 RSA 公钥及 PKCS#1 v1.5 加密 AES Key 原始字节。
    $encryptedKey = '';
    if (!openssl_public_encrypt($aesKey, $encryptedKey, $platformPublicKeyPem, OPENSSL_PKCS1_PADDING)) {
        throw new RuntimeException('RSA key encryption failed');
    }
    $encryptKey = base64_encode($encryptedKey);

    // 使用秒级 Unix 时间戳,每次请求生成新的 nonce。
    $timestamp = (string) time();
    $nonce = bin2hex(random_bytes(16));

    // 按顺序直接拼接四段原始字符串,不加分隔符;时间戳须与请求头完全一致。
    // 业务明文、整个信封、X-IV 和 X-Encrypt-Key 不参与签名。
    $signPayload = $apiKey . $timestamp . $nonce . $cipherText;

    // 使用商户 RSA 私钥执行 SHA256withRSA,再将签名字节 Base64 编码。
    $signature = '';
    if (!openssl_sign($signPayload, $signature, $merchantPrivateKeyPem, OPENSSL_ALGO_SHA256)) {
        throw new RuntimeException('RSA signing failed');
    }
    $sign = base64_encode($signature);

    return [
        'headers' => [
            'Content-Type' => 'application/json',
            'X-Api-Key' => $apiKey,
            'X-Timestamp' => $timestamp,
            'X-Nonce' => $nonce,
            'X-Encrypt-Key' => $encryptKey,
            'X-IV' => base64_encode($iv),
            'X-Sign' => $sign,
        ],
        'body' => json_encode(['cipherText' => $cipherText], JSON_THROW_ON_ERROR),
    ];
}
Java · 依赖 Jackson
生成签名与加密信封
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

public final class ArthaPayMerchantCrypto {
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final SecureRandom RANDOM = new SecureRandom();
    private static final int GCM_TAG_LENGTH_BIT = 128;

    public static RequestData buildRequest(
            String apiKey,
            Object businessPayload,
            String platformPublicKeyPem,
            String merchantPrivateKeyPem
    ) throws Exception {
        // 业务参数先序列化为 JSON;明文不直接参与签名。
        String plainJson = MAPPER.writeValueAsString(businessPayload);
        // 每次请求(包括重试)都生成新的 AES Key 和 IV。
        byte[] aesKey = randomBytes(32);
        byte[] iv = randomBytes(12);

        // Java 的 GCM doFinal 输出已包含 128 位认证标签,整体进行 Base64 编码。
        String cipherText = aesGcmEncryptToBase64(plainJson, aesKey, iv);
        // 使用平台 RSA 公钥和 PKCS#1 v1.5 加密 AES Key 原始字节,作为 X-Encrypt-Key。
        String encryptKey = rsaEncryptToBase64(aesKey, readPublicKey(platformPublicKeyPem));
        // 推荐秒级 Unix 时间戳;nonce 每次请求均须不同。
        String timestamp = String.valueOf(Instant.now().getEpochSecond());
        String nonce = UUID.randomUUID().toString().replace("-", "");
        // 按顺序直接拼接四段原始字符串,不添加分隔符或空格;时间戳须与请求头完全一致。
        // 不要把业务明文、整个信封、X-IV 或 X-Encrypt-Key 加入签名原文。
        String signPayload = apiKey + timestamp + nonce + cipherText;
        // 使用商户 RSA 私钥对 UTF-8 签名原文执行 SHA256withRSA,并将结果 Base64 编码。
        String sign = rsaSignToBase64(signPayload, readPrivateKey(merchantPrivateKeyPem));

        Map<String, String> headers = new LinkedHashMap<>();
        headers.put("Content-Type", "application/json");
        headers.put("X-Api-Key", apiKey);
        headers.put("X-Timestamp", timestamp);
        headers.put("X-Nonce", nonce);
        headers.put("X-Encrypt-Key", encryptKey);
        headers.put("X-IV", Base64.getEncoder().encodeToString(iv));
        headers.put("X-Sign", sign);

        String body = MAPPER.writeValueAsString(Map.of("cipherText", cipherText));
        return new RequestData(headers, body);
    }

    private static String aesGcmEncryptToBase64(String plainJson, byte[] aesKey, byte[] iv) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(
                Cipher.ENCRYPT_MODE,
                new SecretKeySpec(aesKey, "AES"),
                new GCMParameterSpec(GCM_TAG_LENGTH_BIT, iv)
        );
        return Base64.getEncoder().encodeToString(cipher.doFinal(plainJson.getBytes(StandardCharsets.UTF_8)));
    }

    private static String rsaEncryptToBase64(byte[] plainBytes, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return Base64.getEncoder().encodeToString(cipher.doFinal(plainBytes));
    }

    private static String rsaSignToBase64(String payload, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(payload.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(signature.sign());
    }

    // 公钥使用 X.509 SubjectPublicKeyInfo PEM(BEGIN PUBLIC KEY)。
    private static PublicKey readPublicKey(String pem) throws Exception {
        String key = normalizePem(pem, "PUBLIC KEY");
        return KeyFactory.getInstance("RSA").generatePublic(
                new X509EncodedKeySpec(Base64.getDecoder().decode(key))
        );
    }

    // 私钥使用 PKCS#8 PEM(BEGIN PRIVATE KEY)。
    private static PrivateKey readPrivateKey(String pem) throws Exception {
        String key = normalizePem(pem, "PRIVATE KEY");
        return KeyFactory.getInstance("RSA").generatePrivate(
                new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key))
        );
    }

    private static String normalizePem(String pem, String type) {
        return pem
                .replace("-----BEGIN " + type + "-----", "")
                .replace("-----END " + type + "-----", "")
                .replaceAll("\\s", "");
    }

    private static byte[] randomBytes(int length) {
        byte[] bytes = new byte[length];
        RANDOM.nextBytes(bytes);
        return bytes;
    }

    public record RequestData(Map<String, String> headers, String body) {
    }
}