On this page
Poll feed
Poll confirmed payment-channel deposits in publication order with a cursor that always advances, so no settled payment is skipped or replayed silently.
API key: Payment
An integration feed, not a history page. It returns confirmed deposits only, oldest first, in a global publication order, and it always hands back a cursor. Poll it on a schedule and you will see every settled payment exactly once, in an order that never changes underneath you.
Webhooks are the fast path; this feed is the reliable one. Run both: webhooks give you seconds, the feed guarantees you eventually see everything even if your endpoint was down.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 20 |
Page size from 1 to 100. |
cursor |
string | — | Opaque cursor from the previous page's next_cursor. |
project_id |
string | — | Restrict to one project (prj_…). |
payment_channel_id |
string | — | Restrict to one channel (pc_…). |
confirmed_from |
integer | — | Unix seconds; only deposits confirmed at or after this instant. It bounds the first poll only — see below. |
There is deliberately no status parameter. A poller that could ask for confirming would be treating money that a reorg can still take away as settled.
Example
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
Code examples
Each tab uses the official SDK for that language. Signing, serialization, safe retries, and typed errors are handled by the 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);
Response (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…"
}
How to poll it correctly
- 01Start with no cursor. Process the whole page.
- 02Persist
next_cursorafter the page is fully processed, not before. - 03Send it back on the next poll. Repeat.
The contract behind those three steps:
- —
next_cursoris never null. It is present on every response, including an empty page. Positions are global rather than per-merchant, so a filter that matches nothing still has to advance past the ones it skipped — which is why an idle integration does not rescan the same range forever. - —Stop on an empty or short page, not on a null cursor. Listing invoices ends when
next_cursorturnsnull; this feed never ends that way, and a loop written that way never exits. Fewer items thanlimitmeans there is nothing more for you right now: store the cursor, leave the loop, and poll again on your schedule. - —
confirmed_frombounds the first poll only. From the second poll on, the stored cursor decides where you resume. Keep sending the same value unchanged next to the cursor — it is part of what the cursor is bound to, so dropping it or moving it forward invalidates the cursor. - —Order is monotonic. A deposit is published once, at a fixed position, and a later confirmation can never appear before an earlier one you have already read.
- —Advancing is your decision. Retry an older cursor and you may see deposits again. That is safe as long as you deduplicate on
id— thepcd_identifier is stable across webhook delivery, feed delivery and any replay. - —Cursors expire after 24 hours, so schedule the job to run at least once a day. A cursor is where you resume, not a bookmark you can shelve for a week — a poller that goes quiet for longer comes back to
400 pagination_cursor_invalidand has to start over bounded byconfirmed_from. - —A cursor is also bound to your credential, its environment, its project scope and the filters you sent. Change a filter, or widen the key's project access, and the old cursor is refused the same way. Restart from the first page: deduplication on
pcd_makes a full resynchronization harmless.
When the feed stops
If a single deposit cannot be rendered, the feed does not skip it and does not fail the page. It delivers everything before it, holds next_cursor at that position, and tells you which deposit and why:
{
"items": [],
"next_cursor": "CfDJ8JvN…",
"blocked": {
"deposit_id": "pcd_4KdQzT9rVn6WsB1mYhFxLg",
"reason": "booked_auth_missing"
}
}
blocked is absent on every normal page. When it is present:
| Reason | Meaning |
|---|---|
booked_auth_missing |
The payment is confirmed but its accounting entry has not been written yet. |
inconsistent |
The deposit, its channel and its chain record disagree with each other. |
deposit_missing |
The published record no longer resolves to a deposit. |
channel_missing |
The deposit's channel could not be loaded. |
transfer_missing |
The deposit has no linked chain record to source its evidence from. |
not_confirmed |
A published deposit is not in a confirmed state; only final money enters this feed. |
Keep polling. next_cursor deliberately stays below the blocked position, so the feed resumes on its own the moment the underlying record is repaired and nothing is lost in between. Every one of these is an operational fault on our side — contact support with the deposit_id if it persists.
Errors
| Code | HTTP | When |
|---|---|---|
payment_channels_disabled |
503 | Payment channels are not enabled for your account in this environment. |
payment_key_required |
403 | The request was signed with a Payout (rk_) key. |
pagination_cursor_invalid |
400 | The cursor is malformed, expired, or bound to different filters or a different scope. |
field_invalid_format |
400 | project_id or payment_channel_id is not a valid prefixed identifier. |
field_out_of_range |
400 | limit is outside 1–100. |
query_parameter_unknown |
400 | A parameter that is not in the table above. |
See Error Codes for the full catalogue.