跳到主要内容
版本:v4

服务端开发指南

提示

服务通用规则请参考 请求规则和签算

主查服务

请求域名:

  • https://cloud-payment.tapapis.com
接口地址 Method描述
/order/v1/info?client_id={{client_id}}&order_id={{order_id}}GET查询订单信息
/order/v1/unconfirmed?client_id={{client_id}}GET查询未核销的订单列表
/order/v1/verify?client_id={{client_id}}POST核销订单

查询订单信息

服务 URL

  • https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}}

请求方式

  • GET

请求签算

通过订单 ID 查询订单详细信息和支付状态

curl -X GET \
-H 'X-Tap-Sign: {{signature}}' \
-H 'X-Tap-Ts: {{unix timestamp}}' \
-H 'X-Tap-Nonce: {{random nonce}}' \
https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}}

data.order 对象结构见 订单信息

{
"data": {
"order": {}
},
"success": true
}

查询未核销的订单列表

服务 URL

  • https://{{domain}}/order/v1/unconfirmed?client_id={{client_id}}

请求方式

  • GET

请求签算

查询当前未核销的订单列表,正常情况下在用户支付成功后,应通过 verify 接口核销订单并同时保证对用户发货成功。如果因异常原因没有完成核销,可以通过此接口查询,重新 verify 并完成发货。

curl -X GET \
-H 'X-Tap-Sign: {{signature}}' \
-H 'X-Tap-Ts: {{unix timestamp}}' \
-H 'X-Tap-Nonce: {{random nonce}}' \
https://{{domain}}/order/v1/unconfirmed?client_id={{client_id}}

data.list 数组内对象结构见 订单信息

{
"data": {
"list": [
{}
]
},
"success": true
}

核销订单

服务 URL

  • https://{{domain}}/order/v1/verify?client_id={{client_id}}

请求方式

  • POST[application/json; charset=utf-8]

请求正文信息

参数名称 必填 格式 描述
order_idYstring订单唯一 ID
purchase_tokenYstring用于订单核销的 token

请求签算

当支付成功后,核销订单表示已经确认支付结果并已对买家完成发货,订单状态也会从 charge.succeeded 变为 charge.confirmed

curl -X POST \
-H 'X-Tap-Sign: {{signature}}' \
-H 'X-Tap-Ts: {{unix timestamp}}' \
-H 'X-Tap-Nonce: {{random nonce}}' \
-H 'Content-Type: application/json; charset=utf-8'
-d '{"order_id":"{{order_id}}","purchase_token":"{{purchase_token}}"}'
https://{{domain}}/order/v1/verify?client_id={{client_id}}

data.order 对象结构见 订单信息

{
"data": {
"order": {}
},
"success": true
}

Webhook 回调

提示

同样的通知可能会多次发送,商户系统必须正确处理重复通知。

推荐做法是:当商户系统收到通知时,首先进行签名验证,然后检查对应业务数据的状态,如未处理,则进行处理;如已处理,则直接返回成功。

在处理业务数据时建议采用数据锁进行并发控制,以避免可能出现的数据异常。

Webhook 说明

目前 Webhook 支持监听「充值成功」「退款成功」「退款失败」事件。对于「充值成功」建议采取主动核销订单,根据订单状态完成发货。

  1. 需要依次进入 TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > API 密钥,检查是否有已生效的密钥,没有则需要 添加新的密钥
  2. 需要依次进入 TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > Webhooks 设置 > 添加,添加有效的 充值成功 URL。

Webhook 请求

服务 URL

  • 由开发者提供,在 Webhooks 设置中添加

请求方式

  • POST[application/json; charset=utf-8]

请求正文信息

参数名称 必填 格式 描述
orderYobject对象结构见 订单信息
event_typeYstring事件枚举见 Webhook的事件枚举

请求签算

