Skip to content

创建支付订单 ​

支付订单用于把你网站里的业务订单连接到 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 示例 ​

ts
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 示例 ​

bash
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 字段调整:

json
{
  "orderId": "order_1001",
  "amount": "12.50",
  "currency": "USDC",
  "paymentMethods": {
    "wallet": true,
    "transfer": true,
    "transferDefaultOpen": true
  }
}
字段默认值说明
wallettrue是否展示"连接钱包"付款模块
transfertrue是否提供直接转账;设为 false 时服务端也会拒绝该订单的转账请求
transferDefaultOpenfalse转账区是否默认展开;wallet: false 时自动展开

规则与兜底:

  • wallet 和 transfer 不能同时为 false,否则返回 400。
  • 两个开关都在服务端强制,不只是隐藏界面:关掉的方式对应的接口会返回 403。
  • 直接转账仍受网络支持范围约束。如果你设置了 wallet: false,但订单当前所在的网络不支持转账,结账页会自动回落展示钱包模块,并允许在该网络上用钱包付款,保证买家始终有可用的付款方式;买家切换到支持转账的网络后,钱包模块会重新隐藏。
  • 不传该字段时行为与现在完全一致,已有接入无需任何改动。

老客户固定收款地址 ​

默认情况下,每笔订单都会分配一个全新的、不复用的转账收款地址。对于已验证身份的老客户,你可以改为给每位客户一个固定收款地址:该客户的所有订单、所有受支持的网络都使用同一个地址。常从交易所付款的回头客可以把地址存入交易所地址簿,不必每次重新复制新地址。

创建订单时传入你自己系统里的客户 ID:

ts
const paymentIntent = await tariapay.paymentIntents.create({
  orderId: "order_1001",
  amount: "12.50",
  currency: "USDC",
  fixedTransferCustomerId: "user_42", // 你的客户 ID,仅限已验证账号
})

用 REST 时改为设置保留 metadata 键(SDK 字段做的就是这件事):

json
{
  "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,超时的请求就能安全重试。

bash
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:

json
{
  "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:

ts
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:

ts
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 秒轮询一次;从交易所到账通常要几分钟。

bash
curl "https://api.tariapay.com/v1/payment-intents/<paymentId>/transfer-status" \
  -H "Authorization: Bearer tpk_..."
json
{
  "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", "…": "…" }
}
字段含义
stateawaiting_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 当作最终支付结果。

下一步 ​