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

API

На странице

Создание

Создайте постоянный адрес приёма для одного плательщика по вашему собственному идентификатору — по одному адресу на каждую поддерживаемую сеть.

POST/v1/payment-channels

API-ключ: Payment

Платёжный канал — это постоянный адрес приёма для одного вашего плательщика. В отличие от инвойса у него нет суммы, срока и финального состояния: вы создаёте его один раз, показываете адреса, и каждый пришедший перевод становится депозитом, зачисленным на ваш баланс.

Тело запроса

Параметр Тип Обязательный Описание
project_id string Да Идентификатор проекта с префиксом (prj_…). Проект определяет, какие токены принимает канал.
external_id string Да Ваш собственный постоянный идентификатор плательщика, до 128 символов. После создания не меняется.
{
  "project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
  "external_id": "customer-42"
}

Идемпотентность

Пара project_id + external_id в среде того ключа, которым вы подписываете запрос, и есть ключ идемпотентности. Больше передавать нечего, поэтому у повтора нет конфликта данных.

  • Первый вызов → 201 Created и заголовок Location со ссылкой на новый канал.
  • Любой следующий вызов с той же парой → 200 OK и тот же канал. Дубликат не создаётся.
  • Один и тот же external_id в песочнице и в рабочей среде — два независимых канала.

Передавайте тот идентификатор плательщика, который уже используете в своей системе, и вызывайте метод при каждой оплате — хранить pc_ у себя не обязательно. Повтор не ошибка и не дубликат: канал приходит в обоих ответах, и 200 отдаёт те адреса и токены, которые есть у канала на этот момент. Запрос, оборвавшийся по таймауту, безопасно повторить: вернётся исходный канал, а не второй.

Примеры кода

В каждой вкладке используется официальный SDK соответствующего языка. Подпись, сериализацию, безопасные повторы и типизированные ошибки обрабатывает SDK.

API_KEY_ID="pk_live_xxxxxxxxxxxx"
API_SECRET="sk_live_xxxxxxxxxxxx"
BODY='{"project_id":"prj_xxxxxxxxxxxx","external_id":"customer-42"}'

TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGNATURE=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/payment-channels '' "$BODY_HASH" \
  | openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)

curl -sS https://api.paymos.io/v1/payment-channels \
  -H "Authorization: HMAC-SHA256 $API_KEY_ID:$SIGNATURE" \
  -H "X-Request-Timestamp: $TS" \
  -H "Content-Type: application/json" \
  -d "$BODY"
import { Paymos } from '@paymos/sdk';

const paymos = new Paymos({
  apiKey: process.env.PAYMOS_API_KEY,
  apiSecret: process.env.PAYMOS_API_SECRET,
});

const channel = await paymos.paymentChannels.create({
  projectId: 'prj_xxxxxxxxxxxx',
  externalId: 'customer-42',
});

for (const rail of channel.networks) {
  console.log(rail.network, rail.address ?? rail.status);
}
import os

from paymos import Paymos

paymos = Paymos(
    api_key=os.environ["PAYMOS_API_KEY"],
    api_secret=os.environ["PAYMOS_API_SECRET"],
)
channel = paymos.payment_channels.create(
    project_id="prj_xxxxxxxxxxxx",
    external_id="customer-42",
)

for rail in channel["networks"]:
    print(rail["network"], rail.get("address", rail["status"]))
<?php
use Paymos\Client;
use Paymos\ClientConfig;

$paymos = new Client(new ClientConfig(
    getenv('PAYMOS_API_KEY'),
    getenv('PAYMOS_API_SECRET')
));
$channel = $paymos->paymentChannels()->create(array(
    'project_id' => 'prj_xxxxxxxxxxxx',
    'external_id' => 'customer-42',
));

foreach ($channel['networks'] as $rail) {
    $address = isset($rail['address']) ? $rail['address'] : $rail['status'];
    echo $rail['network'] . ' ' . $address . 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)
	}
	channel, err := client.PaymentChannels.Create(context.Background(), paymos.CreatePaymentChannelParams{
		ProjectID:  "prj_xxxxxxxxxxxx",
		ExternalID: "customer-42",
	})
	if err != nil {
		panic(err)
	}
	for _, rail := range channel.Networks {
		fmt.Println(rail.Network, addressOrStatus(rail))
	}
}

func addressOrStatus(rail paymos.PaymentChannelNetwork) string {
	if rail.Address == nil {
		return string(rail.Status)
	}
	return *rail.Address
}
using Paymos;

using var paymos = new PaymosClient(
    Environment.GetEnvironmentVariable("PAYMOS_API_KEY")!,
    Environment.GetEnvironmentVariable("PAYMOS_API_SECRET")!);

var channel = await paymos.PaymentChannels.CreateAsync(new CreatePaymentChannelRequest(
    ProjectId: "prj_xxxxxxxxxxxx",
    ExternalId: "customer-42"));

foreach (var rail in channel.Networks)
{
    Console.WriteLine($"{rail.Network} {rail.Address ?? rail.Status}");
}
import io.paymos.CreatePaymentChannelRequest;
import io.paymos.PaymentChannel;
import io.paymos.PaymentChannelNetwork;
import io.paymos.PaymosClient;

PaymosClient paymos = new PaymosClient(
    System.getenv("PAYMOS_API_KEY"),
    System.getenv("PAYMOS_API_SECRET"));

PaymentChannel channel = paymos.paymentChannels.create(
    new CreatePaymentChannelRequest("prj_xxxxxxxxxxxx", "customer-42"));

for (PaymentChannelNetwork rail : channel.networks()) {
    System.out.println(rail.network() + " "
        + (rail.address() == null ? rail.status() : rail.address()));
}
require 'paymos'

