创建支付订单
支付订单用于把你网站里的业务订单连接到 Taria Pay 支付流程。你的服务端创建 payment intent 后,会得到 paymentId 和 checkoutUrl。
- 使用 Hosted Checkout 时,前端跳转到
checkoutUrl。 - 使用 Checkout Button 时,前端按钮打开
checkoutUrl。 - 使用 Checkout Panel 时,前端把
paymentId传给<CheckoutPanel />。
请求由谁发起
必须由你的服务端发起。不要从浏览器直接调用 Taria Pay API,也不要把 API key 放到前端、移动端包或公开仓库里。
推荐字段
| 字段 | 是否必填 | 说明 |
|---|---|---|
orderId | 必填 | 你自己系统里的订单号,同一商户下必须唯一 |
amount | 必填 | 订单金额,建议使用字符串 |
currency | 必填 | USDC、USDT 或 EURC,主币种 |
acceptedCurrencies | 可选 | 结账页还可选的币种,例如 ["USDC","USDT"]。主币种会自动包含。不传则只显示 currency。 |
successUrl | 推荐 | 买家支付后返回你网站的地址。结账页拿不到这个地址:只有在支付进入处理中或已确认后,Taria Pay 才会由服务端跳转过去,并附带 payment_id、tx_hash、checkout_mode。这次跳转只发给付款前打开过结账页的浏览器;其他访问者只能看到已支付状态,没有返回链接 |
cancelUrl | 推荐 | 买家取消支付后返回你网站的地址(同样由服务端跳转) |
displayReference | 可选 | 买家在结账页看到的订单参考号(发票号、订单短号),最多 64 个可打印字符。你的 orderId 不会展示给买家;不传则结账页不显示订单参考号 |
publicMetadata | 可选 | 想展示给买家的键值对,最多 10 个(键仅限 A-Z a-z 0-9 _ . -,不超过 40 字符;值不超过 200 个可打印字符) |
customer | 可选 | 买家邮箱、姓名等信息 |
metadata | 可选 | 你的业务上下文,例如购物车 ID。只对你可见:会出现在 webhook、后台和 API 中,不会出现在结账页(展示用的 title、product_name、merchant_name、brand_name 除外) |
paymentMethods | 可选 | 控制结账页展示哪些付款方式,见下文 |
deferTransferDeposit | 可选 | REST API 布尔值,默认 false。跳转式收银台可设为 true,将收款地址准备延后至收银台加载,减少创建请求等待;创建响应可能不含 transferDepositAddress。固定客户地址的占用检查仍在创建时完成。 |
fixedTransferCustomerId | 可选 | 为已验证身份的老客户启用固定收款地址,见下文 |
expiresAt | 可选 | 订单过期时间,ISO-8601 格式。服务端没有上限也没有默认值:不传则订单不会自行过期。程序付款(见下文)请至少给一小时 |
可选请求头:Idempotency-Key(1–255 个可打印 ASCII 字符,按商户账号隔离)。用同一个 key 和同样的请求体重试创建,会返回原来的订单(HTTP 200,idempotentReplay: true)而不是再建一笔;同一个 key 配不同的请求体会被拒绝(HTTP 409,错误码 idempotency_key_reused)。不带该头时,重复的 orderId 仍返回原有的 409 冲突。
SDK 示例
const paymentIntent = await tariapay.paymentIntents.create({
orderId: "order_1001",
amount: "12.50",
currency: "USDC",
acceptedCurrencies: ["USDC", "USDT"],
successUrl: "https://merchant.example/success",
cancelUrl: "https://merchant.example/cancel",
customer: {
email: "buyer@example.com",
name: "Alice",
},
metadata: {
cartId: "cart_12",
},
});
return {
paymentId: paymentIntent.paymentId,
checkoutUrl: paymentIntent.checkoutUrl,
};REST 示例
curl -X POST "https://api.tariapay.com/v1/payment-intents" \
-H "Authorization: Bearer tpk_..." \
-H "Content-Type: application/json" \
-d '{
"orderId": "order_1001",
"amount": "12.50",
"currency": "USDC",
"acceptedCurrencies": ["USDC", "USDT"],
"successUrl": "https://merchant.example/success",
"cancelUrl": "https://merchant.example/cancel",
"customer": {
"email": "buyer@example.com",
"name": "Alice"
},
"metadata": {
"cartId": "cart_12"
}
}'控制结账页的付款方式
默认情况下,结账页同时提供钱包支付和直接转账(在支持的网络上),转账区默认收起。你可以在创建支付订单时用可选的 paymentMethods 字段调整:
{
"orderId": "order_1001",
"amount": "12.50",
"currency": "USDC",
"paymentMethods": {
"wallet": true,
"transfer": true,
"transferDefaultOpen": true
}
}| 字段 | 默认值 | 说明 |
|---|---|---|
wallet | true | 是否展示"连接钱包"付款模块 |
transfer | true | 是否提供直接转账;设为 false 时服务端也会拒绝该订单的转账请求 |
transferDefaultOpen | false | 转账区是否默认展开;wallet: false 时自动展开 |
规则与兜底:
wallet和transfer不能同时为false,否则返回 400。- 两个开关都在服务端强制,不只是隐藏界面:关掉的方式对应的接口会返回 403。
- 直接转账仍受网络支持范围约束。如果你设置了
wallet: false,但订单当前所在的网络不支持转账,结账页会自动回落展示钱包模块,并允许在该网络上用钱包付款,保证买家始终有可用的付款方式;买家切换到支持转账的网络后,钱包模块会重新隐藏。 - 不传该字段时行为与现在完全一致,已有接入无需任何改动。
老客户固定收款地址
默认情况下,每笔订单都会分配一个全新的、不复用的转账收款地址。对于已验证身份的老客户,你可以改为给每位客户一个固定收款地址:该客户的所有订单、所有受支持的网络都使用同一个地址。常从交易所付款的回头客可以把地址存入交易所地址簿,不必每次重新复制新地址。
创建订单时传入你自己系统里的客户 ID:
const paymentIntent = await tariapay.paymentIntents.create({
orderId: "order_1001",
amount: "12.50",
currency: "USDC",
fixedTransferCustomerId: "user_42", // 你的客户 ID,仅限已验证账号
})用 REST 时改为设置保留 metadata 键(SDK 字段做的就是这件事):
{
"orderId": "order_1001",
"amount": "12.50",
"currency": "USDC",
"metadata": {
"fixed_transfer_customer_id": "user_42"
}
}规则与责任:
- 只为你已验证身份的客户传入该字段——已登录且邮箱通过验证的账号、钱包签名登录,或等效的校验。买家会长期信任这个地址属于自己,因此绝不能用未经验证的输入(比如买家随手填的邮箱)推导资格。访客和未验证用户直接不传即可,他们继续使用每单一个的新地址。
- 地址按「你的商户账号 + 你传的客户 ID」隔离。请使用你系统里稳定、不含个人信息的 ID(用户 ID,而不是可能变更的邮箱)。
- 同一时刻只允许一笔未支付订单使用该地址。 客户已有一笔未支付订单占用固定地址时,用同一个
fixedTransferCustomerId再创建订单会返回409,错误码fixed_transfer_active_payment。此时应引导客户完成或取消上一笔订单,而不是重试——保存了orderId的话可以用retrieveByOrderId找回。 - 只有订单开着期间转入的资金才计入该订单。 每笔新订单都会先在地址上记录一个余额基准点,订单创建之前(或两笔订单之间)到账的资金不会自动计入后面的订单。请提示买家先下单再转账;如果资金转在了订单之外,请联系我们协助找回。
- 钱包支付不受影响:结账页照常提供两种付款方式,支付结果和 webhook 事件也完全一致。
- 固定地址只改变转账支付使用哪个收款地址,其余一切——确认、错链救援、webhook、转账退款流程——都与普通直接转账相同(见支持的付款方式)。
无收银台:用自己的钱包程序化付款
有些付款方不是人而是程序:API 客户用热钱包给预付余额充值、财务系统付一张发票、agent 结一笔账。他们不该被要求打开结账页。每个支付订单本来就带一个直接转账的收款地址,所以流程是:创建订单 → 读取结构化的付款指令 → 用自己的钱包按精确金额发 ERC-20 转账 → 轮询转账状态直到到账被检测到 → 以 webhook 为准完成履约。
1. 创建订单并读取 transferInstructions
照常创建订单(不要传 deferTransferDeposit)。expiresAt 请给得长一些——一小时以上:从交易所或热钱包转出要几分钟才到账,窗口还得容得下一次重试。带上 Idempotency-Key,超时的请求就能安全重试。
curl -X POST "https://api.tariapay.com/v1/payment-intents" \
-H "Authorization: Bearer tpk_..." \
-H "Idempotency-Key: topup_7f3a" \
-H "Content-Type: application/json" \
-d '{ "orderId": "topup_7f3a", "amount": "25", "currency": "USDC",
"acceptedCurrencies": ["USDC", "USDT"],
"expiresAt": "2026-10-18T18:00:00.000Z" }'响应(以及 GET /v1/payment-intents/:paymentId)在 paymentIntent 之外还带 transferDepositAddress 与 transferInstructions:
{
"transferDepositAddress": "0x1c73d3e5db612db118aa99c864f277a5743b152c",
"transferInstructions": {
"depositAddress": "0x1c73d3e5db612db118aa99c864f277a5743b152c",
"expiresAt": "2026-10-18T18:00:00.000Z",
"payable": true,
"chains": [
{
"chainId": 8453,
"chainName": "Base",
"tokens": [
{ "symbol": "USDC", "currency": "USDC", "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "decimals": 6, "amountMinor": "25000000", "amount": "25" },
{ "symbol": "USDT", "currency": "USDT", "tokenAddress": "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2", "decimals": 6, "amountMinor": "25000000", "amount": "25" }
]
},
{
"chainId": 56,
"chainName": "BNB Smart Chain",
"tokens": [
{ "symbol": "USDC", "currency": "USDC", "tokenAddress": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", "decimals": 18, "amountMinor": "25000000000000000000", "amount": "25" },
{ "symbol": "USDT", "currency": "USDT", "tokenAddress": "0x55d398326f99059ff775485246999027b3197955", "decimals": 18, "amountMinor": "25000000000000000000", "amount": "25" }
]
}
],
"rules": {
"exactAmount": true,
"singleTransfer": true,
"topUpAllowed": true,
"addressSingleUse": true,
"sameAddressOnEveryChain": true,
"nativeTokenNotAccepted": true,
"payerCoversNetworkFees": true,
"payBefore": "2026-10-18T18:00:00.000Z"
}
}
}把这些原样交给付款方,并让他们遵守 rules:
- 从列表里选一条链、一种币,发送精确的
amountMinor(按该币自己的decimals计的整数——同一种币在 Base 上是 6 位小数、在 BNB 智能链上是 18 位,所以绝不能跨链复用最小单位金额)。amount是同一数值的十进制字符串,用于日志和界面。 - 一次转账,任何钱包都行。交易所提币可以;提币手续费由付款方承担而不是从订单金额里扣,所以交易所从金额里扣费的付款方要把手续费补回去。少付可以再向同一地址补转差额(
topUpAllowed)。 - 地址在所列每条链上都相同(
sameAddressOnEveryChain);在所列的另一条链上转出也能被检测并结算。只认所列的 ERC-20 代币——绝不接受链的原生币。 addressSingleUse: true表示该地址只属于这一笔订单,绝不要把第二笔订单打进去。只有老客户固定收款地址时它才是false。- 在
payBefore(=expiresAt)之前发出。晚到的款仍会被后台扫描检测并结算,只是在下面的状态里会标fundedAfterExpiry。
只有订单已持有收款地址时才有 transferInstructions;用 deferTransferDeposit: true 创建且结账页尚未加载时没有。
2. 发送转账
任何 ERC-20 的 transfer(depositAddress, amountMinor) 都可以。用 viem:
import { createWalletClient, http, erc20Abi, type Address } from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { base } from "viem/chains"
const chain = instructions.chains.find((c) => c.chainId === base.id)!
const usdc = chain.tokens.find((t) => t.symbol === "USDC")!
const wallet = createWalletClient({
account: privateKeyToAccount(process.env.HOT_WALLET_KEY as `0x${string}`),
chain: base,
transport: http(process.env.BASE_RPC_URL),
})
const txHash = await wallet.writeContract({
address: usdc.tokenAddress as Address,
abi: erc20Abi,
functionName: "transfer",
args: [instructions.depositAddress as Address, BigInt(usdc.amountMinor)],
})用 ethers:
import { Contract, JsonRpcProvider, Wallet } from "ethers"
const wallet = new Wallet(process.env.HOT_WALLET_KEY!, new JsonRpcProvider(process.env.BASE_RPC_URL))
const erc20 = new Contract(usdc.tokenAddress, ["function transfer(address to, uint256 value) returns (bool)"], wallet)
const receipt = await (await erc20.transfer(instructions.depositAddress, BigInt(usdc.amountMinor))).wait()3. 轮询 GET /v1/payment-intents/:paymentId/transfer-status
平时是结账页的轮询在驱动到账检测。没有页面打开时,你的轮询就承担同样的工作:每次调用都实时读取收款地址,一旦足额到账就标记已收款(订单变为 processing,并发出 payment_intent.processing webhook)并启动结算。付款方转账期间每 10–15 秒轮询一次;从交易所到账通常要几分钟。
curl "https://api.tariapay.com/v1/payment-intents/<paymentId>/transfer-status" \
-H "Authorization: Bearer tpk_..."{
"paymentIntentStatus": "requires_payment",
"state": "underpaid",
"transfer": {
"chainId": 8453,
"depositAddress": "0x1c73d3e5db612db118aa99c864f277a5743b152c",
"funded": false,
"orderProcessed": false,
"settlementTxHash": null,
"settlementChainId": null,
"tokens": [
{ "currency": "USDC", "tokenAddress": "0x8335…2913", "decimals": 6, "amountMinor": "25000000", "balanceMinor": "0" },
{ "currency": "USDT", "tokenAddress": "0xfde4…9bb2", "decimals": 6, "amountMinor": "25000000", "balanceMinor": "24000000" }
],
"residueSweeps": []
},
"received": { "chainId": 8453, "currency": "USDT", "tokenAddress": "0xfde4…9bb2", "decimals": 6, "amountMinor": "24000000", "source": "balance", "at": null },
"confirmations": { "txHash": null, "chainStatus": null, "paidAt": null, "confirmedAt": null },
"anomalies": {
"underpaid": true,
"shortfall": { "chainId": 8453, "currency": "USDT", "tokenAddress": "0xfde4…9bb2", "decimals": 6, "amountMinor": "1000000" },
"overpaid": false,
"excess": null,
"wrongToken": false,
"wrongTokenDeposits": [],
"wrongChain": null,
"wrongChainDeposits": [],
"recoveredOnChainId": null,
"fundedAfterExpiry": false,
"degraded": false
},
"observedAt": "2026-10-18T11:00:00.000Z",
"paymentIntent": { "id": "…", "status": "requires_payment", "…": "…" }
}| 字段 | 含义 |
|---|---|
state | awaiting_transfer → underpaid(到了一部分,不足额)→ funded(看到足额,正在核实)→ processing(已收款,正在结算进你的余额)→ confirmed。另有 expired、failed、refunded。 |
received | 支付这笔订单的那笔入金:还在到账中时是实时余额(source: "balance"),之后是已记录的收款(deposit_received)或结算(settlement)及其时间。 |
transfer.tokens[].balanceMinor | 地址上此刻的余额,按所钉链上每种接受的币列出。balanceUnavailable: true 表示该次链上读取失败。 |
confirmations | 订单进入 processing/confirmed 后,结算交易及其链上确认情况。 |
anomalies.underpaid / shortfall | 到账少于应付金额;shortfall 是付款方所用那种币还差多少。让付款方把差额补转到同一地址。 |
anomalies.overpaid / excess | 到账多于应付金额(结算前:实时余额超出的部分;结算后:从地址上归集走的多余部分)。不会自动退回——请联系客服并提供付款方的退款地址。 |
anomalies.wrongToken / wrongTokenDeposits | 所钉链上的地址里躺着这笔订单不接受的稳定币(例如美元订单收到了 EURC)。 |
anomalies.wrongChain / wrongChainDeposits / recoveredOnChainId | 入金落在了另一条已启用的链上。wrongChain 在你用 ?scanAllChains=true 请求跨链读取(每条链多一次打包读取——付款方说"我已经转了"却什么都没看到时用,不要每次轮询都带)或某次结算证明之前都是 null;recoveredOnChainId 给出实际结算的链(当它不同于所钉链时)。 |
anomalies.fundedAfterExpiry | 入金在 expiresAt 之后到达或结算。仍然有效。 |
anomalies.degraded | 这次调用有链上读取失败,零余额可能不准。请再轮询一次。 |
该接口只回答已持有收款地址的订单(否则 HTTP 409,错误码 transfer_deposit_not_allocated),且只回答你自己的订单。这些标记只用于报告:绝不会因此移动、拦截或退回资金。原有的 GET …/transfer-deposit 仍然保留,作为严格只读的版本(从不触发检测)。
4. 以 webhook 为准履约
把轮询当作进度展示。收到 payment_intent.confirmed(或 GET /v1/payment-intents/:paymentId 返回 confirmed)时再履约;payment_intent.processing 表示付款方已付、结算正在进行。载荷结构和事件类型与钱包支付完全一致——见接收支付结果。
安全约定
结账页对任何持有链接的人都可访问,所以请把能定位一笔支付的东西当作秘密,并在你自己这边做授权:
paymentId和checkoutUrl是秘密。 拿到它就能打开结账页、看到这笔支付的状态。只发给付款的买家,不要写进日志、分析事件或第三方 URL。- 返回跳转只属于买家的浏览器。 只有付款前打开过结账页的浏览器才会被带到你的
successUrl/cancelUrl;付款后才打开结账链接的人(包括换了设备或浏览器的买家本人)只能看到已支付状态,没有返回链接。不要让履约依赖这次跳转。 - 订单页、成功页不要只凭订单号授权。 请绑定买家的登录态或你签发的一次性令牌。Taria Pay 附加在
successUrl上的payment_id只标识这笔支付,不能证明访问者就是买家。 - 以 HMAC 校验过的 webhook 判定已付(或用带鉴权的 API:
GET /v1/payment-intents/:paymentId、GET /v1/payment-status?orderId=)。不要依据成功页跳转,也不要用交易哈希轮询公开状态接口来判定。 metadata不放个人信息。 要给买家看的内容放displayReference/publicMetadata。- 不要让纯顺序的订单号成为陌生人与订单之间唯一的屏障。 订单号是你用 API key 查询的键,不是访问凭证。
重要规则
- 一个支付订单同时支持钱包支付和直接转账(结账页展示订单专属收款地址),无需传任何付款方式相关字段;两种方式的支付结果和 webhook 事件完全一致。想调整展示方式见上文
paymentMethods,更多背景见支持的付款方式。 orderId对同一商户必须唯一。- 重复创建相同
orderId通常会返回冲突错误。创建超时时请retrieveByOrderId,不要换新的 id——或者带上Idempotency-Key原样重试。 checkoutUrl形如https://checkout.tariapay.com/pi_<uuid>,请原样使用接口返回值,不要自行拼接结账 URL。paymentId本身是不带前缀的 UUID。- 商家不需要提交链上归属地址。
- 通常不需要在创建支付订单时传
chainId,结账页会展示当前可用的支付网络。 - 不要用
successUrl当作最终支付结果。
下一步
- 前端如何处理:跳转到结账页
- 支付按钮或 Checkout Panel:Checkout Button 与 Checkout Panel
- 支付后如何更新订单:接收支付结果