Skip to content

API

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.

GET/v1/payment-channel-deposits

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_cursor after the page is fully processed, not before.
  • 03Send it back on the next poll. Repeat.

The contract behind those three steps:

  • next_cursor is 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_cursor turns null; this feed never ends that way, and a loop written that way never exits. Fewer items than limit means there is nothing more for you right now: store the cursor, leave the loop, and poll again on your schedule.
  • confirmed_from bounds 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 — the pcd_ 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_invalid and has to start over bounded by confirmed_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.