paymos = Paymos::Client.new(
  api_key: ENV.fetch('PAYMOS_API_KEY'),
  api_secret: ENV.fetch('PAYMOS_API_SECRET')
)
channel = paymos.payment_channels.create(
  project_id: 'prj_xxxxxxxxxxxx',
  external_id: 'customer-42'
)

channel.networks.each do |rail|
  puts "#{rail.network} #{rail.address || rail.status}"
end
use paymos::{CreatePaymentChannelRequest, PaymosClient};

let paymos = PaymosClient::new(
    std::env::var("PAYMOS_API_KEY")?,
    std::env::var("PAYMOS_API_SECRET")?,
)?;
let channel = paymos
    .payment_channels()
    .create(&CreatePaymentChannelRequest {
        project_id: "prj_xxxxxxxxxxxx".to_owned(),
        external_id: "customer-42".to_owned(),
    })
    .await?;

for rail in &channel.networks {
    println!("{} {}", rail.network, rail.address.as_deref().unwrap_or(&rail.status));
}

Ответ (201 Created, при повторе — 200)

{
  "id": "pc_2QhKZv6mRt9WdA3nYpLbXf",
  "project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
  "external_id": "customer-42",
  "status": "active",
  "is_accepting_payments": true,
  "is_fully_provisioned": false,
  "is_test": false,
  "applied_fee_percent": 1.0,
  "customer_fee_percent": 0,
  "networks": [
    {
      "network": "TRC20",
      "status": "active",
      "address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
      "tokens": [
        { "symbol": "USDT", "minimum_deposit": "1.00" },
        { "symbol": "USDC", "minimum_deposit": "1.00" }
      ]
    },
    {
      "network": "ERC20",
      "status": "active",
      "address": "0x9f2a6c1b4e8d7350ab11c9e5f0d2743b86ce41d9",
      "tokens": [
        { "symbol": "USDT", "minimum_deposit": "12.00" },
        { "symbol": "USDC", "minimum_deposit": "12.00" }
      ]
    },
    {
      "network": "SOL",
      "status": "provisioning",
      "tokens": [
        { "symbol": "USDC", "minimum_deposit": null }
      ]
    }
  ],
  "created_at": 1767225600,
  "updated_at": 1767225780
}

Сети и адреса

У канала нет собственного списка токенов. Он принимает то, что принимает проект: включённый на проекте токен появляется сразу во всех его каналах, выключенный — исчезает везде. Список настраивается в Дашборд → Проекты.

Каждый элемент networks описывает одну сеть:

Поле Значение
network Код сети, например TRC20 или ERC20.
status active, когда адрес готов, и provisioning, пока идёт подготовка.
address Постоянный адрес приёма. Отсутствует, пока сеть не закончила подготовку: это ещё не готов, а не не будет. Выданный адрес больше не меняется.
tokens Что сеть принимает прямо сейчас. Каждый элемент — объект: symbol и minimum_deposit, а не просто код токена.

Что показывать плательщику, определяют два правила:

  • Показывайте только сети со статусом active и непустым массивом tokens. У элемента provisioning адреса ещё нет.
  • Перестаньте показывать маршрут, которого больше нет в ответе. Адреса никогда не передаются другому каналу, но сеть, убранная из проекта, пропадает из ответа, и деньги там больше не ожидаются.

Возвращённый адрес закреплён за каналом навсегда. Кэшируйте его: повторное чтение канала вернёт то же значение, и один адрес обслуживает все токены своей сети.

У каждого токена в списке свой minimum_deposit — наименьшая сумма перевода, которая зачислится по этому маршруту. Берите её из того же ответа, который показываете, и выводите рядом с адресом. Само поле разобрано в Чтении канала, включая случай null.

Подготовка

Новый канал в рабочей среде открывается в состоянии provisioning, без адресов. Каждая сеть готовится независимо и становится рабочей, как только у неё появился свой адрес, — ждать последнюю цепочку не нужно. Канал переходит в active на первом готовом адресе и остаётся там.

is_fully_provisioned показывает, готовы ли уже все требуемые сети. Канал может быть active и при этом не полностью подготовлен — это нормальное состояние, пока остальные цепочки догоняют.

Каналы в песочнице готовы сразу: их адреса вычисляются локально и не затрагивают блокчейн.

Комиссии

applied_fee_percent и customer_fee_percent — ваши текущие комиссии. Это справочные значения, а не расчёт по конкретному платежу: они меняются вместе с вашим тарифом.

Обязывающие цифры хранятся в каждом депозите. Депозит фиксирует комиссии на момент зачисления и сообщает собственные gross, fee и net. Смена тарифа не переписывает уже полученный платёж. Отдельного метода предварительного расчёта комиссии нет: чтобы увидеть арифметику, создайте депозит в песочнице.

Ошибки

Код HTTP Когда
payment_channels_disabled 503 Платёжные каналы не включены для вашего аккаунта в этой среде.
payment_key_required 403 Запрос подписан ключом выплат (rk_).
project_not_found 404 project_id не соответствует ничему, доступному этому ключу.
payment_channel_external_id_invalid 400 external_id пуст, состоит из пробелов или длиннее 128 символов.
payment_channel_project_has_no_supported_tokens 409 В проекте не включён ни один токен, который канал мог бы принимать.

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

Чего в этом API нет

Нет методов изменения, удаления, переназначения, callback-URL, отдельного списка токенов канала и предварительного расчёта комиссии. Идентичность канала неизменна намеренно: адрес, сохранённый плательщиком, никогда не должен начать принадлежать кому-то другому. Чтобы вывести канал из обращения, заблокируйте его.