curl -X POST \
-H 'X-Tap-Sign: {{signature}}' \
-H 'X-Tap-Ts: {{unix timestamp}}' \
-H 'X-Tap-Nonce: {{random nonce}}' \
-H 'Content-Type: application/json; charset=utf-8'
-d '{"order":{},"event_type":"charge.succeeded"}'
{{your webhook url}}

Webhook 响应

{
"code": "SUCCESS",
"msg": ""
}

字段描述

字段类型是否必须描述
codestringY状态码,SUCCESS 为接收成功,FAIL 或其他均认为失败
msgstringN当接收失败时需返回失败原因

请求规则和签算

请求 Headers

Header是否必须描述
X-Tap-SignY接口签算,详见 签算
X-Tap-TsY请求方当前时间 unix timestamp
X-Tap-NonceY随机数,需要大于等于 6 字节,小于等于 60 字节,每次请求需重新生成

请求 Request

保留参数

所有 HTTP METHOD 必传,需要作为查询参数的一部分

Key描述
client_id开发平台应用 ID
https://{{domain}}/order/v1/info?client_id={{client_id}}&order_id={{order_id}}

当请求方法是 POST

HTTP body 需使用 JSON 编码格式传输参数,即请求 headers 中应该携带 Content-Type: application/json; charset=utf-8

curl -X POST \
-H 'X-Tap-Sign: {{signature}}' \
-H 'X-Tap-Ts: {{unix timestamp}}' \
-H 'X-Tap-Nonce: {{random nonce}}' \
-H 'Content-Type: application/json; charset=utf-8'
-d '{"order_id":{{order_id}},"purchase_token":"{{purchase_token}}"}'
https://{{domain}}/order/v1/verify?client_id={{client_id}}

请求 Response

请求成功响应

{
"data": {},
"now": 1640966400,
"success": true
}

请求异常响应

{
"data": {
"code": 100004,
"msg": "NotFound: Unknown Error",
"error_description": "order not found"
},
"now": 1640966400,
"success": false
}

字段描述

字段类型是否必须描述
dataobjectY业务数据,或异常信息描述
nowintY服务端时间 (unix timestamp)
successboolY响应状态,true 为成功

异常响应 data 字段描述

字段类型是否必须描述
codeintY错误码
msgstringY通用错误描述
error_descriptionstringY详细错误描述,辅助理解和解决发生的错误

签算

签算采用 HMAC-SHA256 算法

密钥获取

  • 可在 TapTap 开发者中心 > 你的游戏 > 游戏服务 > TapPayment > 商品与订单 > API 密钥 查看。

签算说明

sign = HMAC.New(Sha256, "{{Server Secret}}").Hash(message)

以下为 message 组成

{method}\n
{url_path_and_query}\n
{headers}\n
{body}\n

method: 请求 HTTP 方法,如 GET、POST。

url_path_and_query: 完整请求路径及参数,如 /service/v1/method?client_id={{client_id}}&foo={{foo}}&bar={{bar}}&foo_bar={{foo_bar}}

headers: 所有 X-Tap- 前缀的 headers 组合,将 keys 按 ASCII-code order 排序后以换行符 \n 为分隔符拼接。如 {key1}:{value1}\n{key2}:{value2}\n{key3}:{value3}。为避免不同网络框架导致 keys 的排序结果不一致,签算时需要执行 key 转小写操作 key = tolower(key)

body: 请求体,如果请求体为空,则最后一行仅为一个 \n

以下为请求体为空时的 message 组成

{method}\n
{url_path_and_query}\n
{headers}\n
\n
提示

