PayPal/ACH/VENMO支付接口
2645字约9分钟
1:对接说明
1.1:统一请求方式
- Http Method : POST
- Content type : application/json
- 测试环境网关:https://uat-trade.xwinpay.tech
- 正式环境网关:https://trade.xwinpay.tech
- 业务请求地址:URL + 资源路径
- 注意: 注意:如有业务逻辑关联处理,请使用code字段,name,message等字段会随时变动。
1.2:开户后参数(API请求需要)
- 商户编码(请求头: X-MERCHANT-CODE):123456
- 商户名称: 8848
- 商户私钥(商户下单加密) :MIICdQIBADANBgkqhkiG9w0B********qqLS
- 商户公钥(平台接收解密):MIGIb3DQEBAQUAA4GNAD*****QAB
- 平台公钥(商户接收回调解密):MIGfMA0GCSqGSIb3DEBAQ*****QAB
- apikey(请求头: X-API-KEY):74918b3925ccd0******27478087
1.3:Http Header 参数
参数名 | 必须 | 类型 | 说明 |
---|---|---|---|
X-MERCHANT-CODE | 是 | Http Header (String) | 商户编码 |
X-API-KEY | 是 | Http Header (String) | 商户后台获取apikey |
1.4:Http 请求Body参数
{
"accountNumber": "",
"amount": 50000,
"app": "NICE",
****,
****,
****,
"sign": "SQiqFRTmBn0***4Yz8iUCdeyWi/JObp9mXzXfNCQ+zbOCQ="
}
1.5:Http 响应Body参数
{
"code": 200,
"message": "操作成功",
"data": {
"merchantNo": "58256833",
"merchantOrderNo": "5825683320230508569",
****,
****,
****
}
}
参数名 | 必选 | 类型 | 说明 |
---|---|---|---|
code | 是 | Integer | 接口状态(200: 接口响应正常) |
message | 是 | String | 接口描述 |
data | 是 | Object | 业务数据 |
1.6:错误编码
错误编码 | 英文回复 | 中文含义 |
---|---|---|
200 | Request successful"), | |
415 | parameter passing exception | 参数传递异常 |
500 | Please contact technical support for business exceptions. | 业务异常,请联系技术支持 |
4001 | Merchant code cannot be empty | 商户编码不能为空 |
4002 | Merchant does not exist | 商户不存在 |
4003 | Merchant APIKEY is incorrect | 商户APIKEY不正确 |
4004 | Merchant APIKEY cannot be empty | 商户APIKEY不能为空 |
4005 | The sign signature cannot be empty. | sign签名不能为空 |
4006 | Sign Failure | sign验签失败 |
4007 | The current merchant does not have this type of currency account | 当前商户未开通该类货币账户 |
4008 | The current merchant balance is insufficient and the payment on behalf of the merchant has failed! | 当前商户余额不足,代付失败! |
40081 | There is an outstanding escrow order on the account! | 账户存在未完成代付订单! |
40082 | Payment on behalf of an order creation exception! | 代付创建订单异常! |
40083 | Payment on behalf of an order rate calculation anomaly! | 代付订单费率计算异常! |
4009 | Duplicate Merchant Order Number | 商户订单号重复 |
4010 | Merchant order number not passed | 商户订单号未传递 |
4011 | Missing parameter error | 缺少参数错误 |
4012 | Merchant order does not exist | 商户订单不存在 |
4013 | Current order parameter validation error | 当前订单参数验证错误 |
4014 | channel request param error | 通道请求参数错误 |
4015 | No permission to pay for products, please contact the administrator | 支付产品无权限,请联系管理员 |
4016 | Merchant fee calculation method is wrong | 商户手续费计算方式错误 |
4017 | amount in excess of limit | 金额超出限制 |
4018 | bank request param error | 银行参数错误 |
4019 | ewallet request param error | 钱包参数错误 |
4020 | Abnormal rate setting | 费率设置异常 |
4021 | The bank has suspended use, please contact the administrator. | 银行暂停使用,请联系管理员 |
4022 | param error" | 参数错误 |
4023 | reached daily transaction count limit | 已达到每日交易笔数上限 |
4024 | transaction must between %d and %d" | 单笔交易金额限制 |
4025 | The daily transaction volume has reached %d" | 日交易量已达 |
5001 | The payment channel is temporarily unavailable, please try again later. | 支付渠道暂不可用,请稍后再试 |
5002 | Order data does not exist | 订单数据不存在 |
5003 | Unknown handling fee calculation method | 未知手续费计算方式 |
5004 | Not getting the fee calculation method | 未获取手续费计算方式 |
5006 | Payment request is abnormal, please check the parameters | 支付请求异常,请检查参数 |
5007 | Payment product is not configured | 支付产品未配置 |
5008 | Payment exception | 支付异常 |
50071 | The merchant has not configured payment products | 商户未配置支付产品 |
40084 | Abnormal data in access channel for payment orders | 代付订单接入通道数据异常 |
40085 | Pipeline write exception | 流水写入异常 |
1.7:公共订单状态枚举:
参数名 | 说明 |
---|---|
PENDING | 交易中 |
COMPLETED | 入账成功 |
FAILED | 交易失败 |
CANCEL | 取消订单 |
NO_PAY | 未支付 |
1.8:签名统一生成规则:
基于RSA的签名验证方式。 将所有非空参数的key按照ASCII排序后,取参数值(不包含Key)进行拼接后 采用RAS算法对字符串计算,算出签名字符串.
1.9:参数示例值
参数 | 类型 |
---|---|
merchantCode | S820211021094748000001 |
orderNum | T1642592278863 |
payMoney | 150.6 |
notifyUrl | your notify ur |
dateTime | 2022/1/1 10:55 |
expiryPeriod | 1440 |
1.10:拼接字符串
示例 首先根据参数Key按照ASCII排序,排序后加密字符串. StrA = 2022-01-01 10:55:[email protected]
1440S820211021094748000001014JackMayour notify urlT1642593166888150.60082122965511Test Pay
2:加密说明(JAVA)
2.1:SignUtils签名字符串方法
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class SignUtils {
/**
* 对象签名
*
* @param map 签名对象
* @return
*/
public static String getSign(Map<String, Object> map) {
Set<String> keys = map.keySet();
List<String> list = new ArrayList<>(keys);
Collections.sort(list);
// 构造签名键值对的格式
StringBuilder sb = new StringBuilder();
// 构造签名键值对的格式
for (String key : list) {
Object val = map.get(key);
if (!("".equals(val) || val == null || "sign".equals(key))) {
sb.append(val);
}
}
return sb.toString();
}
}
2.2:签名工具方法
package com.example.eventdemo;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* SHA256WithRSA 非对称加密算法
* Base64编码使用 apache工具类, maven依赖 https://mvnrepository.com/artifact/commons-codec/commons-codec
*/
public class SHA256WithRSAUtils {
/**
* 密钥算法
*/
public static final String ALGORITHM_RSA = "RSA";
private SHA256WithRSAUtils() {
}
/**
* 得到私钥
*
* @param privateKey 密钥字符串(经过base64编码)
*/
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
return (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
}
/**
* 得到公钥
*
* @param publicKey 密钥字符串(经过base64编码)
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
return (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
}
/**
* RSA算法公钥解密数据
*
* @param data 待解密的经过Base64编码的密文字符串
* @param key RSA公钥字符串
* @return RSA公钥解密后的明文字符串
*/
public static String decryptByPublicKey(String data, String key) {
try {
RSAPublicKey publicKey = getPublicKey(key);
Cipher cipher = Cipher.getInstance(ALGORITHM_RSA);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("decrypt [" + data + "] error", e);
}
}
/**
* RSA算法私钥加密数据
*
* @param data 待加密的明文字符串
* @param key RSA私钥字符串
* @return RSA私钥加密后的经过Base64编码的密文字符串
*/
public static String encryptByPrivateKey(String data, String key) {
try {
//通过PKCS#8编码的Key指令获得私钥对象
RSAPrivateKey privateKey = getPrivateKey(key);
Cipher cipher = Cipher.getInstance(ALGORITHM_RSA);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(StandardCharsets.UTF_8), privateKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("encrypt [" + data + "] error", e);
}
}
/**
* RSA算法分段加解密数据
*
* @param cipher 初始化了加解密工作模式后的javax.crypto.Cipher对象
* @param opmode 加解密模式,值为javax.crypto.Cipher.ENCRYPT_MODE/DECRYPT_MODE
* @return 加密或解密后得到的数据的字节数组
*/
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) throws IOException {
int maxBlock = 0;
if (opmode == Cipher.DECRYPT_MODE) {
maxBlock = keySize / 8;
} else {
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try {
while (datas.length > offSet) {
if (datas.length - offSet > maxBlock) {
buff = cipher.doFinal(datas, offSet, maxBlock);
} else {
buff = cipher.doFinal(datas, offSet, datas.length - offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
} catch (Exception e) {
throw new RuntimeException("encrypt [" + maxBlock + "] error", e);
}
byte[] resultDatas = out.toByteArray();
out.close();
return resultDatas;
}
}
2.3:测试demo
/**
* 测试类
*
* @param args
*/
public static void main(String[] args) {
String privateKey = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANkyOMTXOBCWBy3Ps39b/rymWWFqujOPxpdp1IAVlk8PuTxWBBscFIHLVAXdf9sCqAw+Wm89RSGDzky6TVIgRXSo6+6hk3ZSIMnu53f9cZlL2G/3+b4BRP3lKsP7nVTTwAEOH1uiJFlJbcCaRuRpGyQJ0mNg/HbCTIWxdKyelBXPAgMBAAECgYAB/eTpYTPhaw7Ly8DQpS5T2o6tRwZIHMRsdQr+1bPYK8O+GufUu9AwVIYDu8FFZ+PUoOnBZWVx5jyJFZhJ7YPVhrZSE4XqMqrtJqIER7m0VDejDSKcKIm6pnmdZHlIvQqkhoCdaCI9kFYgmS0x82SrlMqBpsA9qkSYrRJMZdDv2QJBAPiqjtR5LmfHtrgWE8JezcQqq6IQGQXzGTY73PpuBdIuRAPIvE3b8zuccyUPdozwNzxWEY429aw/XnkZQImZCHsCQQDfmhA2vmSh4eJqtpfhoQqa3IH7wQ20O6nQeIr10lwScfnj8UiKh5GuiOrus8EY0KIQUtA5DAks8RBcJfpNfYm9AkEA9n37P1swSOeLlEcuJwpa5g12PRu/8knbwArvLb9KPeJmwWmGX5ecMIcRDLebSHIGDuUyWcrZFHlsaJZDhyIPaQJBAIgxoNqXUVhA69Yv7YbivkDhOtMLHbu/84klQw7D2Izrm1e5qYOnW5bBksdd+amRuoTSzD1TFWuoUVyvTSxR4MkCQAgTzQAytBOYITp/Ks+0KOrsDPP7KquBDLKNLttayW+0L+ZLJyZvLCpY9Rj1T8lElUD2O16NBp8SafLz7XYEgLk=";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZMjjE1zgQlgctz7N/W/68pllharozj8aXadSAFZZPD7k8VgQbHBSBy1QF3X/bAqgMPlpvPUUhg85Muk1SIEV0qOvuoZN2UiDJ7ud3/XGZS9hv9/m+AUT95SrD+51U08ABDh9boiRZSW3AmkbkaRskCdJjYPx2wkyFsXSsnpQVzwIDAQAB";
Map<String, Object> param = new HashMap<>();
param.put("merOrderNo", "123456");
param.put("amount", 100);
String sign = SignUtils.getSign(param);
// sign = 100123456
System.out.println("sign = " + sign);
String encryptData = SHA256WithRSAUtils.encryptByPrivateKey(sign, privateKey);
String decryptData = SHA256WithRSAUtils.decryptByPublicKey(encryptData, publicKey);
//加密:bg2Q3dZY0Tv9ycQUMGKVOL7rR1sRZCUk5EnkYeOCpKaCgg1E/NRM9njtNEW5205/BABW6PHnLX9tmNR+C7n9Aj4edsXdCr1uOdgSgaki+4M/Kh2Z3vO+PrYJxBRZjolV+ue51RUqlxYQXcKrygj74QLCLsFRKzk4V2qcHVhs8e4=
System.out.println("加密:" + encryptData);
//解密:100123456
System.out.println("解密:" + decryptData);
//验签:true
System.out.println("验签:" + sign.equals(decryptData));
}
3:账户余额查询
URL:/common/openapi/account/balance
param 参数:
参数名 | 必选 | 固定值 | 说明 |
---|---|---|---|
country | 是 | UN | 国家编码 |
currency | 是 | USD | 币种 |
request
{
"param": {
"country": "UN",
"currency": "USD",
},
"sign": "Gw6D0kuWlnngLMiSxQQ2Dz2ciZz4LUZ7gWA8dRkEbcsE+UoiTOy6T3g6vNvGUlz+vPV2aRHKZ6aPBh0PaJkWW36T/lf8qId4D9uh8lFeTe7zW0+hTgTnN9YmruA6rlRNCo9QytbMRk6qkEZh1PZ2NrFnC77FhYVEVdOk6bIOv/vnGo1RVpbmsO+Gw+tPnvkQ1jvVDEHUlKgnAFGYa7+oWw8eClh9T4Ob7ZUPI0Z+dhefAmEX+z8DCyUxZrtbgA0li21KW2TmErIiRJQxep/f4quWRoTKkPziiMefKo1jFgbyL/gVzgkp91YaMMyk1QJKW1UoZ8iqHKNMI6pmvBCk8g=="
}
response
{
"code": 200,
"message": "",
"data": {
"merId": "",
"merName": "",
"currency": "",
"amountTotal": "0.00",
"freeAmount": "1085178690.00",
"unrecordedAmount": "0.00",
"freezeAmount": "1085178690.00",
"frzBal": "0.00"
}
}
返回data参数说明
参数名 | 类型 | 说明 |
---|---|---|
merId | string | 商户号 |
merName | string | 商户名称 |
currency | string | 币种 |
amountTotal | BigDecimal | 总金额 |
freeAmount | BigDecimal | 可用金额 |
unrecordedAmount | BigDecimal | 在途金额 |
freezeAmount | BigDecimal | 冻结金额 |
4:代付接口
4.1:代付下单
URL:/usd/openapi/payout
body请求参数:
参数名 | 必选 | 类型 | 说明 |
---|---|---|---|
merchantOrderNo | 是 | string | 商户订单号 |
timestamp | 是 | string | 时间戳 |
payMode | 是 | string | 支付方式,BANK,EWALLET |
channelCode | 是 | string | 支付方式为EWALLET时,支持PayPal,VENMO 支付方式为BANK时,支持ACH |
currency | 是 | string | 币种:USD |
amount | 是 | string | 金额,支持两位小数,不能包含无效小数位,不能包含逗号 |
notifyUrl | 是 | string | 回调地址 |
remark | 是 | string | 备注,仅支持字母、数字、空格和下划线 |
firstName | 是 | string | 名,只包含字母,3-11个字符 |
lastName | 是 | string | 姓,只包含字母,2-10个字符 |
accountNumber | 否 | string | 账号,PayPal时不需要传递 |
是 | string | 邮箱,PayPal时,邮箱即为账户,需激活并绑定银行卡 | |
bankRouting | 否 | string | 收款银行路由,ACH方式必传 |
country | 否 | String | 收款人所在国家,ACH方式必填,美国:US |
address1 | 否 | String | 收款地址-详细街道,ACH方式必填 |
address2 | 否 | String | 收款地址-城市,ACH方式必填 |
address3 | 否 | String | 收款地址(省、州),ACH方式必填 |
postalCode | 否 | String | 收款邮编,ACH方式必填 |
sign | 是 | string | 签名 |
request,仅为参考示例,具体信息以表格为准
{
"amount": "",
. . .
. . .
. . .
"sign": ""
}
response
{
"code": 0,
"data": {
"merchantOrderNo": "",
"plaOrderNo": 0,
"sign": "",
"status": "",
"timestamp": 0
},
"message": ""
}
返回data参数说明
参数名 | 类型 | 说明 |
---|---|---|
merchantNo | string | 平台商户号 |
plaOrderNo | string | 平台订单号 |
sign | string | 签名 |
status | string | 交易状态('PENDING','COMPLETED','FAILED') |
5:订单回调通知
URL:系统交易完成或余额不足时会发送回调到notify_url回调通知地址
响应: 接受请求后请返回“success”字符串给我们
请求方式:
- method: POST
- Content Type: application/json
body参数:
参数名 | 类型 | 说明 |
---|---|---|
amount | string | 订单金额 |
fee | string | 手续费 |
merchantNo | string | 平台商户号 |
merchantOrderNo | string | 商户订单号 |
merchantPayTime | string | 商户交易时间 |
plaOrderNo | string | 平台订单号 |
plaStatusTime | string | 平台订单状态流转时间 |
status | string | 支付状态('COMPLETED','FAILED') |
errorCode | string | 错误编码(status为FAILED时有值) |
errorMessage | string | 错误信息(status为FAILED时有值) |
sign | string | 签名使用平台公钥验签(取reqeust body里的数据,) |
request
{
"merchantNo": "58256833",
"merchantOrderNo": "5825683320230508569",
"plaOrderNo": "1223050839733260230",
"status": "COMPLETED",
"amount": "100000",
"merchantPayTime": "2023-05-08 11:14:01",
"plaStatusTime": "2023-05-08 11:14:30",
"fee": "200",
"sign": "bXE1dI7TrjmRvIGrcfgBqMKhSCsTAtvtoZBd4GIl6NQOqd/8qGudTmX3WJn37NX+v8ehaSGUNSa+4ocAzWQlNQMqJ/OH2Hd+mViXjR2+6n3q4WsvHTzoDGR0VT85jT2cjR421RnnzaC/eLy/02Zx/j7y2snodVnzi6L3RfiY2gM="
}
6:订单状态查询
URL:/usd/openapi/payout/orderStatus
body请求参数:
参数名 | 必选 | 类型 | 说明 |
---|---|---|---|
sign | 是 | string | 签名 |
plaOrderNo | 是 | Long | 平台订单号 |
返回data参数说明
参数名 | 类型 | 说明 |
---|---|---|
amount | string | 订单金额 |
channelCode | string | 支付渠道 |
merchantNo | string | 平台商户号 |
merchantOrderNo | string | 商户订单号 |
merchantPayTime | string | 商户交易时间 |
plaOrderNo | long | 平台订单号 |
plaStatusTime | string | 平台订单状态流转时间 |
status | string | 支付状态('PENDING','COMPLETED','FAILED') |