Перейти к содержимому

API

На странице

Лента

Читайте подтверждённые депозиты платёжных каналов в порядке публикации с курсором, который всегда двигается вперёд, не пропуская и не теряя платежи.

GET/v1/payment-channel-deposits

API-ключ: Payment

Это лента для интеграции, а не страница истории. Она возвращает только подтверждённые депозиты, от старых к новым, в едином порядке публикации, и всегда отдаёт курсор. Опрашивайте её по расписанию — и увидите каждый расчётный платёж ровно один раз, в порядке, который не меняется задним числом.

Вебхуки — быстрый путь, лента — надёжный. Используйте оба: вебхуки дают секунды, лента гарантирует, что вы в итоге увидите всё, даже если ваш endpoint был недоступен.

Параметры запроса

Параметр Тип По умолчанию Описание
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 ограничивает только первый опрос. Дальше место возобновления определяет сохранённый курсор. Отправляйте то же значение без изменений вместе с курсором: курсор привязан в том числе к нему, поэтому убранный или сдвинутый confirmed_from делает его недействительным.
  • Порядок монотонный. Депозит публикуется один раз, на фиксированной позиции, и более позднее подтверждение не может оказаться раньше того, что вы уже прочитали.
  • Продвижение курсора — ваше решение. Повторив старый курсор, вы можете увидеть депозиты снова. Это безопасно, если вы дедуплицируете по id: идентификатор pcd_ одинаков в вебхуке, в ленте и при любой повторной доставке.
  • Курсор живёт 24 часа, поэтому опрашивайте ленту не реже раза в сутки. Это точка возобновления, а не закладка, которую можно отложить на неделю: если опрос молчал дольше, курсор отклонят с 400 pagination_cursor_invalid и читать придётся заново, от confirmed_from.
  • Курсор привязан к вашему ключу, его среде, его области видимости проектов и отправленным фильтрам. Смените фильтр или расширьте доступ ключа к проектам — старый курсор отклонят так же. Начните с первой страницы: дедупликация по 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 Запрос подписан ключом выплат (rk_).
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 Параметр, которого нет в таблице выше.

Полный каталог — в Кодах ошибок.