开始接入
通过安全接口创建代收、代付订单,并通过查单或异步回调确认支付结果。
API Key、平台 RSA 公钥、接口环境地址和已开通的支付方式编号;向平台登记商户 RSA 公钥、服务器公网出口 IP 及代收 / 代付回调地址。商户私钥由商户自行保管。
接口环境
https://openapi.hzpay.arthapay24.comhttps://sandbox-openapi.hzpay.arthapay24.com以上地址来自当前接口配置,具体开通情况和地址以平台提供的信息为准。文档中的订单号、金额和支付方式编号仅作示例。
回调服务器出口 IP
平台发送商户回调时使用以下 IP;如您的回调接口配置了来源 IP 白名单,请添加这两个地址。
16.79.84.22016.78.208.187接入流程
- 向平台登记商户 RSA 公钥,获取商户 API Key、平台 RSA 公钥,并登记代收/代付回调地址和请求出口 IP。建议使用 HTTPS 回调地址,生产签名应在商户服务端完成。
- 在商户服务端按业务参数组装代收 / 代付明文 JSON。
- 生成 AES Key 与 IV,使用 AES-GCM 加密明文、平台公钥加密 AES Key、商户私钥签名。
- 以固定信封 body 调用对应 /merchant-api 接口,保存响应中的 platformOrderId;代收响应还会返回 cashierUrl。
- 代收引导付款人打开 cashierUrl 完成支付;代付通过回调或查单获取后续结果。
- 通过异步回调或按固定间隔主动查单,确认订单最终状态,不要用 HTTP 200 推断成功。
success=true 只代表接口请求处理结果,不能作为付款成功的依据。请根据订单状态及回调确认结果。签名与加密
所有商户接口使用 POST、JSON 加密信封和相同的安全请求头。普通接口同步响应为明文 JSON。
请求头
| Header | 格式 | 说明 |
|---|---|---|
| Content-Type | application/json | 请求体固定为 JSON envelope。 |
| X-Api-Key | 平台分配 | 商户 API Key,用于识别商户身份。 |
| X-Timestamp | Unix timestamp | 推荐秒级时间戳,服务端兼容毫秒,默认允许 300 秒偏差。 |
| X-Nonce | 随机字符串 | 同一商户 5 分钟内不可重复,长度不超过 128。 |
| X-Encrypt-Key | Base64 | 使用平台 RSA 公钥加密 AES Key 原始字节。 |
| X-IV | Base64 | AES-GCM IV,推荐 12 字节随机数。 |
| X-Sign | Base64 | 商户 RSA 私钥对签名串执行 SHA256withRSA。 |
构造请求
- 将业务 JSON 序列化为 UTF-8,随机生成 AES Key(16 / 24 / 32 字节,推荐 32 字节)及 12 字节 IV。
- 使用 AES-GCM 加密,认证标签为 128 bit。将密文和认证标签拼接后 Base64 编码,得到
cipherText。 - 使用平台 RSA 公钥,以 RSA/ECB/PKCS1Padding 加密 AES Key 原始字节并 Base64 编码,得到
X-Encrypt-Key;IV Base64 编码后放入X-IV。 - 生成时间戳和唯一 nonce,以商户 RSA 私钥执行 SHA256withRSA 签名,再将签名字节 Base64 编码,放入
X-Sign。 - 将加密信封和所有请求头发送至接口。每次请求及重试均使用新的 AES Key、IV、时间戳和 nonce。
X-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 条
| 支付方式 | 支付编码 |
|---|---|
| OVO | 202001 |
| DANA | 202002 |
| SHOPEEPAY | 202003 |
| LINKAJA | 202004 |
| GOPAY | 201017 |
| DANA_FAKE | 202005 |
| DANA_T+1 | 202006 |
| DANA_D0 | 202007 |
银行 15 条
| 支付方式 | 支付编码 |
|---|---|
| BCA | 201001 |
| MANDIRI | 201002 |
| BSI | 201003 |
| BRI | 201004 |
| BNI | 201005 |
| PERMATA | 201006 |
| CIMB | 201007 |
| DANAMON | 201008 |
| OCBC | 201009 |
| BTPN | 201010 |
| SAHABAT_SAMPOERNA | 201011 |
| SINARMAS | 201012 |
| MAYBANK | 201013 |
| SYARIAH | 201014 |
| BNC | 201016 |
QRIS 1 条
| 支付方式 | 支付编码 |
|---|---|
| QRIS | 203001 |
代付
钱包 7 条
| 支付方式 | 支付编码 |
|---|---|
| OVO | 205001 |
| DANA | 205002 |
| GOPAY | 205003 |
| SHOPEEPAY | 205004 |
| LINKAJA | 205005 |
| ASTRAPAY | 205006 |
| ISAKU | 205007 |
银行 239 条
| 支付方式 | 支付编码 |
|---|---|
| BANK BCA | 204001 |
| BANK BRI | 204002 |
| BANK MANDIRI | 204003 |
| BANK BNI | 204004 |
| BANK DANAMON INDONESIA | 204005 |
| BANK PERMATA | 204006 |
| MAYBANK | 204007 |
| BANK PANIN | 204008 |
| BANK CIMB NIAGA | 204009 |
| BANK UOB BUANA INDONESIA | 204010 |
| BANK OCBC NISP | 204011 |
| CITIBANK | 204012 |
| BANK CCB INDONESIA | 204013 |
| BANK ARTHA GRAHA | 204014 |
| BANK MUFG | 204015 |
| BANK DBS INDONESIA | 204016 |
| BANK STANDARD CHARTERED | 204017 |
| BANK CAPITAL | 204018 |
| BANK ANZ INDONESIA | 204019 |
| THE BANK OF CHINA | 204020 |
| BANK BUMI ARTA | 204021 |
| BANK HSBC | 204022 |
| BANK JTRUST INDONESIA | 204023 |
| BANK MAYAPADA INTERNATIONAL | 204024 |
| BANK JABAR | 204025 |
| BPD DKI JAKARTA | 204026 |
| BPD DIY | 204027 |
| BANK JATENG | 204028 |
| BANK JATIM | 204029 |
| BPD JAMBI | 204030 |
| BPD ACEH | 204031 |
| BANK SUMUT | 204032 |
| BPD SUMATERA BARAT | 204033 |
| BPD RIAU | 204034 |
| BPD SUMSEL BABEL | 204035 |
| BPD LAMPUNG | 204036 |
| BPD KALSEL | 204037 |
| BPD KALBAR | 204038 |
| BPD KALTIMTARA | 204039 |
| BPD KALTENG | 204040 |
| BANK SULSELBAR | 204041 |
| BANK SULUTGO | 204042 |
| BPD NUSA TENGGARA BARAT | 204043 |
| BPD BALI | 204044 |
| Bank NTT | 204045 |
| BPD MALUKU | 204046 |
| BPD PAPUA | 204047 |
| BPD BENGKULU | 204048 |
| BPD SULAWESI TENGAH | 204049 |
| BPD SULAWESI TENGGARA | 204050 |
| BPD BANTEN | 204051 |
| BANK OF INDIA INDONESIA | 204052 |
| BANK MUAMALAT INDONESIA | 204053 |
| BANK MESTIKA DHARMA | 204054 |
| BANK SHINHAN INDONESIA | 204055 |
| BANK SINARMAS | 204056 |
| BANK MASPION INDONESIA | 204057 |
| BANK GANESHA | 204058 |
| BANK ICBC | 204059 |
| BANK QNB KESAWAN | 204060 |
| BANK BTN | 204061 |
| BANK WOORI SAUDARA | 204062 |
| BANK BTPN | 204063 |
| BANK VICTORIA SYARIAH | 204064 |
| BANK JABAR BANTEN SYARIAH | 204065 |
| BANK MEGA | 204066 |
| BANK KB BUKOPIN | 204067 |
| BANK BSI (BANK SYARIAH INDONESIA) | 204068 |
| BANK JASA JAKARTA | 204069 |
| BANK KEB HANA | 204070 |
| BANK MNC INTERNASIONAL | 204071 |
| BANK NEO COMMERCE | 204072 |
| BANK RAYA INDONESIA | 204073 |
| BANK SBI INDONESIA | 204074 |
| BANK DIGITAL BCA | 204075 |
| BANK NATIONAL NOBU | 204076 |
| BANK MEGA SYARIAH | 204077 |
| BANK INA PERDANA | 204078 |
| BANK PANIN DUBAI SYARIAH | 204079 |
| BANK PRIMA MASTER | 204080 |
| BANK BUKOPIN SYARIAH | 204081 |
| BANK MUTIARA | 204129 |
| BANK SAHABAT SAMPOERNA | 204082 |
| BANK OKE INDONESIA | 204083 |
| BANK SEABANK INDONESIA | 204084 |
| BANK BCA SYARIAH | 204085 |
| BANK JAGO | 204086 |
| BANK BTPN SYARIAH | 204087 |
| BANK MULTI ARTA SENTOSA | 204088 |
| BANK MAYORA(Hibank) | 204089 |
| BANK INDEX SELINDO | 204090 |
| BANK MANDIRI TASPEN POS(Bank MANTAP) | 204091 |
| BANK VICTORIA INTERNASIONAL | 204092 |
| BANK ALLO | 204093 |
| BANK IBK INDONESIA | 204094 |
| BANK ALADIN SYARIAH | 204095 |
| BANK CTBC INDONESIA | 204096 |
| BANK COMMONWEALTH | 204097 |
| BANK AMAR INDONESIA | 204098 |
| BANK Andara | 204099 |
| BANK Anglomas International | 204100 |
| ARTAJASA PEMBAYARAN ELEKTRONIK | 204101 |
| BANK Arta Niaga Kencana | 204102 |
| BANK Bisnis Internasional | 204103 |
| BANK Bangkok | 204104 |
| Bank BNI Syariah | 204105 |
| Bank BRI Syariah | 204106 |
| BANK OF AMERICA | 204107 |
| BPR KS(KARYAJATNIKA SADAYA) | 204108 |
| Bank Tabungan Negara (BTN) UUS | 204109 |
| Centratama Nasional Bank(CNB) | 204110 |
| BANK CIMB Niaga UUS | 204111 |
| BANK PEMBANGUNAN DAERAH DIY UNIT USAHA SYARIAH | 204112 |
| BANK Danamon UUS | 204113 |
| BANK Deutsche | 204114 |
| BANK DKI UUS | 204115 |
| BANK EKA | 204116 |
| BANK Fama International | 204117 |
| BANK Himpunan Saudara 1906 | 204118 |
| Hongkong and Shanghai Bank Corporation (HSBC) | 204119 |
| BPD JAWA TENGAH UNIT USAHA SYARIAH | 204120 |
| BANK Jatim UUS | 204121 |
| JPMORGAN CHASE BANK | 204122 |
| BPD Kalimantan Barat UUS | 204123 |
| BPD Kalimantan Selatan UUS | 204124 |
| BPD Kalimantan Timur UUS | 204125 |
| BANK METRO EXPRESS | 204126 |
| BANK Mitra Niaga | 204127 |
| BANK Mizuho Indonesia | 204128 |
| BPD Nusa Tenggara Barat (NTB) UUS | 204130 |
| Bank Nusantara Parahyangan | 204176 |
| Bank OCBC NISP UUS | 204131 |
| Bank Permata UUS | 204132 |
| BANK PUNDI INDONESIA | 204133 |
| Rabobank International Indonesia | 204134 |
| Bank Resona Perdania | 204135 |
| BPD Riau Dan Kepri UUS | 204136 |
| Bank Sulselbar UUS | 204137 |
| BPD Sumatera Barat UUS | 204138 |
| BANK NAGARI | 204139 |
| Bank Sumsel Dan Babel UUS | 204140 |
| Bank Sumut UUS | 204141 |
| Bank Sumitomo Mitsui Indonesia | 204142 |
| BANK BNP PARIBAS INDONESIA | 204143 |
| BPR SUPRA ARTAPERSADA | 204144 |
| Bank Syariah Mandiri | 204145 |
| Bank BJB | 204146 |
| Bank Tabungan Pensiunan Nasional | 204147 |
| BPD Kalimantan Timur | 204148 |
| BPD Riau Dan Kepri | 204149 |
| BPD Kalimantan Barat | 204150 |
| Hongkong and Shanghai Bank Corporation (HSBC) UUS | 204151 |
| BPD Aceh UUS | 204152 |
| BPD Nusa Tenggara Timur | 204153 |
| BPD Jambi UUS | 204154 |
| BPD Jawa Tengah | 204155 |
| Bank Maybank Syariah Indonesia | 204156 |
| Bank Agris | 204157 |
| Bank Chinatrust Indonesia | 204158 |
| Bank of America Merill-Lynch | 204159 |
| Bank of Tokyo Mitsubishi UFJ | 204160 |
| Bank Dinar Indonesia | 204177 |
| BPD Jawa Timur UUS | 204161 |
| Bank antardaerah | 204162 |
| Bank harmoni international | 204163 |
| Bank woori indonesia | 204164 |
| Bank ekspor indonesia | 204165 |
| Bank BJB Syariah | 204166 |
| Bank BRI Agroniaga | 204167 |
| BANK SINARMAS UUS | 204168 |
| Virtual Account Bank Mandiri | 204169 |
| Virtual Account Bank BRI | 204170 |
| Virtual Account Bank CIMB | 204171 |
| Virtual Account Bank Permata | 204172 |
| Virtual Account Bank BCA | 204173 |
| Virtual Account Bank BNI | 204174 |
| Virtual Account Bank Danamon | 204175 |
| BANK NANO SYARIAH | 204189 |
| BANK NAGARI SYARIAH | 204178 |
| BANK JAGO SYARIAH | 204179 |
| BANK EKONOMI RAHARJA | 204180 |
| BANK ROYAL INDONESIA | 204181 |
| BANK YUDHA BAKTI (BNC) | 204182 |
| BANK INTERIM INDONESIA | 204183 |
| BANK PIKKO | 204184 |
| BANK KESEJAHTERAAN | 204185 |
| BANK SEABANK | 204186 |
| BANK HANA | 204187 |
| BANK MEGA TBK. | 204188 |
| BANK UOB INDONESIA | 204191 |
| BCA | 204190 |
| BANK JENIUS | 204215 |
| BANK Syariah Indonesia (eks BRI Syariah) | 204192 |
| Bank Sulteng (Sulawesi Tengah) | 204193 |
| Bank Sultra | 204194 |
| Bank ING Indonesia | 204195 |
| Bank Credit Agricole Indosuez | 204196 |
| Bank ABN Amro | 204197 |
| Bank Keppel Tatlee Buana | 204198 |
| Bank Haga | 204199 |
| Bank IFI | 204200 |
| Bank Hagakita | 204201 |
| Bank Windu Kentjana | 204202 |
| Bank Swaguna | 204203 |
| Bank Sri Partha | 204204 |
| Bank Bintang Manunggal | 204205 |
| Bank Harfa | 204207 |
| Bank Persyarikatan Indonesia | 204208 |
| Bank Akita | 204209 |
| Bank Liman International | 204210 |
| Bank Artos IND | 204211 |
| Bank Purba Danarta | 204212 |
| Bank Harda | 204213 |
| Bank Merincorp | 204214 |
| Citibank N.A | 204216 |
| Bank Jawa Barat | 204218 |
| Bank Syariah Indonesia (Eks BNI SYARIAH) | 204219 |
| Bank Syariah Indonesia (Eks BSM) | 204220 |
| Bank KB Bukopin Syariah | 204221 |
| Bank Lippo | 204222 |
| Indosat Dompetku | 204223 |
| Telkomsel Tcash | 204224 |
| Digibank | 204225 |
| TMRW by UOB Indonesia | 204226 |
| BANK NATIONALNOBU | 204227 |
| KUSTODIAN SENTRAL EFEK INDONESIA | 204228 |
| BANK RAKYAT INDONESIA (PERSERO) | 204229 |
| BANK CENTRAL ASIA | 204230 |
| BPD KALIMANTAN TENGAH | 204231 |
| BPD KALIMANTAN SELATAN | 204332 |
| BANK TABUNGAN PENSIUNAN NASIONAL Syariah | 204333 |
| BANK NEGARA INDONESIA (PERSERO) | 204334 |
| BANK TABUNGAN NEGARA (PERSERO) | 204335 |
| BANK BPD SULAWESI SELATAN | 204336 |
| BANK INDONESIA | 204337 |
| BPD JAWA TIMUR | 204338 |
| BPD DAERAH ISTIMEWA YOGYAKARTA | 204339 |
| BANK ROYAL OF SCOTLAND(RBS) | 204340 |
| BANK BSG( Bank sulawesi Utara dan Gorontalo) | 204341 |
代收下单
/merchant-api/collect/orders币种固定为 IDR,不传币种字段;仅使用表中正式字段名。回调地址由平台预先配置,不能在下单时传入 notifyUrl 或 callbackUrl。
| 字段 | 类型 | 要求 | 说明 |
|---|---|---|---|
| merchantOrderId | String | 必填 | 商户侧唯一订单号。 |
| transactionAmount | Decimal | 必填 | 订单金额,必须大于 0,最多 4 位小数。 |
| method | Integer | 必填 | 填写一个已开通的支付方式正整数编号;使用 JSON 整数,不接受字符串、小数或列表。 |
| payerName | String | 必填 | 付款人姓名,不能为空。 |
| mobile | String | 必填 | 付款人手机号,不能为空。 |
| contactEmail | String | 必填 | 付款人邮箱,不能为空。 |
| redirectUrl | String | 可选 | 支付完成后的商户跳转地址。 |
{
"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 为收银台过期时间。
代付下单
/merchant-api/disburse/orders币种固定为 IDR;收款账号去除空格和连字符后须为 ASCII 数字,最多 128 位,保留前导零。回调地址由平台预先配置。
| 字段 | 类型 | 要求 | 说明 |
|---|---|---|---|
| merchantOrderId | String | 必填 | 商户侧唯一订单号。 |
| method | Integer | 必填 | 填写一个已开通的代付支付方式正整数编号;使用 JSON 整数,不接受字符串、小数或列表。 |
| transactionAmount | Decimal | 必填 | 代付金额,必须大于 0,最多 4 位小数。 |
| beneficiaryName | String | 必填 | 收款人姓名,不能为空。 |
| beneficiaryMobile | String | 必填 | 收款人手机号,不能为空。 |
| beneficiaryEmail | String | 必填 | 收款人邮箱,不能为空。 |
| recipientAccountNo | String | 必填 | 收款账号,不能为空;去除空格和连字符后须为纯数字,最多 128 位。 |
{
"merchantOrderId": "MCH-PAYOUT-202607130001",
"method": 204001,
"transactionAmount": 10000.00,
"beneficiaryName": "Example Recipient",
"beneficiaryMobile": "6281234567890",
"beneficiaryEmail": "receiver@example.com",
"recipientAccountNo": "1234567890"
}响应 data 字段
| 字段 | 类型 | 说明 |
|---|---|---|
| merchantOrderId | String | 商户订单号。 |
| platformOrderId | String | 平台代付订单号,后续优先用它查单。 |
| transactionAmount | Decimal | 代付订单金额。 |
| currencyCode | String | 订单币种。 |
| paymentMethodNo | Integer | 本单代付支付方式编号。 |
| feeAmount | Decimal | 商户手续费;尚无费用时返回 0.0000。 |
| orderStatus | String | 下单时的订单状态;代付下单响应使用此字段,不使用 status。 |
| statusReason | String | 平台统一状态说明:FAILED 为 payment failed,REVERSED 为 payment was reversed,PENDING_REVIEW 为 payment is under review,其余为空字符串。 |
| createTime | String | 创建时间,格式 yyyy-MM-dd HH:mm:ss。 |
| completeTime | String 或 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
| 字段 | 类型 | 说明 |
|---|---|---|
| merchantOrderId | String | 商户订单号。 |
| platformOrderId | String | 平台代收订单号。 |
| orderAmount | Decimal | 代收订单金额。 |
| completeAmount | Decimal | 已支付金额。 |
| status | String | 当前订单状态。 |
| payMethod | Integer | 本单支付方式编号。 |
| createTime | String | 创建时间,格式 yyyy-MM-dd HH:mm:ss。 |
| completeTime | String 或 null | 完成时间;尚未完成时可为 null。 |
| serviceFee | Decimal 或 null | 商户手续费,尚未产生时可为 null。 |
| additionalInfo | String | 固定为空字符串。 |
{
"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
| 字段 | 类型 | 说明 |
|---|---|---|
| merchantOrderId | String | 商户订单号。 |
| platformOrderId | String | 平台代付订单号。 |
| status | String | 当前订单状态。 |
| statusReason | String | 平台统一状态说明:FAILED 为 payment failed,REVERSED 为 payment was reversed,PENDING_REVIEW 为 payment is under review,其余为空字符串。 |
| currency | String | 订单币种。 |
| orderAmount | Decimal | 代付订单金额。 |
| createTime | String | 创建时间,格式 yyyy-MM-dd HH:mm:ss。 |
| completeTime | String 或 null | 完成时间;尚未完成时可为 null。 |
| serviceFee | Decimal | 商户手续费;尚无费用时返回 0.0000。 |
| additionalInfo | String | 固定为空字符串。 |
{
"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"
}余额查询
/merchant-api/accounts/balance| 字段 | 类型 | 要求 | 说明 |
|---|---|---|---|
| currency | String | 可选 | 币种,默认 IDR,大小写不敏感。 |
{
"currency": "IDR"
}响应 data 字段
| 字段 | 类型 | 说明 |
|---|---|---|
| currency | String | 查询币种,与请求归一化后一致。 |
| availableBalance | Integer | 可用余额,可用于发起代付与提现。 |
| pendingSettleBalance | Integer | 待结算余额,尚未结算进可用余额。 |
| frozenBalance | Integer | 当前不可用的冻结余额。 |
余额以整数 IDR 返回,如存在小数则向下取整。该接口为只读查询。
异步回调
平台向预先登记的地址发送 POST 请求。建议使用 HTTPS。回调采用加密信封,密钥职责与商户请求相反。
回调请求头
| Header | 说明 |
|---|---|
| X-Api-Key | 商户 API Key,用于识别平台投递的目标商户。 |
| X-Timestamp | 本次回调的 Unix 时间戳。 |
| X-Nonce | 本次回调的随机字符串;每次重试均重新生成。 |
| X-Encrypt-Key | Base64:平台用已登记的商户 RSA 公钥加密的一次性 AES Key。 |
| X-IV | Base64:本次 AES-GCM 加密使用的 IV。 |
| X-Sign | Base64:平台 RSA 私钥对规范串执行 SHA256withRSA 的签名。 |
| X-Key-Version | 可选;平台签名密钥版本,供商户选择对应的平台公钥。 |
{
"cipherText": "BASE64_AES_GCM_CIPHER_TEXT"
}处理流程
- 核对 API Key,使用平台 RSA 公钥验证
X-Sign。签名原文仍为 API Key、时间戳、nonce、cipherText 四段直接拼接。 - 使用商户 RSA 私钥按 RSA/ECB/PKCS1Padding 解密
X-Encrypt-Key;使用解密得到的 AES Key 和 IV 进行 AES-GCM 解密。 - 验签和解密成功后,以
eventNo对业务事件去重;同一事件重试时 eventNo 不变。重复事件不得重复处理,但应返回成功确认。 - 处理完成后严格返回 HTTP 200,正文为纯文本
SUCCESS,不能带空格、换行或 JSON 包装。
失败时默认总共尝试 5 次;首次失败后的退避间隔为 30、60、120、240 秒,并受平台最大退避配置限制。
回调明文字段
| 字段 | 类型 | 说明 |
|---|---|---|
| eventNo | String | 回调事件号;同一事件重试时保持一致,商户以此去重。 |
| merchantId | Long | 平台商户 ID。 |
| platformOrderNo | String | 平台订单号。 |
| merchantOrderNo | String 或 null | 商户订单号。 |
| callbackType | String | 事件类型,如 PAYIN_SUCCESS、PAYIN_REFUND、PAYOUT_SUCCESS、PAYOUT_FAILED、PAYOUT_REVERSED。 |
| status | String | 事件对应的结果状态。 |
| amount | Decimal 或 null | 订单金额。 |
| paidAmount | Decimal 或 null | 代收实付金额。 |
| refundAmount | Decimal 或 null | 代收退款事件的退款金额。 |
| merchantFeeAmount | Decimal 或 null | 商户手续费。 |
| refundNo | String 或 null | 代收退款事件的退款号。 |
| currency | String 或 null | 订单币种。 |
| timestamp | String | 事件生成时间,ISO-8601 格式。 |
| traceId | String | 链路追踪号。 |
{
"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_ALLOWED | HTTP 方法不受接口支持。 | HTTP 405;按接口清单使用 POST。 |
| IDEMPOTENCY_CONFLICT | 同一商户订单号的请求内容不一致。 | 先查原单,保持同一订单号的业务内容一致。 |
| CONFLICT | 订单结果暂未就绪。 | 稍后查单,无法确认时联系平台,不另建重复付款。 |
| TOO_MANY_REQUESTS | 请求频率超限。 | 降低频率并退避重试,每次使用新的时间戳、nonce 和签名。 |
| PAYMENT_SERVICE_UNAVAILABLE | 支付服务暂不可用。 | 先查单确认是否已受理,无法确认时联系平台。 |
| INTERNAL_ERROR | 服务异常。 | 保留 traceId 并联系平台;不能据此认定支付失败。 |
展开认证与加密错误
| code | message | 原因 | 处理建议 |
|---|---|---|---|
| UNAUTHORIZED | {header} is required | 缺少必需的请求头,或请求头为空。 | 补齐 X-Api-Key、X-Timestamp、X-Nonce、X-Sign、X-Encrypt-Key、X-IV;{header} 会替换为缺失的请求头名。 |
| UNAUTHORIZED | merchant not found | API Key 未匹配到商户。 | 核对接入环境和 API Key;无法确认时联系平台核查。 |
| FORBIDDEN | merchant is disabled | 商户已停用。 | 联系平台确认商户状态,恢复启用后再请求。 |
| FORBIDDEN | merchant API access is disabled | 商户 API 接入已关闭。 | 联系平台启用 API 接入后重试。 |
| FORBIDDEN | merchant ip whitelist is required | 商户尚未配置请求 IP 白名单。 | 向平台登记商户服务器的公网出口 IP 或 CIDR。 |
| FORBIDDEN | request ip is not allowed | 本次请求的真实 IP 未命中白名单。 | 核对商户服务器的公网出口 IP,联系平台登记或更新白名单。 |
| BAD_REQUEST | timestamp is invalid | X-Timestamp 无法解析为整数时间戳。 | 按协议传入 Unix 秒级整数字符串,并用同一字符串参与签名。 |
| UNAUTHORIZED | timestamp is expired | 请求时间超出平台允许窗口,默认 300 秒。 | 校准服务器时钟,重新生成时间戳、nonce 和签名。 |
| BAD_REQUEST | nonce is too long | X-Nonce 长度超过 128。 | 使用长度不超过 128 的随机字符串。 |
| UNAUTHORIZED | nonce replay detected | 同一商户在有效期内重复使用 nonce。 | 每次请求及重试生成新的随机 nonce,并重新签名。 |
| BAD_REQUEST | request body is required | HTTP 请求体为空。 | 发送包含 cipherText 的 JSON 加密信封。 |
| BAD_REQUEST | encrypted body is invalid | 请求体不是可解析的加密信封 JSON。 | 检查 Content-Type 和 JSON 格式,发送 {"cipherText":"..."}。 |
| BAD_REQUEST | cipherText is required | 加密信封缺少 cipherText 或该字段为空。 | 将 AES-GCM 密文和认证 Tag 拼接后 Base64 编码,放入 cipherText。 |
| UNAUTHORIZED | merchant public key is required | 平台未登记商户验签公钥。 | 向平台登记与商户签名私钥配对的 RSA 公钥。 |
| UNAUTHORIZED | merchant public key is invalid | 已登记的商户 RSA 公钥无法解析。 | 联系平台核对已登记公钥的 PEM、Base64 和 RSA 格式。 |
| UNAUTHORIZED | signature is invalid | X-Sign 无法按签名格式解析或验证。 | 检查签名的 Base64 格式、RSA 密钥和 SHA256withRSA 算法。 |
| UNAUTHORIZED | signature verification failed | 签名原文不一致,或商户私钥与登记公钥不匹配。 | 只签 X-Api-Key + X-Timestamp + X-Nonce + cipherText;不添加分隔符,不签信封、IV 或明文;重试使用新 nonce。 |
| UNAUTHORIZED | encrypt key is invalid | X-Encrypt-Key 无法被平台私钥解密。 | 用该商户的平台 RSA 公钥和 RSA/ECB/PKCS1Padding 加密原始 AES Key 字节,结果使用 Base64。 |
| UNAUTHORIZED | payload decrypt failed | AES-GCM 密文、AES Key、IV 或认证 Tag 不匹配。 | 核对本次 AES Key 和 Base64 IV;cipherText 必须是 Base64(ciphertext || authTag),Tag 为 128 bit。 |
| UNAUTHORIZED | merchant authentication failed | 商户身份验证未通过。 | 使用正确的商户 API 地址及完整加密签名请求;仍出现时提供 traceId 联系平台。 |
| UNAUTHORIZED | unauthorized | 身份验证未通过。 | 核对 API Key、请求头和签名;仍失败时联系平台。 |
| FORBIDDEN | forbidden | 当前请求未获授权。 | 核对商户状态及授权范围,联系平台确认。 |
展开请求参数与业务错误
| code | message | 原因 | 处理建议 |
|---|---|---|---|
| BAD_REQUEST | request body is invalid | 解密后的业务 JSON 格式、字段名或字段类型不正确,包括旧别名、未定义字段,以及 method 为字符串、小数、列表或超出 Integer 范围。 | 仅使用文档中的正式字段名;检查 JSON 语法和参数类型,金额使用数值,method 使用单一 JSON 整数。 |
| BAD_REQUEST | merchantOrderId must not be blank | 下单缺少商户订单号,或订单号为空白。 | 填写唯一的 merchantOrderId;同一笔业务重试保持订单号和业务内容一致。 |
| BAD_REQUEST | payerName must not be blank | 代收下单缺少付款人姓名,或姓名为空白。 | 填写非空的 payerName。 |
| BAD_REQUEST | mobile must not be blank | 代收下单缺少付款人手机号,或手机号为空白。 | 填写非空的 mobile。 |
| BAD_REQUEST | contactEmail must not be blank | 代收下单缺少付款人邮箱,或邮箱为空白。 | 填写非空的 contactEmail。 |
| BAD_REQUEST | beneficiaryName must not be blank | 代付下单缺少收款人姓名,或姓名为空白。 | 填写非空的 beneficiaryName。 |
| BAD_REQUEST | beneficiaryMobile must not be blank | 代付下单缺少收款人手机号,或手机号为空白。 | 填写非空的 beneficiaryMobile。 |
| BAD_REQUEST | beneficiaryEmail must not be blank | 代付下单缺少收款人邮箱,或邮箱为空白。 | 填写非空的 beneficiaryEmail。 |
| BAD_REQUEST | payment method must be exact | 代收或代付下单的 method 缺失、为 null、为零或负数。 | 填写 method,使用可用支付方式页面中已开通的一个正整数编号。 |
| BAD_REQUEST | amount must be greater than zero | transactionAmount 缺失、为零或负数。 | 填写大于 0 的订单金额。 |
| BAD_REQUEST | amount scale must be less than or equal to 4 | 订单金额超过 4 位小数。 | 按业务规则确定金额后,最多保留 4 位小数再提交。 |
| BAD_REQUEST | recipientAccountNo is required | 代付下单未提供收款账号,或账号为空白。 | 填写非空的 recipientAccountNo。 |
| BAD_REQUEST | recipientAccountNo is invalid | 收款账号去除空格和连字符后不是纯数字,或超过 128 位。 | 核对 recipientAccountNo,提交有效的数字账号。 |
| BAD_REQUEST | merchant is not available | 商户主体不存在或未处于可用状态。 | 联系平台核对商户状态。 |
| BAD_REQUEST | merchant currency is not supported | 订单币种与商户配置的币种不一致。 | 联系平台核对币种配置;代收固定 IDR,无需传币种。 |
| BAD_REQUEST | payin is not enabled for merchant | 商户没有已启用的 IDR 代收配置。 | 联系平台开通或启用代收配置后再请求。 |
| BAD_REQUEST | payout is not enabled for merchant | 商户没有对应币种的已启用代付配置。 | 联系平台开通或启用该币种的代付配置。 |
| BAD_REQUEST | payin callback URL is not configured | 平台未配置商户代收回调地址。 | 联系平台登记代收回调地址;下单请求不传 notifyUrl 或 callbackUrl。 |
| BAD_REQUEST | payout callback URL is not configured | 平台未配置商户代付回调地址。 | 联系平台登记代付回调地址;下单请求不传 notifyUrl 或 callbackUrl。 |
| BAD_REQUEST | payin amount is outside merchant range | 代收金额超出商户代收单笔限额。 | 核对代收最小、最大单笔金额,必要时联系平台调整限额。 |
| BAD_REQUEST | payout amount is outside merchant range | 代付金额超出商户代付单笔限额。 | 核对代付最小、最大单笔金额,必要时联系平台调整限额。 |
| BAD_REQUEST | payment method is not available | 支付方式不存在、已停用,或不支持本次币种或订单方向。 | 从可用支付方式页面选取对应方向和币种的有效方式。 |
| BAD_REQUEST | payment method is disabled for merchant | 商户的具体支付方式已被停用。 | 联系平台确认授权状态,启用后再请求。 |
| BAD_REQUEST | payment method is not enabled for merchant | 商户未开通该支付方式,或本次金额不在允许范围内。 | 核对支付方式授权及单笔限额,联系平台确认配置。 |
| BAD_REQUEST | amount is outside merchant payment method range | 金额超出商户具体支付方式的单笔限额。 | 核对可用支付方式页面中的该方式最小、最大金额。 |
| BAD_REQUEST | merchant balance account is not available | 代付预检查未找到已启用的可用余额账户。 | 联系平台核对商户该币种的账户状态。 |
| BAD_REQUEST | merchant available balance is insufficient | 商户可用余额不足以覆盖代付本金和商户手续费。 | 查询余额并补足本金及手续费后再请求;超时重试前先查单。 |
| BAD_REQUEST | invalid request | 请求不符合接口要求。 | 核对请求字段和类型;无法定位时提供 traceId 联系平台。 |
| code | message | 原因 | 处理建议 |
|---|---|---|---|
| FORBIDDEN | payment request was rejected | 当前付款请求未获准处理。 | 提供 traceId 联系平台核实,不要更换订单号反复发起同一笔付款。 |
| NOT_FOUND | order not found | 当前查询条件未找到本商户订单。 | 核对接入环境和订单号;下单超时后先查单,仍无法确认时联系平台。 |
| FORBIDDEN | order does not belong to merchant | 订单不属于当前商户。 | 核对商户身份和订单号,仅查询本商户订单。 |
| IDEMPOTENCY_CONFLICT | idempotency conflict | 同一商户订单号的业务内容与首次请求不同。 | 先查原单,保持同一订单号的业务内容一致。 |
| CONFLICT | order is not ready | 订单结果暂未就绪。 | 稍后按原订单号查单;持续无法确认时提供 traceId 联系平台,不另建同一笔业务订单。 |
| TOO_MANY_REQUESTS | rate limit exceeded | 请求频率超过平台限制。 | 降低频率并退避重试,使用新的时间戳、nonce 和签名。 |
| TOO_MANY_REQUESTS | too many requests | 请求频率超过平台限制。 | 降低频率并退避重试,使用新的时间戳、nonce 和签名。 |
| PAYMENT_SERVICE_UNAVAILABLE | payment service is temporarily unavailable | 支付服务暂不可用。 | 先查单确认是否受理;无法确认时提供 traceId 联系平台,避免重复付款。 |
| INTERNAL_ERROR | internal 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) {
}
}