На странице
Создание
Создайте постоянный адрес приёма для одного плательщика по вашему собственному идентификатору — по одному адресу на каждую поддерживаемую сеть.
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, отдельного списка токенов канала и предварительного расчёта комиссии. Идентичность канала неизменна намеренно: адрес, сохранённый плательщиком, никогда не должен начать принадлежать кому-то другому. Чтобы вывести канал из обращения, заблокируйте его.