请求体 body,无需处理请求参数的顺序。建议用 String 接收 Webhook 请求的 RequestBody,验证请求签算后,再完成数据的反序列化

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.util.*;
import java.util.stream.Collectors;
public class SignatureExample {

public static String signRequest(String method, URI uri, String body, Map<String, List<String>> headers, String secret) throws Exception {
String urlPathAndQueryPart = uri.getRawPath() + (uri.getRawQuery() != null ? "?" + uri.getRawQuery() : "");
String headersPart = getHeadersPart(headers);

// 包含请求体的签名字符串部分
String signParts = method.toUpperCase() + "\n" + urlPathAndQueryPart + "\n" + headersPart + "\n" + body + "\n";

System.out.println("Sign Parts:\n" + signParts);

Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
sha256_HMAC.init(secretKey);

byte[] hash = sha256_HMAC.doFinal(signParts.getBytes());
return Base64.getEncoder().encodeToString(hash);
}

private static String getHeadersPart(Map<String, List<String>> headers) throws Exception {
TreeMap<String, String> sortedHeaders = new TreeMap<>();
headers.forEach((key, value) -> {
String lowerKey = key.toLowerCase();
if (lowerKey.equals("x-tap-sign")) {
return;
}
if (lowerKey.startsWith("x-tap-")) {
if (value.size() > 1) {
throw new RuntimeException("Invalid header, " + lowerKey + " has multiple values");
}
sortedHeaders.put(lowerKey, value.get(0));
}
});

return sortedHeaders.entrySet().stream()
.map(entry -> entry.getKey() + ":" + entry.getValue())
.collect(Collectors.joining("\n"));
}

public static void main(String[] args) {
try {
String secret = "VRy8aS2xbwImQUwtxc6vs4v51DaJWdlO";
String body = "{\"event_type\":\"charge.succeeded\",\"order\":{\"order_id\":\"1790288650833465345\",\"purchase_token\":\"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=\",\"client_id\":\"o6nD4iNavjQj75zPQk\",\"open_id\":\"4+Axcl2RFgXbt6MZwdh++w==\",\"user_region\":\"US\",\"goods_open_id\":\"com.goods.open_id\",\"goods_name\":\"TestGoodsName\",\"status\":\"charge.succeeded\",\"amount\":\"19000000000\",\"currency\":\"USD\",\"create_time\":\"1716168000\",\"pay_time\":\"1716168000\",\"extra\":\"1111111111111111111\"}}";
URI uri = new URI("https://example.com/my-service/v1/my-method");

HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(uri)
.header("Content-Type", "Content-Type: application/json; charset=utf-8")
.header("X-Tap-Ts", "1716168000")
.header("X-Tap-Nonce", "V7v7zJ");

// Considering body to be added for a POST request
HttpRequest request = requestBuilder
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();

String method = "POST"; // Since we are using the POST method in this example
String signature = signRequest(method, uri, body, request.headers().map(), secret);
System.out.println("Signature: " + signature);
} catch (Exception e) {
e.printStackTrace();
}
}
}

上述示例签算结果

# 参与签算部分
POST\n
/my-service/v1/my-method\n
x-tap-nonce:V7v7zJ\n
x-tap-ts:1716168000\n
{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}\n

# 签算结果 (X-Tap-Sign)
PyKQzlI65e0I9noVxcQc7FPU3nEyEFHKfRde65F6vhI=

通用对象结构描述

订单信息

参数类型是否必须描述
order_idstringY订单唯一 ID
purchase_tokenstringY用于订单核销的 token
client_idstringY应用的 Client ID
open_idstringY用户的开放平台 ID
user_regionstringY用户地区
goods_open_idstringY商品唯一 ID
goods_namestringY商品名称
statusstringY订单状态
amountstringY金额(本币金额 x 1,000,000)
currencystringY币种
create_timestringY创建时间
pay_timestringY支付时间
extrastringY商户自定义数据,如角色信息等,长度不超过 255 UTF-8 字符

订单状态

订单状态描述
charge.pending待支付
charge.succeeded支付成功
charge.confirmed已核销
charge.overdue支付超时关闭
refund.pending退款中
refund.succeeded退款成功
refund.failed退款失败
refund.rejected退款被拒绝

Webhook的事件枚举

event_type描述
charge.succeeded充值成功
refund.succeeded退款成功
refund.failed退款失败

错误码

code描述
-1非法请求
100000支付服务异常
100004订单不存在
100018订单验证错误