本页内容
集成指南
开发者文档
创建账单,将客户引导至付款页面,付款完成后接收已签名的 webhook。请从以下四个步骤开始。
支持范围
网关接受 USDT 和 USDC。此表与 GET /v1/services 一致,核对日期为 2026年11月4日。构建支付方式选择器前,请先读取实时目录。
| 资产 | 网络 | 确认数 |
|---|---|---|
| USDT | Tron | 20 |
| USDT | Ethereum | 12 |
| USDT | Solana | 32 |
| USDT | Polygon | 8 |
| USDT | Arbitrum | 12 |
| USDT | TON | 5 |
| USDC | Ethereum | 12 |
| USDC | Solana | 32 |
| USDC | Polygon | 8 |
| USDC | Base | 12 |
| USDC | Arbitrum | 12 |
付款为托管模式:资金进入平台控制的地址。您的商户余额是应付给您的金额。独立的 Telegram 钱包为非托管模式。
限制
- 出款和退款尚未上线:
GET /v1/services将boundaries.livePayouts和boundaries.liveRefunds报告为false。 - 入账付款未经过 KYT 服务商筛查。
- 费用超过金额 20% 的账单会被拒绝,并返回
AMOUNT_BELOW_MINIMUM。 - 不提供合同约定的 SLA。
起始费率为 0.40%,另加网络费用。请申请费用报价以获取总额和最低金额;参见价格。
身份验证
服务器间调用使用 x-api-key。项目密钥仅限单个商户项目使用。请将其保存在您的服务器上。
测试密钥以 ncp_test_… 开头;正式密钥以 ncp_live_… 开头。较早签发的密钥以 ncp_sandbox_ 开头,仍可继续使用。
curl -sS "https://api.veilnebu.la/v1/balances" \
-H "x-api-key: $PROJECT_API_KEY"为写请求签名
GET 请求只需 x-api-key。每个使用项目密钥认证的 POST、PUT、PATCH 和 DELETE 请求还需要两个请求头,否则 API 会返回 401 Invalid API signature:
x-ncp-timestamp:ISO-8601 格式,与当前时间相差不超过 5 分钟。x-ncp-signature:hex(HMAC-SHA256(your API key, "<timestamp>.<METHOD>.<path incl. query>.<raw body>"))。
方法名使用大写。对实际发送的字节进行签名,包括路径中的查询字符串。没有请求体的请求对空字符串签名。下面的辅助工具需要您的项目密钥;shell 辅助脚本需要 Bash、curl 和 OpenSSL,Node.js 辅助脚本使用原生 fetch。
export API_BASE_URL="https://api.veilnebu.la"
export PROJECT_API_KEY="ncp_test_…"
ncp() {
local method="$1" path="$2" body="" ts sig
shift 2
if [ "$#" -gt 0 ]; then body="$1"; shift; fi
ts=$(date -u +%Y-%m-%dT%H:%M:%S.000Z) || return
sig=$(printf '%s.%s.%s.%s' "$ts" "$method" "$path" "$body" |
openssl dgst -sha256 -hmac "$PROJECT_API_KEY" -hex) || return
sig=${sig##* }
if [ -n "$body" ]; then
curl -sS --request "$method" "$API_BASE_URL$path" \
-H "x-api-key: $PROJECT_API_KEY" \
-H "x-ncp-timestamp: $ts" -H "x-ncp-signature: $sig" \
-H "content-type: application/json" --data-binary "$body" "$@"
else
curl -sS --request "$method" "$API_BASE_URL$path" \
-H "x-api-key: $PROJECT_API_KEY" \
-H "x-ncp-timestamp: $ts" -H "x-ncp-signature: $sig" "$@"
fi
}创建第一张账单
在控制台中创建项目,复制其 API 密钥,然后创建账单。响应中包含 checkoutUrl:将客户引导至该地址。
ncp POST /v1/payments '{
"amountMinor":"10000",
"currencyCode":"USD",
"settlementAssetCode":"USDT",
"externalId":"order-1001"
}' -H "idempotency-key: order-1001"响应节选。每张账单的 ID 都会单独生成。
{
"id": "cm3k9x2f40001qz8e7h5v1abc",
"status": "new",
"amountMinor": "10000",
"currencyCode": "USD",
"settlementAssetCode": "USDT",
"checkoutUrl": "https://app.veilnebu.la/checkout/cm3k9x2f40001qz8e7h5v1abc"
}amountMinor 是以该币种最小单位表示的整数字符串:10000 = 100.00 USD。十进制的 amount 字段仅被浏览器端接口 /v1/public/payments 接受。
浏览器端付款页面与允许的来源
在控制台中创建可公开密钥,并列出您店铺实际使用的来源。浏览器请求将其作为 x-publishable-key 发送到 POST /v1/public/payments。
Origin、successUrl 和 cancelUrl 都必须在该密钥的允许来源列表中。来源由协议、主机和可选端口组成,不含路径或通配符。缺失或未列出的来源会被拒绝。
十进制金额对法币最多接受两位小数,对加密货币最多接受该资产自身的小数位数(USDT 和 USDC:在我们目前支持的所有网络上均为 6 位)。
const response = await fetch('https://api.veilnebu.la/v1/public/payments', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-publishable-key': 'pk_test_…',
},
body: JSON.stringify({
amount: '100.00',
currencyCode: 'USD',
settlementAssetCode: 'USDT',
successUrl: 'https://shop.example/thanks',
cancelUrl: 'https://shop.example/cart',
}),
});
if (!response.ok) throw new Error(await response.text());
const invoice = await response.json();
window.location.assign(invoice.checkoutUrl);付款状态
| 状态 | 下一步操作 |
|---|---|
new | 分享付款链接 |
pending | 等待付款 |
confirming | 等待确认 |
paid | 可以结算 |
partially_paid | 请客户完成付款 |
overpaid | 核查多入账的金额 |
expired | 创建新账单 |
cancelled | 无需操作 |
failed | 查看详情 |
refunded | 无需操作 |
幂等键
创建账单时,请发送由订单 ID 生成的固定 Idempotency-Key。使用相同的键和请求体重试会返回原账单。用同一个键发送不同的请求体会返回 409。超时后重试时,请保持键和请求体不变。
请求示例请参见第一张账单示例。
错误
错误以 JSON 格式返回。message 是一个字符串;对于校验错误,则是字符串数组。
{
"statusCode": 401,
"error": "Unauthorized",
"message": "Invalid API key"
}| HTTP 状态码 | 含义与处理 |
|---|---|
400 | 字段无效;message 会指出具体字段。请先修正再重试。 |
401 | Invalid API key 或 Invalid API signature。请检查密钥、签名的字节和时间戳。 |
404 | Payment invoice not found。请检查 ID 和项目。 |
409 | 相同的 Idempotency-Key 使用了不同的请求体,或 Insufficient merchant payable balance。 |
429 | 请求过于频繁,已被限流。请稍候再重试。 |
5xx | 使用相同的 Idempotency-Key 和请求体重试。 |
Webhooks(HMAC-SHA256)
每次投递都带有 x-nebula-signature: t=<unix>,v1=<hex>。请使用创建端点时返回的端点密钥验证 HMAC。拒绝与当前时间相差超过 300 秒的时间戳。
在 Node.js 中验证
Node.js 验证器仅使用内置的 crypto 模块。将签名请求头、原始请求体字节和端点密钥传入即可。
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(header, rawBody, secret) {
const parts = {};
for (const piece of (header ?? '').split(',')) {
const separator = piece.indexOf('=');
if (separator <= 0) continue;
parts[piece.slice(0, separator).trim()] = piece.slice(separator + 1).trim();
}
if (!/^[0-9]{1,16}$/.test(parts.t ?? '')) return false;
if (!/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) return false;
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac('sha256', secret)
.update(parts.t + '.')
.update(rawBody)
.digest('hex');
if (expected.length !== parts.v1.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}请在解析 JSON 之前,基于原始请求体进行验证。投递至少发生一次:请按 x-nebula-event-id 去重,它与载荷中的 eventId 一致。自动重试保持投递 ID 不变;手动重新发送会生成新的投递 ID,但事件 ID 相同。请在返回 2xx 之前持久化该事件。
在本地测试 webhook
Webhook URL 必须使用公网主机。本地和私有地址会被拒绝。请通过公网 HTTPS 隧道暴露您的接收端,然后使用签名辅助工具注册其 URL。
ncp POST /v1/webhooks/endpoints '{
"url":"https://receiver.example/webhook",
"events":["payment.paid","payment.reorged"]
}'将 URL 替换为您的接收端地址。请保存创建时返回的端点密钥。
事件类型
出款和退款尚未上线(GET /v1/services 将 boundaries.livePayouts 报告为 false)。在此之前,出款和批量出款仅在您的余额中记录转账;payout.completed 和 refund.completed 并不表示资金已转出。
下方目录与 GET /v1/services 一致。目录中有某个条目,并不表示相应的资金流动已上线。
| 事件 | 含义 |
|---|---|
webhook. | 为某个 webhook 端点请求的测试投递。 |
payment. | 客户已选择支付方式,等待首笔入账。 |
payment. | 已检测到入账,正在等待所需的确认数。 |
payment. | 账单已全额付清,并已在账本中结算。 |
payment. | 收到的金额少于请求金额,等待剩余部分。 |
payment. | 收到的金额多于请求金额,已连同超付部分在账本中结算。 |
payment. | 账单在资金不足的情况下已过期。 |
payment. | 账单在付款前已取消。 |
payment. | 由于链上、费用策略或观测错误,付款失败。 |
payment. | 针对已入账存款的提示性风险事件。入账 KYT 筛查尚未上线。 |
payment. | 一笔已确认的存款因链重组而被移除;账单不再处于已付状态,请暂停或撤销履约。 |
payment. | 存入了不受支持的代币,未予入账。 |
payout. | 出款处理中状态。实际转账尚未开放。 |
payout. | 出款已记录在余额中;这并不表示资金已转出。 |
payout. | 出款失败。 |
payout. | 出款在完成前已取消。 |
refund. | 退款处理中状态。实际退款尚未开放。 |
refund. | 退款已记录在余额中;这并不表示资金已转出。 |
refund. | 退款失败。 |
internal_transfer. | 商户间余额转账已为发送方提交。 |
internal_transfer. | 商户间余额转账已计入接收方。 |
static_wallet. | 已为客户创建一个长期有效的充值地址。 |
static_wallet. | 某个静态钱包已被冻结。 |
static_wallet. | 某个静态钱包已解除冻结。 |
static_wallet. | 已检测到一笔静态钱包存款,并已在账本中结算。 |
dispute. | 针对某张账单发起了争议。 |
dispute. | 争议已解决。仅凭解决结果并不能证明已发生链上退款。 |
dispute. | 争议已被驳回;商户无需采取任何操作。 |
subscription. | 一个计费周期生成了新账单。 |
重试策略
如果您的端点未返回 2xx,我们总共会尝试 8 次:首次尝试,之后分别在 1 分钟、5 分钟、30 分钟、2 小时、6 小时、12 小时和 24 小时后重试。每次延迟从上一次失败的尝试之后开始计算。网络错误和超时也计为失败。测试投递只尝试一次。
最后一次失败后,该投递被标记为失败;可使用 POST /v1/webhooks/deliveries/{deliveryId}/resend 重新发送。作为备用方案,可通过 GET /v1/payments/{id} 进行核对。
出款和退款尚未上线(GET /v1/services 将 boundaries.livePayouts 报告为 false)。在此之前,出款和批量出款仅在您的余额中记录转账;payout.completed 和 refund.completed 并不表示资金已转出。
批量出款
批次用于对转账记录进行分组。其审批策略和所需审批数分别以 approvalPolicy 和 approvalsRequired 返回。存在多名审批人时,创建者不能审批自己的批次。唯一符合条件的操作员可以审批自己在会话中创建的批次。请在控制台中使用 WebAuthn 审批批次。
export PROJECT_API_KEY="ncp_test_…"
ncp POST /v1/payout-batches '{
"assetCode":"USDT",
"networkCode":"TRON",
"externalId":"batch-1001",
"items":[{
"destinationAddress":"T…",
"amountMinor":"15000000",
"externalRef":"recipient-1001"
}]
}' -H "idempotency-key: batch-1001"请将收款地址替换为您自己测试项目中的有效地址。
周期性计费
订阅会在每个计费周期创建一张新账单,并通过 subscription.invoice_created 通知。它不会自动从客户钱包扣款。请将新的付款页面 URL 发送给客户。
interval:DAY、WEEK、MONTH 或 YEAR。
ncp POST /v1/subscriptions '{
"customerRef":"customer-42",
"assetCode":"USDT",
"networkCode":"TRON",
"amountMinor":"9000000",
"interval":"MONTH",
"intervalCount":1
}'可使用API 参考中的生命周期操作来暂停、恢复或取消订阅。
风险筛查
出款和退款的收款地址会在创建转账前与公开的制裁名单进行比对;一旦匹配,请求将被拒绝。入账付款不经过筛查。
Telegram 商户机器人
@veilnebula_bot 一次只能关联一个项目。请在私聊中使用该项目的 API 密钥完成关联。机器人会尝试删除您的消息,并对存储的密钥进行加密。如果删除失败,请吊销并轮换该密钥。切勿在群聊中发布密钥。
通知仅限于已关联的项目和当前有效的密钥。如需审批出款,请打开控制台。
/link <API_KEY>
/balance
/invoices
/notify on
/unlink