Qbit API 通知

本文档列出并描述了Qbit Notifications中使用的事件和数据模型。

WebHook

Qbit使用Webhooks来通知客户端以指定格式提供的回调URL。Qbit将根据下面进一步描述的场景发送不同的有效载荷,并且可以根据有效载荷主体中的businessType字段来理解触发事件。

收到通知后,需要在5秒内返回一个应答报文。否则,Qbit认为通知失败,重复发送通知。

同一个通知可以发送多次,重复的通知必须正确处理。如果它已被处理,则直接将成功返回给Qbit。

客户端应该返回指定的返回码。如果发送回调URL后没有收到相应的返回码,则Qbit系统认为推送失败。返回字段如下:

字段类型描述
receivedboolean接收标识

示例:

👍

{

"received": true

}

重试间隔

重试次数间隔重试次数间隔
110 秒97 分钟
230 秒108 分钟
31 分钟119 分钟
42 分钟1210 分钟
53 分钟1320 分钟
64 分钟1430 分钟
75 分钟151 小时
86 分钟162 小时

注意事项

当前所有通知消息的实现都具有以下属性:

字段类型描述
idUUID通知标识
businessTypestring通知的业务类型
signstring签名

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哈希函数进行提取和处理。

  1. 待签名参数集合
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,
};
  1. 将待签名参数集合key依据“字符串首位字符的ASCII码”进行升序排列(排序过程中若出现ASCII码值相同的情况,则依次递增对下一位进行比较)
const keys = Object.keys(params);
keys.sort();
  1. 拼接字符串, 空值以空字符串填充
accountId=&appendFee=0&businessType=Inbound&clientTransactionId=&counterparty=SAILINGWOOD;;US;1800948598;;091000019&createTime=2021-11-22T07:34:10.997Z&currency=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
  1. 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