下游代理商 API 文档
一、认证与签名
1.1 获取凭证
联系平台管理员开通代理商账户,获取:
api_key:公开标识,每次请求必须携带api_secret:私钥,用于签名,创建时一次性返回,之后不可再查,请妥善保存
1.2 Sandbox 测试环境
平台提供 Sandbox 测试账号,用于开发联调。测试账号的所有接口返回模拟数据,不调用真实上游、不消耗真实资金,接口格式与生产环境完全一致。
| 项目 | 值 |
|---|---|
| Base URL | https://mama.best/api/v1(与生产相同) |
| API Key | sandbox_test_key_dd123456789abc |
| API Secret | sandbox_test_secret_dd_abcdef1234567890abcdef1234567890abcdef123456 |
| 初始余额 | $1000(模拟) |
| 文档地址 | https://doc.mama.best |
Sandbox 特征:
- 卡 BIN 以
SB开头(如SB000001) - virtualCardId 以
SB开头(如SB260319...) - 持卡人 ID 以
SB_HOLDER_开头 - 余额正常扣减/退回,验证资金流转逻辑
- 无需真实身份证信息,持卡人随意填写即可通过
Sandbox Webhook 测试
先告知平台配置你的 webhookUrl,然后调用以下接口,平台会主动向你的地址推送一条模拟事件:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| notifyType | string | 否 | 事件类型,默认 CARD_SETTLEMENT。可选:CARD_AUTH / CARD_AUTH_CANCEL / CARD_SETTLEMENT / CARD_REFUND / CARD_CANCEL / CARD_FREEZE / CARD_UNFREEZE |
| virtualCardId | string | 否 | 卡片 ID,不传则用随机 mock ID |
| externalRef | string | 否 | 用户标识,默认 sandbox_user_001 |
响应中 data.httpStatus 是你的 Webhook 端点返回的 HTTP 状态码,data.response 是你端点的响应体,可用于确认接收是否正常。
⚠️ 此接口仅 Sandbox 账号可调用,生产账号调用返回 4003 错误。
1.3 请求规范
- 请求方式:
HTTPS - 数据格式:
application/json - 字符编码:
UTF-8
1.4 请求头(所有接口必填)
| Header | 说明 | 示例 |
|---|---|---|
| X-Api-Key | 代理商 API Key | a1b2c3d4e5f6... |
| X-Timestamp | 当前毫秒时间戳(13位) | 1710641234567 |
| X-Signature | HMAC-SHA256 签名(见下方) | 3a7f9c2b... |
| Content-Type | 固定值 | application/json |
1.3 签名算法
| 组件 | 说明 | 示例 |
|---|---|---|
| METHOD | 大写 HTTP 方法 | GET、POST |
| PATH | pathname,不含域名,不含 Query String | /api/v1/balance |
| TIMESTAMP | 与 X-Timestamp 相同的毫秒时间戳字符串 | 1710641234567 |
| BODY | 原始请求体字符串(GET 请求时为空字符串 "") | {"virtualCardId":"VC..."} |
例如请求
/api/v1/cards?page=1,签名时使用 /api/v1/cards。POST body 签名使用
JSON.stringify 的原始字符串,不能格式化(不能有多余空格/换行)。时间戳有效期
时间戳必须在当前时间 ±5 分钟 内,超时返回 401。服务端使用 timing-safe 比较防止时序攻击。
1.4 签名示例(Node.js)
const crypto = require('crypto');
function sign(secret, method, path, timestamp, body = '') {
const pathname = path.split('?')[0]; // 去掉 query string
const payload = `${method.toUpperCase()}${pathname}${timestamp}${body}`;
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
const secret = 'your_api_secret';
const timestamp = Date.now().toString();
// GET 请求示例
const signature = sign(secret, 'GET', '/api/v1/balance', timestamp, '');
const headers = {
'X-Api-Key': 'your_api_key',
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json',
};
// POST 请求示例
const body = JSON.stringify({ virtualCardId: 'VC2603172099197' });
const postSig = sign(secret, 'POST', '/api/v1/cards/info', timestamp, body);
1.5 签名示例(Python)
import hmac, hashlib, time, json, requests
def sign(secret, method, path, timestamp, body=''):
pathname = path.split('?')[0]
payload = f"{method.upper()}{pathname}{timestamp}{body}"
return hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
API_KEY = 'your_api_key'
API_SECRET = 'your_api_secret'
BASE = 'https://mama.best'
# GET 示例
ts = str(int(time.time() * 1000))
sig = sign(API_SECRET, 'GET', '/api/v1/balance', ts)
resp = requests.get(f'{BASE}/api/v1/balance', headers={
'X-Api-Key': API_KEY, 'X-Timestamp': ts, 'X-Signature': sig,
'Content-Type': 'application/json',
})
# POST 示例(注意 separators 不能有多余空格)
body = json.dumps({"virtualCardId": "VC2603..."}, separators=(',', ':'))
ts = str(int(time.time() * 1000))
sig = sign(API_SECRET, 'POST', '/api/v1/cards/info', ts, body)
resp = requests.post(f'{BASE}/api/v1/cards/info', data=body, headers={
'X-Api-Key': API_KEY, 'X-Timestamp': ts, 'X-Signature': sig,
'Content-Type': 'application/json',
})
二、通用响应格式
{
"code": 0,
"data": { ... },
"message": "success"
}
| code | 说明 |
|---|---|
| 0 | 成功 |
| 401 | 认证失败(签名无效 / API Key 无效 / 时间戳过期 / 账号已禁用) |
| 4001 | 余额不足 / 业务限制 |
| 4003 | 参数错误 / 无权限 |
| 4004 | 资源不存在 |
| 4009 | 重复请求(幂等冲突) |
| 5000 | 服务器内部错误 |
| 5001 | 上游渠道调用失败 |
三、接口总览
四、externalRef 用户标识
externalRef 是代理商自定义的用户标识,用于将平台资源(卡片/持卡人/交易)与你自己系统中的用户关联。格式:任意字符串,最长 128 字符,建议使用你系统中的用户 ID,如 user_123456。
| 接口 | 参数位置 | 说明 |
|---|---|---|
| POST /cardholders | body | 创建持卡人时绑定 |
| GET /cardholders?externalRef=xxx | query | 按用户筛选持卡人 |
| POST /cards/apply | body | 开卡时绑定到卡片 |
| GET /cards?externalRef=xxx | query | 按用户筛选卡片 |
| POST /cards/transactions | body | 查该用户所有卡的交易 |
| Webhook 回调 | payload | 自动携带原始 externalRef |
五、接口详情
GET 查询余额
无请求参数。
{
"code": 0,
"data": {
"balance": "500.00",
"frozenBalance": "10.00",
"currency": "USD"
}
}
| 字段 | 说明 |
|---|---|
| balance | 可用余额(已扣除冻结) |
| frozenBalance | 提现冻结中的金额 |
| currency | 固定 USD |
POST 创建持卡人
持卡人是虚拟卡的归属人,开卡时必须指定 cardholderId。一个持卡人可开多张卡。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| firstName | string | ✅ | 名(英文/拼音,如 San) |
| lastName | string | ✅ | 姓(英文/拼音,如 Zhang) |
| birthday | string | ✅ | 生日,格式 YYYY-MM-DD |
| gender | string | ✅ | M=男 / F=女 |
| idType | string | ✅ | 证件类型(推荐 ID_CARD) |
| idNumber | string | ✅ | 证件号码(身份证需 18 位真实号码) |
| phoneAreaCode | string | ✅ | 手机区号,如 86(不含+号) |
| phone | string | ✅ | 手机号 |
| province | string | ✅ | 省份(中文,如 广东省) |
| city | string | ✅ | 城市(中文,如 深圳市) |
| address | string | ✅ | 详细地址 |
| string | 否 | 邮箱(若提供,全局唯一) | |
| country | string | 否 | 国家代码(默认 CN) |
| postalCode | string | 否 | 邮政编码 |
| remarks | string | 否 | 备注 |
| externalRef | string | 否 | 自定义用户标识,最长 128 字符 |
idType 枚举
| idType | 说明 |
|---|---|
| ID_CARD | 中国身份证(推荐) |
| PASSPORT | 护照 |
| DRIVING_LICENSE | 驾照 |
• 身份证号必须是真实存在的号码(区划码、生日与号码一致,有公安库比对)
•
province / city 必须填中文(如 福建省 / 泉州市),填英文会导致审核失败•
birthday 必须与身份证中间 8 位一致•
email 若提供则全局唯一,同一邮箱不能注册两个持卡人
请求示例
{
"firstName": "San",
"lastName": "Zhang",
"birthday": "1990-01-15",
"gender": "M",
"idType": "ID_CARD",
"idNumber": "350582199001150011",
"phoneAreaCode": "86",
"phone": "13800138000",
"province": "福建省",
"city": "泉州市",
"address": "丰泽区东海街道东海大街1号",
"country": "CN",
"postalCode": "362000",
"email": "[email protected]",
"externalRef": "user_123456"
}
响应示例
{
"code": 0,
"data": {
"cardholderId": "2029856542649827330",
"localId": "5",
"firstName": "San",
"lastName": "Zhang",
"externalRef": "user_123456",
"status": "1"
}
}
cardholderId 是开卡时必传的持卡人 ID,请妥善保存。GET 持卡人列表
| Query 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | number | 否 | 页码,默认 1 |
| pageSize | number | 否 | 每页数量,默认 20,最大 100 |
| externalRef | string | 否 | 按用户标识筛选 |
{
"code": 0,
"data": {
"list": [
{
"cardholderId": "2029856542649827330",
"localId": "5",
"firstName": "San",
"lastName": "Zhang",
"country": "CN",
"idType": "ID_CARD",
"idNumber": "110***011",
"externalRef": "user_123456",
"createdAt": "2026-03-17T06:00:00.000Z"
}
],
"total": 1
}
}
注:idNumber 返回脱敏值(前3位 + *** + 后3位)
GET 查询可用卡 BIN
获取当前平台支持的所有卡 BIN 列表,开卡时 cardBin 字段从此处取值。建议每次开卡前动态获取,不要写死。
无需请求参数。
响应示例
{
"code": 0,
"data": [
{
"binCode": "48331700",
"businessScene": "AD_DELIVERY",
"area": "US",
"currency": "USD",
"cardType": "VISA"
},
{
"binCode": "55616701",
"businessScene": "AD_DELIVERY",
"area": "US",
"currency": "USD",
"cardType": "MASTERCARD"
}
]
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| binCode | string | 卡 BIN 段,开卡时填入 cardBin |
| businessScene | string | 使用场景,如 AD_DELIVERY(广告投放) |
| area | string | 卡片归属地区,如 US |
| currency | string | 卡片结算币种,如 USD |
| cardType | string | 卡组织,如 VISA、MASTERCARD |
POST 开卡
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| outOrderId | string | ✅ | 幂等键,相同值不会重复开卡 |
| cardBin | string | ✅ | 卡 BIN 段,可通过 GET /card-bins 接口获取可用列表 |
| cardAmt | number | ✅ | 开卡充值金额(USD),必须 > 0 |
| cardholderId | string | ✅ | 持卡人 ID(创建持卡人返回的 cardholderId) |
| externalRef | string | 否 | 自定义用户标识 |
walletNo,平台自动使用主钱包。请求示例
{
"outOrderId": "order-20260317-uuid-001",
"cardBin": "556371",
"cardAmt": 100,
"cardholderId": "2029856542649827330",
"externalRef": "user_123456"
}
响应示例
{
"code": 0,
"data": {
"virtualCardId": "VC2603172099197",
"status": "0",
"outOrderId": "order-20260317-uuid-001",
"cardNoMasked": "****"
}
}
virtualCardId 立即返回,但卡片激活(status=3)需等待平台回调(通常几秒~几分钟)。激活后 Webhook 推送
CARD_APPLY 事件,届时可通过 cards/info 查询完整卡号和 CVV。
费用说明
从余额扣 cardAmt(充值到卡片)+ fee_apply(开卡固定费,管理员配置)。开卡失败 / cardholderId 无权限 → 自动全额退款。
GET 卡片列表
| Query 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | number | 否 | 页码,默认 1 |
| pageSize | number | 否 | 每页数量,默认 20,最大 100 |
| externalRef | string | 否 | 按用户标识筛选 |
{
"code": 0,
"data": {
"list": [
{
"virtualCardId": "VC2603172099197",
"cardNoMasked": "5561****7969",
"cardBin": "55616701",
"status": 3,
"cardBalance": "50.00",
"totalLimit": 100,
"expiryDate": "03/27",
"currency": "USD",
"externalRef": "user_123456",
"createdAt": "2026-03-17T08:00:00.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 20
}
}
卡片状态枚举
| status | 说明 |
|---|---|
| 0 | 申请中 |
| 1 | 申请失败 |
| 2 | 待激活 |
| 3 | 正常使用 |
| 4 | 已冻结 |
| 5 | 已注销 |
POST 查卡详情
实时查询卡片数据,包含完整卡号、CVV、实时余额。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| virtualCardId | string | ✅ | 卡片 ID(VC...) |
{
"code": 0,
"data": {
"virtualCardId": "VC2603172099197",
"cardNo": "5561670158047969",
"cardBin": "55616701",
"cvv": "446",
"expiry": "03/27",
"status": "3",
"cardAmt": 100,
"totalLimit": 100,
"usedLimit": 20,
"availableLimit": 80,
"ccy": "USD"
}
}
availableLimit,而非 cardAmt。POST 查卡号/CVV
每次调用都会记录审计日志(含调用 IP 和时间),适合前端展示卡号/CVV 的场景。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| virtualCardId | string | ✅ | 卡片 ID |
{
"code": 0,
"data": {
"cardNo": "5561670158047969",
"cvv": "446",
"expiry": "03/27"
}
}
POST 卡片交易记录
支持两种查询模式:
模式一:按卡片查(单卡)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| virtualCardId | string | ✅ 二选一 | 查单张卡的交易记录 |
| page | number | 否 | 页码,默认 1 |
| pageSize | number | 否 | 每页数量,默认 20,最大 100 |
模式二:按用户查(该用户所有卡)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| externalRef | string | ✅ 二选一 | 查该用户下所有卡的交易 |
| page | number | 否 | 页码,默认 1 |
| pageSize | number | 否 | 每页数量,默认 20,最大 100 |
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"cardId": 5,
"notifyType": "CARD_SETTLEMENT",
"payOrderId": "CT2026...",
"amount": "20.00",
"currency": "USD",
"merchantName": "OPENAI *CHATGPT SUBSCR",
"transactionTime": "2026-03-17T10:00:00.000Z",
"status": 1,
"createdAt": "2026-03-17T10:00:05.000Z"
}
],
"total": 4,
"page": 1,
"pageSize": 20
}
}
POST 提额(向卡片充值)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| virtualCardId | string | ✅ | 卡片 ID |
| amount | number | ✅ | 提额金额(USD),必须 > 0 |
{
"code": 0,
"data": {
"message": "提额成功",
"virtualCardId": "VC2603172099197",
"amount": 50
}
}
从代理商可用余额扣 amount,充值到指定卡片。失败自动退款。不额外收手续费。
POST 降额(从卡片回收资金)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| virtualCardId | string | ✅ | 卡片 ID |
| amount | number | ✅ | 降额金额(USD),必须 > 0 |
{
"code": 0,
"data": {
"message": "降额成功",
"virtualCardId": "VC2603172099197",
"amount": 30
}
}
从卡片取回 amount,退回代理商可用余额。降额金额不能超过卡片当前 availableLimit。不额外收手续费。
POST 申请提现
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| amount | number | ✅ | 提现金额(USD),最小 $10 |
| receiveAddress | string | ✅ | USDT TRC20 收款地址(T 开头,34位 Base58) |
{
"code": 0,
"data": {
"id": 1,
"requestNo": "AW1710641234567ABCD",
"amount": "200.000000",
"feeAmount": "2.000000",
"actualAmount": "198.000000",
"receiveAddress": "TXxxx...",
"status": 0,
"createdAt": "2026-03-17T05:30:00.000Z"
}
}
提现状态枚举
| status | 说明 |
|---|---|
| 0 | 待审核 |
| 1 | 已通过 |
| 2 | 处理中(出金中) |
| 3 | 已完成 |
| 4 | 已拒绝(余额已原路退回) |
GET 提现记录
返回最近 20 条记录,按创建时间倒序。
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"requestNo": "AW1710641234567ABCD",
"amount": "200.000000",
"feeAmount": "2.000000",
"actualAmount": "198.000000",
"receiveAddress": "TXxxx...",
"status": 3,
"txHash": "abc123...",
"createdAt": "2026-03-17T05:30:00.000Z"
}
],
"total": 1
}
}
GET 扣费流水
返回最近 20 条记录,按创建时间倒序。
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"feeType": "APPLY_CARD",
"feeAmount": "2.000000",
"balanceBefore": "502.000000",
"balanceAfter": "500.000000",
"relatedCardId": "VC2603...",
"remark": "开卡费 cardBin=556371",
"createdAt": "2026-03-17T05:00:00.000Z"
}
],
"total": 21
}
}
feeType 枚举
| feeType | 方向 | 说明 |
|---|---|---|
| APPLY_CARD | 扣 | 开卡固定费 |
| CARD_FUND | 扣 | 开卡/提额充值到卡 |
| CARD_RETURN | 退 | 降额退款回余额 |
| WITHDRAW | 扣 | 提现手续费 |
| REFUND | 退 | 开卡失败退款 |
六、资金流转说明
七、Webhook 回调
7.1 概述
当卡片发生消费、退款、状态变更等事件时,平台自动向你配置的 webhookUrl 发送 POST 回调。
- 已配置
webhookUrl(联系管理员设置) - 代理商状态为启用(status=1)
- 卡片
apiClientId已关联(仅转发代理商自己的卡片事件)
7.2 请求格式
| Header | 说明 |
|---|---|
| X-Signature | HMAC-SHA256(apiSecret, requestBody) hex 编码 |
| X-Timestamp | 毫秒时间戳 |
| Content-Type | application/json |
{
"notifyType": "CARD_SETTLEMENT",
"data": {
"virtualCardId": "VC2603172099197",
"payOrderId": "CT2026...",
"externalRef": "user_123456",
"clientId": "4",
"clientName": "你的代理商名称",
"settlementAmount": 20.00,
"settlementCurrency": "USD",
"consumptionAmount": 20.00,
"consumptionCurrency": "USD",
"transactionObject": "OPENAI *CHATGPT SUBSCR",
"transactionTime": "2026-03-17 10:00:00",
"status": "2"
}
}
7.3 验签(必须实现)
const crypto = require('crypto');
function verifyWebhook(apiSecret, rawBody, signature) {
// ⚠️ Webhook 签名只对 body 做 HMAC,不含 method/path/timestamp
const expected = crypto
.createHmac('sha256', apiSecret)
.update(rawBody) // 原始 body 字符串,不要 JSON.parse 后再 stringify
.digest('hex');
return expected === signature;
}
// Express 示例
// ⚠️ 重要:Webhook 路由必须在 express.json() 全局中间件之前注册!
// 全局 app.use(express.json()) 会提前消费 body stream,
// 导致 express.raw() 拿到空 body,签名验证永远失败。
app.post('/webhook/dd', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-signature'];
const raw = req.body.toString();
if (!verifyWebhook('your_api_secret', raw, sig)) {
return res.status(401).json({ code: 1, message: 'invalid signature' });
}
const payload = JSON.parse(raw);
console.log('收到回调:', payload.notifyType, payload.data);
res.json({ code: 0, message: 'success' });
});
code 必须为 0。失败时平台自动重试,最多 3 次(间隔 0s / 5s / 10s)。7.4 notifyType 枚举
| notifyType | 说明 | 建议处理 |
|---|---|---|
| CARD_APPLY | 开卡成功(卡片激活) | 更新卡片状态为可用,通知用户 |
| CARD_AUTH | 消费预授权(冻结额度) | 记录,可展示"处理中"消费 |
| CARD_AUTH_FAIL | 授权失败(余额不足/限额等) | 记录日志,通知用户 |
| CARD_AUTH_CANCEL | 授权撤销 | 释放之前冻结的额度 |
| CARD_SETTLEMENT | 消费结算(最终扣款) | 核心事件:更新交易为已完成 |
| CARD_SETTLE_SUPPLEMENT | 结算补扣(实际金额>预授权) | 额外扣差额 |
| CARD_SETTLE_REFUND | 结算退款(实际金额<预授权) | 退回差额 |
| CARD_REFUND | 商户退款 | 退回用户可用余额 |
| CARD_FREEZE | 卡被冻结(上游风控触发,非主动操作) | 更新卡片状态,通知用户 |
| CARD_UNFREEZE | 卡片解冻(上游风控解除) | 更新卡片状态,通知用户 |
| CARD_CANCEL | 卡片注销(上游强制) | 更新状态,余额已自动退回代理商 |
八、完整接入流程
九、注意事项
- 幂等性:
outOrderId相同的开卡请求只执行一次,重复请求返回原始结果;正在处理中返回 4009 - 金额精度:请求时可传 number 或 string;响应中金额为 string(6位小数精度),显示时建议保留 2 位
- 时间戳:精确到毫秒(13位),与服务器时差不超过 5 分钟
- 签名 body:使用
JSON.stringify的原始字符串,不要格式化或添加多余空格 - 签名路径:使用 pathname,不含 query string
- Webhook 验签:使用
express.raw()保留原始 body;Webhook 签名只对 body 做 HMAC,与 API 签名规则不同;若项目全局注册了express.json(),必须将 Webhook 路由放在它之前,否则 body 被提前消费导致签名永远失败 - api_secret 安全:仅在服务端使用,不要写入前端代码、Git 仓库或日志
- 卡号安全:
cards/info和cards/sensitive返回明文卡号/CVV,仅在你的服务端调用 - 金额上限:单次操作金额不超过 $1,000,000
- 提现地址:仅支持 TRC20 地址(T 开头,34位 Base58 字符)