本页内容
轮询
按发布顺序轮询已确认的收款通道充值,游标只向前推进,任何一笔已结算的付款都不会被跳过或重复。
GET/v1/payment-channel-deposits
API 密钥: Payment
这是一条对接用的数据流,不是历史查询页。它只返回已确认的充值,最早的在前,按全局发布顺序排列,并且每次都回给你一个游标。按计划轮询它,每一笔已结算的付款你都会看到,且只看到一次,顺序不会在你脚下变动。
Webhook 是快路径,这条流是可靠路径。两个都用:webhook 给你秒级速度,这条流保证即使你的端点宕过机,最终也能看到全部。
查询参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
limit |
integer | 20 |
每页大小,1 到 100 |
cursor |
string | — | 上一页 next_cursor 给出的不透明游标 |
project_id |
string | — | 限定到单个项目(prj_…) |
payment_channel_id |
string | — | 限定到单个通道(pc_…) |
confirmed_from |
integer | — | Unix 秒;只返回在该时刻或之后确认的充值。它只约束第一次轮询——见下文 |
这里刻意没有 status 参数。一个能查 confirming 的轮询器,等于把链重组还能收走的钱当成已结算。
示例
GET /v1/payment-channel-deposits?limit=100&cursor=CfDJ8JvN… HTTP/1.1
Host: api.paymos.io
Authorization: HMAC-SHA256 pk_live_…:…
X-Request-Timestamp: 1767225660
代码示例
每个标签页使用该语言的官方 SDK。签名、序列化、安全重试和类型化错误都由 SDK 处理。
API_KEY_ID="pk_live_xxxxxxxxxxxx"
API_SECRET="sk_live_xxxxxxxxxxxx"
QUERY="?limit=100"
TS=$(date +%s)
# No body: the hash slot is the EMPTY STRING, not sha256("") — its fixed
# e3b0c442… digest signs a different payload and the request is rejected. The
# query keeps its leading "?" exactly as transmitted.
SIGNATURE=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" GET /v1/payment-channel-deposits "$QUERY" '' \
| openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)
curl -sS "https://api.paymos.io/v1/payment-channel-deposits$QUERY" \
-H "Authorization: HMAC-SHA256 $API_KEY_ID:$SIGNATURE" \
-H "X-Request-Timestamp: $TS"
import { Paymos } from '@paymos/sdk';
const paymos = new Paymos({
apiKey: process.env.PAYMOS_API_KEY,
apiSecret: process.env.PAYMOS_API_SECRET,
});
const page = await paymos.paymentChannelDeposits.read({ limit: 100 });
for (const deposit of page.items) {
console.log(deposit.id, deposit.net, deposit.currency);
}
console.log('resume from', page.nextCursor);
import os
from paymos import Paymos
paymos = Paymos(
api_key=os.environ["PAYMOS_API_KEY"],
api_secret=os.environ["PAYMOS_API_SECRET"],
)
page = paymos.payment_channel_deposits.read(limit=100)
for deposit in page["items"]:
print(deposit["id"], deposit["net"], deposit["currency"])
print("resume from", page["next_cursor"])
<?php
use Paymos\Client;
use Paymos\ClientConfig;
$paymos = new Client(new ClientConfig(
getenv('PAYMOS_API_KEY'),
getenv('PAYMOS_API_SECRET')
));
$page = $paymos->paymentChannelDeposits()->readPage(array('limit' => 100));
foreach ($page['items'] as $deposit) {
echo $deposit['id'] . ' ' . $deposit['net'] . ' ' . $deposit['currency'] . PHP_EOL;
}
echo 'resume from ' . $page['next_cursor'] . PHP_EOL;
package main
import (
"context"
"fmt"
"os"
paymos "github.com/Paymos-labs/go-sdk/v2"
)
func main() {
client, err := paymos.NewClient(os.Getenv("PAYMOS_API_KEY"), os.Getenv("PAYMOS_API_SECRET"))
if err != nil {
panic(err)
}
page, err := client.PaymentChannelDeposits.Read(context.Background(), paymos.PaymentChannelDepositFeedParams{
Limit: 100,
})
if err != nil {
panic(err)
}
for _, deposit := range page.Items {
fmt.Println(deposit.ID, deposit.Net, deposit.Currency)
}
fmt.Println("resume from", page.NextCursor)
}
using Paymos;
using var paymos = new PaymosClient(
Environment.GetEnvironmentVariable("PAYMOS_API_KEY")!,
Environment.GetEnvironmentVariable("PAYMOS_API_SECRET")!);
var page = await paymos.PaymentChannelDeposits.ReadAsync(
new PaymentChannelDepositFeedOptions(Limit: 100));
foreach (var deposit in page.Items)
{
Console.WriteLine($"{deposit.Id} {deposit.Net} {deposit.Currency}");
}
Console.WriteLine($"resume from {page.NextCursor}");
import io.paymos.PaymentChannelDeposit;
import io.paymos.PaymentChannelDepositFeedOptions;
import io.paymos.PaymentChannelDepositFeedPage;
import io.paymos.PaymosClient;
PaymosClient paymos = new PaymosClient(
System.getenv("PAYMOS_API_KEY"),
System.getenv("PAYMOS_API_SECRET"));
PaymentChannelDepositFeedPage page = paymos.paymentChannelDeposits.read(
PaymentChannelDepositFeedOptions.builder().limit(100).build());
for (PaymentChannelDeposit deposit : page.items()) {
System.out.println(deposit.id() + " " + deposit.net() + " " + deposit.currency());
}
System.out.println("resume from " + page.nextCursor());
require 'paymos'
paymos = Paymos::Client.new(
api_key: ENV.fetch('PAYMOS_API_KEY'),
api_secret: ENV.fetch('PAYMOS_API_SECRET')
)
page = paymos.payment_channel_deposits.read(limit: 100)
page.items.each do |deposit|
puts "#{deposit.id} #{deposit.net} #{deposit.currency}"
end
puts "resume from #{page.next_cursor}"
use paymos::{PaymentChannelDepositFeedParams, PaymosClient};
let paymos = PaymosClient::new(
std::env::var("PAYMOS_API_KEY")?,
std::env::var("PAYMOS_API_SECRET")?,
)?;
let page = paymos
.payment_channel_deposits()
.read(&PaymentChannelDepositFeedParams {
limit: Some(100),
..Default::default()
})
.await?;
for deposit in &page.items {
println!("{} {} {}", deposit.id, deposit.net, deposit.currency);
}
println!("resume from {}", page.next_cursor);
响应 (200 OK)
{
"items": [
{
"id": "pcd_8ScRvL4jNq2XkB7mTfZdWu",
"payment_channel_id": "pc_2QhKZv6mRt9WdA3nYpLbXf",
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"payment_channel_external_id": "customer-42",
"status": "confirmed",
"is_final": true,
"is_test": false,
"currency": "USDT",
"network": "TRC20",
"chain_id": 728126428,
"contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"gross": "100",
"fee": "1",
"net": "99",
"applied_fee_percent": 1.0,
"customer_fee_percent": 0,
"tx_hash": "9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25",
"transfer_id": "9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25:41a614f803b6fd780986a42c78ec9c7f77e6ded13c:41c9f6a2b7d0138e54ca3b91f6072ed48a5c1e93b7:0",
"source_address": "TW9s4RkAqBnLpVdX2ChYzUeGm7QfKt3NbZ",
"destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"block_height": 68421905,
"first_included_block_timestamp": 1767225900,
"explorer_url": "https://tronscan.org/#/transaction/9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25",
"created_at": 1767225870,
"updated_at": 1767225930,
"confirmed_at": 1767225930
},
{
"id": "pcd_5NmXqW7bHt3ZjR9kCvPyAe",
"payment_channel_id": "pc_2QhKZv6mRt9WdA3nYpLbXf",
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"payment_channel_external_id": "customer-42",
"status": "confirmed",
"is_final": true,
"is_test": false,
"currency": "USDC",
"network": "ERC20",
"chain_id": 1,
"contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"gross": "42.5",
"fee": "0.425",
"net": "42.075",
"applied_fee_percent": 1.0,
"customer_fee_percent": 0,
"tx_hash": "0x1d7b64e2c05af398b2e714cd9a06f3518cb7e2409df6a1c8305b47ed12f9ca63",
"transfer_id": "0x1d7b64e2c05af398b2e714cd9a06f3518cb7e2409df6a1c8305b47ed12f9ca63:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48:0x9f2a6c1b4e8d7350ab11c9e5f0d2743b86ce41d9:0",
"source_address": "0x4b8e1f60d3a97c25be04f7183a5cd9027ef6b14a",
"destination_address": "0x9f2a6c1b4e8d7350ab11c9e5f0d2743b86ce41d9",
"block_height": 22984117,
"first_included_block_timestamp": 1767226020,
"explorer_url": "https://etherscan.io/tx/0x1d7b64e2c05af398b2e714cd9a06f3518cb7e2409df6a1c8305b47ed12f9ca63",
"created_at": 1767226020,
"updated_at": 1767226260,
"confirmed_at": 1767226260
}
],
"next_cursor": "CfDJ8JvN…"
}
正确的轮询方式
- 01第一次不带游标,把整页处理完
- 02整页处理完之后再持久化
next_cursor,不要提前 - 03下一轮把它发回来,如此循环
这三步背后的契约:
- —
next_cursor永远不为 null——每个响应都带它,空页也带。位置是全局的,不是每个商户各排一套,所以匹配不到任何东西的筛选条件也必须越过它跳过的那些位置——闲置的集成因此不会永远重扫同一段 - —在空页或不满页停下,而不是在游标为
null时停——账单列表在next_cursor变成null时结束;这条流没有这样的结尾,按这个条件写的循环永远退不出来。返回条数少于limit,说明此刻已经没有你的份了:存好游标,跳出循环,按你自己的节奏下次再来 - —
confirmed_from只约束第一次轮询——从第二次起,从哪里接着读由存下来的游标决定。请把同一个值原样跟着游标一起发:它也是游标绑定的一部分,去掉它或者往后挪都会让游标失效 - —顺序单调——一条充值只发布一次,位置固定;后确认的永远不会出现在你已经读过的先确认的之前
- —推进由你决定——重试较早的游标可能再次看到某些充值。只要你按
id去重就是安全的:pcd_标识在 webhook 投递、数据流投递和任何重放之间保持不变 - —游标 24 小时后过期,所以任务至少每天跑一次。游标是接着读的位置,不是能搁一周的书签:停得更久,再拿出来就是
400 pagination_cursor_invalid,只能用confirmed_from划定范围重头读 - —游标还绑定你的凭证、它的环境、它的项目作用域,以及你发送的筛选条件。改一个筛选条件,或放宽 key 的项目访问范围,旧游标同样被拒。从第一页重新开始即可:按
pcd_去重让整轮重新同步无害
数据流停下来的时候
如果某一条充值渲染不出来,这条流不跳过它,也不让整页失败。它把它之前的全部投递出去,把 next_cursor 停在那个位置,并告诉你是哪一条、为什么:
{
"items": [],
"next_cursor": "CfDJ8JvN…",
"blocked": {
"deposit_id": "pcd_4KdQzT9rVn6WsB1mYhFxLg",
"reason": "booked_auth_missing"
}
}
正常页面上不会出现 blocked。出现时:
| 原因 | 说明 |
|---|---|
booked_auth_missing |
付款已确认,但对应的记账分录还没有写入 |
inconsistent |
充值、它的通道和它的链上记录三者对不上 |
deposit_missing |
已发布的记录解析不到充值 |
channel_missing |
充值所属的通道加载不出来 |
transfer_missing |
充值没有关联的链上记录可作凭据来源 |
not_confirmed |
已发布的充值不处于已确认状态;只有最终的钱才进这条流 |
继续轮询。next_cursor 刻意停在被卡住的位置之前,底层记录一修好,这条流就自行恢复,中间什么都不会丢。以上每一种都是我们这边的运维故障——如果持续存在,请带上 deposit_id 联系支持。
错误
| 错误码 | HTTP | 何时出现 |
|---|---|---|
payment_channels_disabled |
503 | 本环境下你的账户未开通收款通道 |
payment_key_required |
403 | 请求用 Payout(rk_)key 签名 |
pagination_cursor_invalid |
400 | 游标格式错误、已过期,或绑定的是另一组筛选条件、另一个作用域 |
field_invalid_format |
400 | project_id 或 payment_channel_id 不是合法的带前缀标识 |
field_out_of_range |
400 | limit 超出 1 到 100 的范围 |
query_parameter_unknown |
400 | 传了上表之外的参数 |
完整目录见错误码。