Qbit API 通知
本文档列出并描述了Qbit Notifications中使用的事件和数据模型。
WebHook
Qbit使用Webhooks来通知客户端以指定格式提供的回调URL。Qbit将根据下面进一步描述的场景发送不同的有效载荷,并且可以根据有效载荷主体中的businessType
字段来理解触发事件。
收到通知后,需要在5秒内返回一个应答报文。否则,Qbit认为通知失败,重复发送通知。
同一个通知可以发送多次,重复的通知必须正确处理。如果它已被处理,则直接将成功返回给Qbit。
客户端应该返回指定的返回码。如果发送回调URL后没有收到相应的返回码,则Qbit系统认为推送失败。返回字段如下:
字段 | 类型 | 描述 |
---|---|---|
received | boolean | 接收标识 |
示例:
{
"received": true
}
重试间隔
重试次数 | 间隔 | 重试次数 | 间隔 |
---|---|---|---|
1 | 10 秒 | 9 | 7 分钟 |
2 | 30 秒 | 10 | 8 分钟 |
3 | 1 分钟 | 11 | 9 分钟 |
4 | 2 分钟 | 12 | 10 分钟 |
5 | 3 分钟 | 13 | 20 分钟 |
6 | 4 分钟 | 14 | 30 分钟 |
7 | 5 分钟 | 15 | 1 小时 |
8 | 6 分钟 | 16 | 2 小时 |
注意事项
当前所有通知消息的实现都具有以下属性:
字段 | 类型 | 描述 |
---|---|---|
id | UUID | 通知标识 |
businessType | string | 通知的业务类型 |
sign | string | 签名 |
Account 通知
AccountRegistered
在创建子帐户时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "AccountRegistered",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 Account 对象.
KYC
在子帐户KYC状态发生变化时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "KYC",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 Account 对象.
FaceAuthentication
在子账户提交人脸认证时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "FaceAuthentication",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 FaceAuthentication 对象.
量子卡通知
CreateCard
创建量子卡时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "CreateCard",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 Card 对象.
CardStateChange
量子卡状态变更时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "CardStateChange",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 Card 对象.
CardTransaction
量子卡产生交易时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "CardTransaction",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 CardTransaction 对象.
FrozenAmount
量子卡冻结金额时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "FrozenAmount",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 CardTransaction 对象.
UnfrozenAmount
量子卡解冻金额时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "UnfrozenAmount",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 CardTransaction 对象.
BudgetTransaction
预算产生交易时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "BudgetTransaction",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 BudgetTransaction 对象.
Card3dsOtp
量子卡3DS验证,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "Card3dsOtp",
"data": {
"cardId": "7c9cde3a-12e6-4320-b11a-835d6b6e35db", // 量子卡 ID
"accountId": "b5d2fb72-b8bd-408b-ab95-91ef03a02bd6", // 账户 ID
"currency": "USD", // 交易币种
"amount": 100, // 交易金额
"cardNumber": "4931-93xx-xxxx-1234", // 卡号(前六后四)
"otp": "123456", // 交易的 3DS OTP
"detail": "Apple Pay" // 商户信息
},
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
ThreeDomainSecureForwarding
当您需要使用除默认短信之外的替代方法做出 3DS 决定时,会触发此 Webhook,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "ThreeDomainSecureForwarding",
"data": {
"cardId": "7c9cde3a-12e6-4320-b11a-835d6b6e35db", // 量子卡 ID
"accountId": "b5d2fb72-b8bd-408b-ab95-91ef03a02bd6", // 账户 ID
"actionId": "1824275858735009795",
"currency": "USD", // 交易币种
"amount": 100, // 交易金额
"cardNumber": "4931-93xx-xxxx-1234", // 卡号(前六后四)
"detail": "Apple Pay", // 商户信息
"timestamp": "1725350485682", // 消费时间-毫秒戳
"expirationTime": "1725350785682", // 过期时间-毫秒戳
"url": "https://提交3DS信息的URL"
},
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
Overspend
预算或量子卡超支时(每日0点推送),您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "Overspend",
"data": { "fileUrl":"文件URL(有效期7天)"},
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
CardBinStatus
卡bin维护/恢复时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "CardBinStatus",
"data": {
"status": "Operation", // {Maintenance-维护中, Operation-可操作}
"cardBins": ["433451","441112","489683"], // 卡bin列表
"time":"2024-03-05T03:39:08.000Z" // 维护/恢复时间
},
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
全球账户通知
CreateGlobalAccount
开通全球账户时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "CreateGlobalAccount",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 BankAccount 对象.
GlobalAccountTransaction
全球账户产生交易时,您应该看到如下所示的通知消息。
{
"id": "6a94b9c7-40d6-4007-a5d0-a96d714a1108",
"businessType": "CreateGlobalAccount",
"data": { ... },
"sign": "055144f3c59f2412d5575daee1ecc473b028378d3c3e359970fc96b33314e5bd"
}
data
有效载荷将是 GlobalAccountTransaction 对象.
签名
Qbit发起的每个请求都包含一个sign参数,可用于验证来自Qbit的请求的真实性。对于每个请求,data参数的数据都通过HMAC-SHA256哈希函数进行提取和处理。
- 待签名参数集合
const params = {
"id": "ee74c872-8173-4b67-81b1-5746e7d5ab88",
"accountId": null,
"holderId": "d2bd6ab3-3c28-4ac7-a7c4-b7eed5eee367",
"currency": "USD",
"settlementCurrency": null,
"counterparty": "SAILINGWOOD;;US;1800948598;;091000019",
"transactionAmount": 11,
"fee": 0,
"businessType": "Inbound",
"status": "Closed",
"transactionTime": "2021-11-22T07:34:10.997Z",
"transactionId": "124d3804-defa-4033-9f30-1d8b0468e506",
"clientTransactionId": null,
"createTime": "2021-11-22T07:34:10.997Z",
"appendFee": 0,
};
- 将待签名参数集合key依据“字符串首位字符的ASCII码”进行升序排列(排序过程中若出现ASCII码值相同的情况,则依次递增对下一位进行比较)
const keys = Object.keys(params);
keys.sort();
- 拼接字符串, 空值以空字符串填充
accountId=&appendFee=0&businessType=Inbound&clientTransactionId=&counterparty=SAILINGWOOD;;US;1800948598;;091000019&createTime=2021-11-22T07:34:10.997Z¤cy=USD&fee=0&holderId=d2bd6ab3-3c28-4ac7-a7c4-b7eed5eee367&id=ee74c872-8173-4b67-81b1-5746e7d5ab88&settlementCurrency=&status=Closed&transactionAmount=11&transactionId=124d3804-defa-4033-9f30-1d8b0468e506&transactionTime=2021-11-22T07:34:10.997Z
- CLIENT_SECRET用于对连接后的字符串进行hmac-sha256签名,采用十六进制编码方式获取签名。
const hmac = crypto.createHmac('sha256', '25d55ad283aa400af464c76d713c07ad');
const sign = hmac.update('Concatenates a string').digest('hex');
示例代码
const crypto = require('crypto');
function joinStr(params) {
const keys = Object.keys(params);
keys.sort();
const result = [];
for (const key of keys) {
let val = params[key];
if (val == null) {
val = '';
} else if (typeof val === 'object') {
if (!Array.isArray(val)) {
const fields = Object.keys(val);
fields.sort();
const res = {};
for (const field of fields) {
res[field] = val[field];
}
val = res;
}
val = JSON.stringify(val);
}
result.push(`${key}=${val}`);
}
return result.join('&');
}
const params = {
'createTime': '2023-05-31T07:29:46.784Z',
'budgetId': null,
'provider': 'PrepaidCard_493728',
'currency': 'USD',
'qbitCardNoLastFour': '1234',
'id': 'b9ce056b-c1f8-4f19-b014-d7be02a54598',
'status': 'Active',
'useType': '79f22263-a3fe-4347-8a40-2af6bf422839',
'label': 'ce08100b-fca8-4a13-bbfc-c381aeaec5d0',
'balanceId': 'ab43462f-93b3-4540-8601-11d759948ee7',
'cardAddress': {
'country': 'US',
'postalCode': '94402',
'addressLine2': '',
'addressLine1': '20 Barneson ave',
'state': 'California',
'city': 'San Mateo'
},
'accountId': '01eba490-5f9c-48a6-aa2d-7bcfdff0d720',
'token': '0ef85b24-866f-4c03-a7e8-459e3742642b',
'userName': 'test test'
};
const hmac = crypto.createHmac('sha256', '25d55ad283aa400af464c76d713c07ad');
const sign = hmac.update(joinStr(params)).digest('hex');
console.log(sign);
// => 178997e5960603afc573a28743d1680e3719a400e83936076f4dae4cb123a35a
package com.qbitnetwork.demo;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class Signature {
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
Formatter formatter = new Formatter(sb);
for (byte b : bytes) {
formatter.format("%02x", b);
}
return sb.toString();
}
public static byte[] sign(String str, String secret) throws NoSuchAlgorithmException, InvalidKeyException {
Key sk = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
Mac mac = Mac.getInstance(sk.getAlgorithm());
mac.init(sk);
return mac.doFinal(str.getBytes());
}
public static String joinStr(Map<String, Object> data) {
String[] keys = data.keySet().toArray(new String[0]);
Arrays.sort(keys);
StringBuilder sb = new StringBuilder();
for (String key : keys) {
Object val = data.get(key);
if (val == null) {
val = "";
}
if (val instanceof Map<?, ?>) {
val = JSON.toJSONString(new TreeMap<>((Map<?, ?>) val), JSONWriter.Feature.WriteMapNullValue);
}
if (val instanceof Collection<?> || val instanceof Object[]) {
val = JSON.toJSONString(val);
}
sb.append(key).append("=").append(val).append("&");
}
String str = sb.toString();
return str.substring(0, str.length() - 1);
}
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
Map<String, String> address = new HashMap<>();
address.put("addressLine1", "20 Barneson ave");
address.put("addressLine2", "");
address.put("city", "San Mateo");
address.put("country", "US");
address.put("postalCode", "94402");
address.put("state", "California");
Map<String, Object> data = new HashMap<>();
data.put("id", "b9ce056b-c1f8-4f19-b014-d7be02a54598");
data.put("accountId", "01eba490-5f9c-48a6-aa2d-7bcfdff0d720");
data.put("token", "0ef85b24-866f-4c03-a7e8-459e3742642b");
data.put("status", "Active");
data.put("currency", "USD");
data.put("provider", "PrepaidCard_493728");
data.put("userName", "test test");
data.put("createTime", "2023-05-31T07:29:46.784Z");
data.put("qbitCardNoLastFour", "1234");
data.put("label", "ce08100b-fca8-4a13-bbfc-c381aeaec5d0");
data.put("useType", "79f22263-a3fe-4347-8a40-2af6bf422839");
data.put("balanceId", "ab43462f-93b3-4540-8601-11d759948ee7");
data.put("budgetId", null);
data.put("cardAddress", address);
String signStr = bytesToHex(sign(joinStr(data), "25d55ad283aa400af464c76d713c07ad"));
System.out.println(signStr);
// => 178997e5960603afc573a28743d1680e3719a400e83936076f4dae4cb123a35a
}
}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
)
func getKeys(m map[string]any) []string {
j := 0
keys := make([]string, len(m))
for k := range m {
keys[j] = k
j++
}
return keys
}
func joinStr(params map[string]any) string {
keys := getKeys(params)
sort.Strings(keys)
var result []string
for _, key := range keys {
val := params[key]
if val == nil {
val = ""
}
t := reflect.TypeOf(val)
switch t.Kind() {
case reflect.Array, reflect.Map, reflect.Slice:
bytes, _ := json.Marshal(&val)
val = string(bytes)
break
default:
}
result = append(result, fmt.Sprintf("%s=%s", key, fmt.Sprintf("%v", val)))
}
return strings.Join(result, "&")
}
func sign(message string, secret string) string {
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
}
func main() {
params := map[string]any{
"createTime": "2023-05-31T07:29:46.784Z",
"budgetId": nil,
"provider": "PrepaidCard_493728",
"currency": "USD",
"qbitCardNoLastFour": "1234",
"id": "b9ce056b-c1f8-4f19-b014-d7be02a54598",
"status": "Active",
"useType": "79f22263-a3fe-4347-8a40-2af6bf422839",
"label": "ce08100b-fca8-4a13-bbfc-c381aeaec5d0",
"balanceId": "ab43462f-93b3-4540-8601-11d759948ee7",
"cardAddress": map[string]any{
"country": "US",
"postalCode": "94402",
"addressLine2": "",
"addressLine1": "20 Barneson ave",
"state": "California",
"city": "San Mateo",
},
"accountId": "01eba490-5f9c-48a6-aa2d-7bcfdff0d720",
"token": "0ef85b24-866f-4c03-a7e8-459e3742642b",
"userName": "test test",
}
fmt.Println(sign(joinStr(params), "25d55ad283aa400af464c76d713c07ad"))
// => 178997e5960603afc573a28743d1680e3719a400e83936076f4dae4cb123a35a
}
import json
import hashlib
import hmac
def sign(message, secret):
encryption = hmac.new(bytes(secret, encoding='UTF-8'), bytes(message, encoding='UTF-8'), hashlib.sha256)
return encryption.hexdigest()
def join_str(origin):
keys = list(origin.keys())
keys.sort()
content = []
for key in keys:
val = origin[key]
if val is None:
val = ''
if isinstance(val, dict) or isinstance(val, list):
val = json.dumps(val, sort_keys=True, ensure_ascii=False, separators=(',', ':'))
content.append(key + '=' + str(val))
return '&'.join(content)
if __name__ == '__main__':
data = {
'createTime': '2023-05-31T07:29:46.784Z',
'budgetId': None,
'provider': 'PrepaidCard_493728',
'currency': 'USD',
'qbitCardNoLastFour': '1234',
'id': 'b9ce056b-c1f8-4f19-b014-d7be02a54598',
'status': 'Active',
'useType': '79f22263-a3fe-4347-8a40-2af6bf422839',
'label': 'ce08100b-fca8-4a13-bbfc-c381aeaec5d0',
'balanceId': 'ab43462f-93b3-4540-8601-11d759948ee7',
'cardAddress': {
'country': 'US',
'postalCode': '94402',
'addressLine2': '',
'addressLine1': '20 Barneson ave',
'state': 'California',
'city': 'San Mateo'
},
'accountId': '01eba490-5f9c-48a6-aa2d-7bcfdff0d720',
'token': '0ef85b24-866f-4c03-a7e8-459e3742642b',
'userName': 'test test'
}
sign_str = sign(join_str(data), '25d55ad283aa400af464c76d713c07ad')
print(sign_str)
# => 178997e5960603afc573a28743d1680e3719a400e83936076f4dae4cb123a35a
function sign($message, $secret): string
{
return hash_hmac("sha256", $message, $secret);
}
function join_str($data): string
{
ksort($data);
$content = array();
foreach ($data as $key => $val) {
if (is_null($val)) {
$val = "";
}
if (is_array($val)) {
ksort($val);
$val = json_encode($val);
}
$content[] = "$key=$val";
}
echo '<br>';
echo join("&", $content);
echo '<br>';
return join("&", $content);
}
function test(): string
{
$params = array(
"createTime" => "2023-05-31T07:29:46.784Z",
"budgetId" => null,
"provider" => "PrepaidCard_493728",
"currency" => "USD",
"qbitCardNoLastFour" => "1234",
"id" => "b9ce056b-c1f8-4f19-b014-d7be02a54598",
"status" => "Active",
"useType" => "79f22263-a3fe-4347-8a40-2af6bf422839",
"label" => "ce08100b-fca8-4a13-bbfc-c381aeaec5d0",
"balanceId" => "ab43462f-93b3-4540-8601-11d759948ee7",
"cardAddress" => array(
"country" => "US",
"postalCode" => "94402",
"addressLine2" => "",
"addressLine1" => "20 Barneson ave",
"state" => "California",
"city" => "San Mateo"
),
"accountId" => "01eba490-5f9c-48a6-aa2d-7bcfdff0d720",
"token" => "0ef85b24-866f-4c03-a7e8-459e3742642b",
"userName" => "test test"
);
return sign(join_str($params), "25d55ad283aa400af464c76d713c07ad");
}
echo test();
// => 178997e5960603afc573a28743d1680e3719a400e83936076f4dae4cb123a35a
Updated about 1 month ago