# Cowpay api 说明
1. 目标读者
本文的主要目标读者是支付平台合作方的技术对接人员
2. 名词定义
* 合作方:指对接支付平台的机构
* 商户:指委托收单的营业机构,如超市,便利店等
# 数据交互与通讯安全协议
> 本章介绍支付平台对接交互模式一级通讯安全协议,描述组成支付平台的各个组件以及他们之间的协作方式,以及在通讯过程中保证数据的保密性和完整性。
## 1.通讯安全协议
### 1.1 通讯过程
1. 合作方请求报文上送时,采用HTTP/HTTPS协议,POST推送方式,编码格式统一为UTF-8。
2. 请求报文:
>统一请求报文格式:
{
"signtype": "MD5",
"sign": urlencode($sign, 'utf8'),
"transdata": urlencode($data, "utf8")
}
* signtype 为签名算法,默认为MD5算法
* sign 为业务请求签名后数据,并URLEncoder.encode(sign, “UTF-8”)
* transdata 为JSON格式的业务请求报文,并URLEncoder.encode(transdata, “UTF-8”)
3. 报文结构及样例:
> 所有请求报文统一采用JSON格
{
"transdata":"%7B%22pay_type%22%3A%2210072%22%2C%22user_no%22%3A%22Neo%22%2C%22product _name%22%3A%22pidai%22%2C%22product_code%22%3A%22product-123%22%2C%22order_no%22%3A%221507704879000%22%2C%22order_time%22%3A%222017-10-12T12%3A22%3A05.452Z%22%2C%22order_amount%22%3A0.1%2C%22payment%22%3A%22%E6%94%AF%E4%BB%98%E6%88%90%E5%8A%9F%22%7D",
"sign":"RLiujUr8AHm7V%2BNfPmdzkZgFuwiluyxJJNkso9nep3YY2wCO4lCh444Nk%2Fr1SxN2CxmpJ333DuaZfNPsBd647Q%2FYpH89fIYz3A07H7NE8EWN008FNBwDhBr6N3hyisJMNdwsJof7D3tCTtc28adOlC5k1naToseOP3x38H%2Fe5Vg%3D",
"signtype": "MD5"
}
## 2. 签名
>当前签名仅支持MD5方式,待签名字段(transdata是json格式,不用排序)无需排序。以transdata的json内容进行签名:将所有非空字段按ASCII码排序,以 key=value 形式用&(与符号)连接起来,最后拼上&key=${key},进行md5签名,并把签名转大写。先签名后urlencode
### 2.1
2.1 拼装必要参数
{
merchant_code: '1001',
pay_type: 'india-upi',
user_no: '1',
notify_url: 'http://www.cowpay.com',
return_url: 'http://www.cowpay.com/',
product_name: 'ttt001',
product_code: 'test001',
order_no: '541445444144414',
order_time: '20201102081725',
order_amount: 11.00,
merchant_ip: '127.0.0.1'
}
2.2 转成JSON
>将对象转成json字符串
2.4 拼装请求报文
{
"signtype": "MD5",
"sign": urlencode($sign, 'utf8'),
"transdata": urlencode($data, "utf8")
}
2.5 对transdata和sign进行urlencode
2.6 MD5签名源样例(代收)
```merchant_code=test1111¬ify_url=1&order_amount=50.00&order_no=541445444144414&order_time=20201102081725&pay_type=india-upi&product_code=test001&product_name=ttt001&return_url=1&user_no=51070173&key=F1C14F3DEFF6A38450BA97161A3DF3C```
2.7 MD5签名源串样例(代付)
```bank_branch=KKBK0000888&bank_card=624144124411xxxx&merchant_code=test1111¬ify_url=1&order_amount=3&order_no=541445444144414&&pay_type=india-bank-repay&user_name=Michael Taylor&key=F1C14F3DEFF6A38450BA97161A3DF3CF```
## 3. 请求接口
>POST Application/json 请求接口
## 4. 测试账号
>测试账号只能调试支付,调试完支付之后,请联系商务下正式号
测试账户请跟商户申请
# 接口参数符号约定
>在接口文档出现的具体报文格式描述中, “出现要求”列包含的值的含义如下表表示:
| 符号 | 请求方约束 |服务方约束|
| ------ | ------ |------|
| M | 必须包含该域 |必须校验该域是否存在和内容的合法性|
| C | 如果条件符合必须包含该域 |当条件满足时,必须校验该域是否存在 当该域存在时,必须检查其内容的合法性|
| O | 该域可选 |当该域存在时,必须检查其内容的合法性|
# 常见错误解决方案
## 1. 提示不是JSON类型,JSON解析出错
>原因:
1.1 报文格式错误,请查参考章节‘数据交互与通讯安全协议
1.2 头部字段未设置好Content-type:application/json
1.3 传递的不是JSON类型字符串
## 2. MD5加密提示验签失败
>原因
2.1 输入字段中间带有空格
2.2 Md5加密串是否正确
2.3 signtype的值是否为MD5
2.4 signtype字段拼写错误
3.3 加密串不是transdata 未urlencode前的数据
## 4. 提示商户号验证失败,请检查商户号和merchant_code是否填写正确
4.1 检查请求的host是否正确
4.2 向商务咨询是否开户
## 5. 提示交易功能未开通,请联系商务开通
>联系运营人员开户
## 6. 向商务咨询是否开户
6.1 确认运营人员是否配置好通道
6.2 确认输入金额是否传入限额内的金额或是固定金额指定的金额(如果是固额)
## 7. 提示通道维护中,请联系客服。
7.1 请联系运营开启通道
## 8. 接口返回 This library (validator.js) validates strings only
8.1 请检查除order_amount字段以外的值是否都为String类型
## 9. 获取支付地址后,打开无法获取支付页面
9.1 (yl,jd,zhifubao-h5,weixin)类型支付地址需要转成二维码,用相应的app扫码
9.2 咨询运维人员通道是否正常
## 10. 请先联系商务设置IP白名单
检查merchantcode是否填写正确
## 11 异步用什么加密方式
异步加密方式看下单(代付/支付)的加密方式,下单如果用MD5异步加来就是MD5,如果是RSA异步加来也是RSA
## 12. 请求方只需要返回Response status code 200即表示确认收到通知
此处不是指返回内容,而是指http状态码(如果完全不知道如何设置,麻烦直接响应一个空请求或者返回字符串200均可,默认情况下请求成功响应码即为200)
# 对接说明
* 支付对接文档:
* 网关服务器地址:`https://pay365.cowpay.co`
* 商户管理后台:`https://m138.cowpay.co`
* 支付下单地址:`/pay`
* 支付订单查询地址: `/queryPayOrder`
* 发起提现: `/v2/withdraw`
* 提现订单查询:`/v2/queryWithdrawOrder`
* 余额查询:`/v2/queryBalance`
* 接口请求中需要的账户信息 商户号(merchant_code)、密钥(MD5)、签名私钥(RSA) 使用后台网址登录,找到个人配置获取
* 回调IP:`平台获取`
* 接口地址 = 网关(host) + 接口路由(path 看文档对应接口 请求URL项目)
# 1.支付下单接口
>简要描述:
* 统一支付下单接口
> 请求URL:
• `${host}`/ pay
> 请求方式:
* POST
* application/ json
> 参数:
| 参数名 | 必选 | 类型 | 最大长度 | 中文描述 | 说明 |
| ------ | ------ |------|------|------|------|
| merchant_code | M |string|20|商户编号|平台分配的唯一编号|
| order_no | M |string|30|商户订单号|平商户订单号,不可重复,最长30位|
| order_amount | M |string||交易金额|单位:inr(只支持整数)|
| order_time | M |string|15|交易时间|时间戳,纯数字|
| product_name | M |string|60|产品名称|请尽量不要传固定值,否则会影响成功率;请尽量不要带空格。|
| notify_url | M |string|254|异步通知地址|异步回调通知地址,不支持参数传递|
| pay_type | M |string|30|支付类型|指定支付方式,详见 `[支付类型]`|
| return_url | C |string|30|成功回跳地址|提交成功后跳转的地址,非必填,但建议商户也传递该字段|
| payer_info | C |string|30|付款人姓名|付款人姓名|
>返回示例
```
{
"code":0,
"content":
{
"accountName":"Dhanendra prakash singh ",
"upi":"Dhanendra973@federal"
},
"html":"",
"msg":"操作成功",
"orderNo":"C1731191630813200384",
"payUrl":"https://mm.cowpay.cc/pay125605ef1927312c77995d4ac133696ee0fab741e8baa6ac554","qrcode":"image/gif;base64, /9j/4AAQS"
}
```
>返回参数说明
| 参数名 | 类型 | 说明 |
| ------ | ------ |------|
| code | int |请求结果,0代表成功,其他为失败|
| msg | string |失败原因|
| html | string |部分通道会以 html 字符串的形式输出,商户优先判断html字段是否存在,在则输出到页面中,页面会自动跳转,否则使用payUrl|
| orderNo | string |订单号|
| payUrl | string |支付收营台地址链接|
| qrcode | string |收款二维码|
| content | json |{"accountName":"Dhanendra","upi":"Dhanendra973@gmil.com"}|
| accountName | string |upi账号收款人名称|
| upi | string |upi账号|
# 2.支付订单查询
## 简要描述:
* 统一支付下单接口
## 请求URL:
• `${host}`/queryPayOrder
## 请求方式:
* POST
* application/ json
## 参数:
|参数名|必填|类型|长度|中文描述|说明
|------|------|------|------|------|------
|merchant_code|M|string|商户编号|平台分配的唯一编号
|order_no|M|sting|商户订单号|商户订单号
|signtype|M|sting||签名类型 `MD5` 或 `RSA`
|sign|M|sting||生成签名串`A7F0F0EB66562B3AC105F719E5E56960`
## 请求参数
```
{
"sign":"A7F0F0EB66562B3AC105F719E5E56960",
"signtype":"MD5","transdata":"%7B%22order_no%22%3A%22I0543064507662789%22%2C%22merchant_code%22%3A%221698133934504%22%7D"
}
```
## 查询成功返回数据
|参数名|类型|说明
| ------ |------|------
|order_no|String|订单号
|merchant_code|String|商户号
|sign|String|验证签名
|pay_type|String|支付类型
|payment|booloean|true:支付成功 false:支付失败
|order_time|long|Timestamp
|order_amount|String|订单金额
|status|booloean|true:订单存在 false:订单不存在
```
{
"order_no":"I0543064507662789",
"merchant_code":"1698133934504",
"order_amount":"500.00",
"sign":"0F39BB8C132071B96BBC1737AF0FFBC2",
"pay_type":"india-upi-h5",
"payment":true,
"order_time":1701585044000,
"status":true
}
```
## 查询失败返回数据
```
{"message":"查询不到该笔订单信息","status":false}
```
# 3.支付异步通知
>服务端发送的验签与支付验签相同
支付成功后会将以下字段通过JSON的方式POST到支付时填写参数地址,请求方只需要返回Response status code 200即表示确认收到通知,否则我们会每隔5分钟再次请求你们提交的异步接口,直达请求10次后,就不在发起请求
* POST字段如下:
* 异步回调报文示例:
```
无utr示例:
{
"transdata":"%7B%22pay_type%22%3A%2210072%22%2C%22user_no%22%3A%22Neo%22%2C%22product_name%22%3A%22pidai%22%2C%22product_code%22%3A%22product-123%22%2C%22order_no%22%3A%221507704879000%22%2C%22order_time%22%3A%222017-10-12T12%3A22%3A05.452Z%22%2C%22order_amount%22%3A0.1%2C%22payment%22%3A%22%E6%94%AF%E4%BB%98%E6%88%90%E5%8A%9F%22%7D",
"sign":"RLiujUr8AHm7V%2BNfPmdzkZgFuwiluyxJJNkso9nep3YY2wCO4lCh444Nk%2Fr1SxN2CxmpJ333DuaZfNPsBd647Q%2FYpH89fIYz3A07H7NE8EWN008FNBwDhBr6N3hyisJMNdwsJof7D3tCTtc28adOlC5k1naToseOP3x38H%2Fe5Vg%3D"
}
有utr示例:
{
"sign":"DA7046288CA8199B2ECC1D41D29834AE",
"transdata":"%7B%22order_no%22%3A%22I6060301291056389%22%2C%22order_time%22%3A1717655449000%2C%22product_code%22%3A%22%E5%95%86%E5%93%81Code%22%2C%22product_name%22%3A%22%E5%95%86%E5%93%81%E5%90%8D%22%2C%22user_no%22%3A%221%22%2C%22order_amount%22%3A%22100.000%22%2C%22utr_code%22%3A%2211111%22%2C%22pay_type%22%3A%22india-upi-h5%22%2C%22payment%22%3A%22%E6%94%AF%E4%BB%98%E6%88%90%E5%8A%9F%22%7D"
}
```
> 验签字段包含在”sign”字段,验签规则与发起支付相同,将transdata urldecode后获取到的JSON数据与私钥一起加密后urlencode。只有支付成功才会通知。请收到通知后校验签名和金额,并做好重复通知的处理。
> `transdata` 字段decode后为JSON字段如下:
|参数名|类型|说明
| ------ |------|------
|order_no|String|商户订单号|
|order_amount|String|订单金额(卢比为单位)
|order_time|String|系统订单创建时间戳(毫秒)
|pay_type|String|订单支付通道码
|product_name|String|支付时上传的商品名称
|product_code|String|支付时上传的商品CODE
|user_no|String|支付时上传的用户ID
|payment|String|支付成功,目前该字段值一定为支付成功四个字
|utr_code|String|交易utr,不为空时才返回,并参与签名;为空时不返回,不参与签名
# 4.代付下单
> 简要描述:
* 代付下单接口
* 后台代付需要绑定谷歌验证码,请联系商务
* API代付需要报备IP,请联系商务
## 请求URL:
* `${host}`/v2/withdraw
## 请求方式
* POST
* application/json
>参数
|参数名|必填|类型|长度|中文描述|说明
|------|------|------|------|------|------
|merchant_code|M|string|20|商户编号|平台分配的唯一编号
|order_no|M|string|30|商户订单号|商户订单号,不可重复
|order_amount|M|string|30|交易金额|单位:inr(100-50000,只支持整数)
|pay_type|M|string|100|代付类型|代付类型:`india-bank-repay`或`india-upi-repay`
|bank_name|C|string|100|银行名称|银行名称,不限制,随意填,upi代付不用填
|bank_card|M|string|32|银行卡号或upi账号|`e.g: 3339997788`,`xxx@sbi`
|bank_branch|OM|string|50|IFSC Code,upi代付不用填|`e.g: KKBK0000888`
|user_name|M|string|50|持卡人姓名|`e.g: Michael Taylor`
|notify_url|M|string|50|持异步通知地址|`e.g: https://callback.xxxxx/`
>返回示例
```
{ "status": true, "message": "代付申请提交成功" }
```
>返回参数说明
|参数名|类型|说明
|------|------|------
|status|boolean|请求结果,true代表代付发起成功,其他为失败
|message|string|失败原因
# 5.代付异步通知
## 服务端发送的验签与支付验签相同
>支付成功后会将以下字段通过JSON的方式POST到支付时填写参数地址,请求方只需要返回Response status code 200即表示确认收到通知,否则我们会每隔5分钟再次请求你们提交的异步接口,直达请求10次后,就不在发起请求验签字段包含在”sign”字段,验签规则与发起支付相同,将transdata urldecode后获取到的JSON数据与私钥一起加密后urlencode。
* `transdata` 字段decode后为JSON字段如下:
|参数名|类型|说明
|------|------|------
|order_no|boolean|商户订单号
|order_amount|string|订单金额(inr为单位)
|message|string|返回:提现成功,提现失败
|resp_code|string|以该字段判断代付结果:`S` 代付成功,`F` 代付失败,`P` 代付中
|utr_code|String|该字段有值时才返回,并参与签名;无值时不返回,不参与签名。
# 6.代付订单查询
## 简要描述
* 代付订单查询
## 请求URL
* `${host}`/v2/queryWithdrawOrder
## 请求方式:
* POST
* application/json
|参数名|必填|类型|长度|中文描述|说明
|------|------|------|------|------|------
|merchant_code|M|string|商户编号|平台分配的唯一编号
|order_no|M|sting|商户订单号|商户订单号
|signtype|M|sting||签名类型 `MD5` 或 `RSA`
|sign|M|sting||生成签名串`A7F0F0EB66562B3AC105F719E5E56960`
## 返回参数
|参数名|类型|说明
| ------ |------|------
|order_no|String|订单号
|merchant_code|String|商户号
|sign|String|验证签名
|order_amount|String|代付金额
|message|String|提示消息
|order_time|long|Timestamp
|status|booloean|true:订单存在 false:订单不存在
|resp_code|String|S:成功 F:失败 P:代付中
* 支付成功
```
{
"status": true,
"merchant_code": "test001",
"order_no": "15621201145410",
"message": "提现成功",
"order_time": "20201223181928",
"order_amount": "5.01",
"resp_code": "S"
}
```
* 订单不存在
```
{ "status": false, "message": "查询不到该笔订单信息"}
```
## 返回参数说明
|参数名|类型|说明
|------|------|------
|status|Boolean|查询状态:true 代表查询成功
## status为true时,会返回以下内容:
|参数名|类型|说明
|------|------|------
|merchant_code|string|商户编号
|order_no|string|商户订单号
|order_amount|string|订单金额(inr为单位)
|order_time|string|系统下单时间
|resp_code|string|查以该字段判断代付状态。`S` 代付成功,`F` 代付失败,`P` 代付中
|message|string|订单状态描述
|sign|string|urlencode后的加密字段。待加密内容为:除sign字段之外的其他字段的json
## status为false时,会返回以下内容:
|参数名|类型|说明
|------|------|------
|message|Boolean|查询请求失败时,此字段将会返回具体错误信息
# 7.查询余额
## 简要描述:
* 查询余额
## 请求URL:
* `${host}`/v2/queryBalance
## 请求方式:
* POST
* application/json
## 参数:
|参数名|必填|类型|长度|中文描述|说明
|------|------|------|------|------|------
|merchant_code|M|string|商户编号|平台分配的唯一编号
## 返回示例
* 成功返回
```
{
"status": true,
"balance": {"merchant_code": "test001001",
"total_order_amount": 1000.45,
"cashing_balance": 1000.23,
"total_withdraw_amount": 120.00,
"frozen_balance": 10.00,
"balance": 154.23,
"withdraw_balance": 2541.23,
"unsettled_balance": 0,
"freeze_balance": 0
}
}
```
* • 请求失败
```
{ "status": false, "message": "失败原因"}
```
## 返回参数说明
|参数名|类型|说明
|------|------|------
|status|Boolean|请求成功与否标志
|message|string|失败原因,status为false返回
|balance|json Object|余额明细,status为true返回
## balance参数 status为true返回
|参数名|类型|说明
|------|------|------
|merchant_code|string|平台分配的唯一编号
|total_order_amount|Number|交易总金额
|cashing_balance|Number|代付中金额
|total_withdraw_amount|Number|已代付总金额
|frozen_balance|Number|冻结中金额
|balance|Number|总余额
|withdraw_balance|Number|可提现余额
|unsettled_balance|Number|未结算余额
# 支付类型
> 目前支持的支付类型。注:平台当前仅开放 india-upi和india-upi-h5
|支付类型|中文描述
|------|------
|india-upi|upi扫码
|india-upi-h5|upi自动唤醒
|india-bank|印度银行卡代收
# 签名和验签示例:Java代码
```
package com.xxx;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class SignatureDemo {
static String md5Key = "xxxxxxxxx";
public static void main(String[] args) throws Exception {
//request json string
String request = signatureAndRequest();
//verify signature
decrypt(request);
}
private static String signatureAndRequest() throws UnsupportedEncodingException {
//pay params
Pay pay = new Pay();
pay.setMerchant_code("111");
pay.setPay_type("222");
pay.setUser_no("333");
pay.setNotify_url("444");
pay.setReturn_url("555");
pay.setProduct_name("666");
pay.setProduct_code("777");
pay.setOrder_no("888");
pay.setOrder_time("999");
pay.setOrder_amount("1");
pay.setMerchant_ip("2");
//params order by ascll
JSONObject jsonObject = (JSONObject) JSONObject.toJSON(pay);
String waitSignStr = mapToStringByASCll(jsonObject) + "&key=" + md5Key;
//signature
String sign = hash(waitSignStr).toUpperCase();
//request params
HashMap<String, String> requestMap = new HashMap<>();
requestMap.put("signtype", "MD5");
requestMap.put("sign", URLEncoder.encode(sign, "UTF-8"));
requestMap.put("transdata", URLEncoder.encode(jsonObject.toJSONString(), "UTF-8"));
String request = JSONObject.toJSONString(requestMap);
return request;
}
public static void decrypt(String json) throws UnsupportedEncodingException {
JSONObject receiveJson = JSONObject.parseObject(json);
String transdata = URLDecoder.decode(receiveJson.getString("transdata"), "UTF-8");
String waitSignStr = mapToStringByASCll(JSONObject.parseObject(transdata)) + "&key=" + md5Key;
if (hash(waitSignStr).toUpperCase().equals(URLDecoder.decode(receiveJson.getString("sign"), "UTF-8"))) {
System.out.println("signature verify success");
} else {
System.out.println("signature verify failed");
}
}
@Data
public static class Pay {
private String merchant_code;
private String pay_type;
private String user_no;
private String notify_url;
private String return_url;
private String product_name;
private String product_code;
private String order_no;
private String order_time;
private String order_amount;
private String merchant_ip;
}
public static String mapToStringByASCll(JSONObject object) {
Map<String, Object> map = (Map<String, Object>) object.toJavaObject(Map.class);
String[] sortedKeys = (String[]) map.keySet().toArray((Object[]) new String[0]);
Arrays.sort((Object[]) sortedKeys);
StringBuilder sb = new StringBuilder();
byte b;
int i;
String[] arrayOfString1;
for (i = (arrayOfString1 = sortedKeys).length, b = 0; b < i;) {
String key = arrayOfString1[b];
if (!"sign".equals(key)) {
if (map.get(key) != null && !StringUtils.isBlank(map.get(key).toString()))
sb.append(key).append("=").append(map.get(key).toString()).append("&");
}
b++;
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
private static byte[] md5(String s) {
try {
MessageDigest algorithm = MessageDigest.getInstance("MD5");
algorithm.reset();
algorithm.update(s.getBytes("UTF-8"));
byte[] messageDigest = algorithm.digest();
return messageDigest;
} catch (Exception e) {
return null;
}
}
private static final String toHex(byte[] hash) {
if (hash == null) {
return null;
}
StringBuffer buf = new StringBuffer(hash.length * 2);
for (int i = 0; i < hash.length; i++) {
if ((hash[i] & 0xFF) < 16) {
buf.append("0");
}
buf.append(Long.toString((hash[i] & 0xFF), 16));
}
return buf.toString();
}
public static String hash(String s) {
try {
return new String(toHex(md5(s)).getBytes("UTF-8"), "UTF-8");
} catch (Exception e) {
return s;
}
}
}
```