На странице
Создание
Создание платёжного инвойса для приёма криптовалютных платежей. Два потока: прямой крипто (токен указан) и фиатный (клиент выбирает токен на странице оплаты).
API-ключ: Payment
Тело запроса
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
project_id |
string | Да | Идентификатор проекта с префиксом (prj_...). API-ключ привязан к мерчанту; укажите, какой проект получит инвойс |
amount |
string | Да | Сумма платежа строкой с десятичной точкой (например, "50.00") |
currency |
string | Да | Код фиатной валюты (USD, EUR и т.д.) или символ криптоактива. Полный список — Поддерживаемые валюты. У криптовалюты на вкладке «Валюты» проекта должна остаться включённой хотя бы одна сеть; если выключены все, запрос вернёт 400 currency_not_enabled_for_project |
network |
string | Условно | Код сети блокчейна — TRC20, ERC20, BEP20, POLYGON, ARBITRUM, OPTIMISM, BASE, TON, AVAX, SOL, NEAR, SUI или PLASMA (см. Поддерживаемые валюты). Нужна, если вы хотите зафиксировать крипто-инвойс на конкретной сети. Сеть, выключенную в проекте, API отклоняет с 400 field_invalid_enum по полю network |
external_order_id |
string | Да | Ваш уникальный идентификатор заказа (макс. 128 символов), уникальный в проекте — один на тестовый и рабочий режимы. Делает создание инвойса идемпотентным: на повтор того же запроса придёт уже созданный инвойс со статусом 200 OK, а не дубль. Тот же id с другими amount, currency, network или из другого режима вернёт 409 (invoice_idempotency_conflict). Не может начинаться с wgt_ — этот префикс занят инвойсами Low-Code SDK |
client_id |
string | Нет | Идентификатор вашего клиента (макс. 200 символов). Хранится в инвойсе и возвращается при чтении и в списках — по нему вы группируете инвойсы в своём учёте. Никаких ограничений Paymos по этому полю не применяет |
allow_multiple_payments |
boolean | Нет | Принимать ли несколько частичных платежей. По умолчанию: true |
customer_fee_percent |
integer | Нет | Процент комиссии для клиента (0-100). Переопределяет настройку проекта для этого инвойса |
Точность фиатной суммы
Для фиатного инвойса число знаков после точки в amount должно укладываться в минорную единицу валюты currency (см. таблицу ниже).
| Знаков после точки | Валюты |
|---|---|
| 0 знаков | JPY, KRW, VND, CLP |
| 2 знака | USD, EUR, GBP, CHF, CAD, AUD, NZD, PLN, CZK, DKK, SEK, NOK, HUF, RUB, UAH, GEL, CNY, HKD, INR, IDR, MYR, PHP, THB, PKR, SGD, BRL, MXN, ARS, TRY, AED, ILS, NGN, ZAR, BDT, BMD, LKR, MMK, SAR, TWD, VES |
| 3 знака | BHD, KWD |
Примеры: 23.34 валидно для USD; 23.345 отклоняется для USD; 23 валидно для JPY; 23.01 отклоняется для JPY.
Определение потока
Поток задают поля, которые вы передаёте. currency решает, в чём указана сумма — в токене или в фиатной валюте. network решает, закреплена ли сеть заранее или её выбирает клиент.
currency |
network |
Поток | Что выбирает клиент |
|---|---|---|---|
токен (USDT, USDC, …) |
сеть (TRC20, ERC20, …) |
Прямой крипто — amount это сумма в токене, сеть закреплена |
Ничего: токен и сеть уже заданы |
токен (USDT, USDC, …) |
— | Крипто-поток — amount это сумма в токене, сеть открыта |
Сеть для оплаты |
фиат (USD, EUR, …) |
— | Фиатный — amount это сумма в фиатной валюте |
Токен и сеть |
Любой инвойс создаётся в статусе awaiting_client — платёжного адреса ещё нет. Адрес назначается и инвойс переходит в awaiting_payment только после того, как клиент подтвердит оплату на странице Paymos. В прямом крипто-потоке выбирать нечего: страница оплаты подтверждает заранее заданный токен и сеть и назначает адрес одним шагом. В фиатном потоке в этот же момент фиксируется курс фиат → крипто; на инвойсе он доступен в поле payment.exchange_rate.
См. Платёжный процесс → Два потока создания — полный жизненный цикл, начальные статусы и как фиксируется курс.
Пример запроса — фиатный поток
Сумма в фиате; клиент выбирает токен на странице оплаты, курс фиксируется в момент выбора.
{
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"amount": "50.00",
"currency": "USD",
"external_order_id": "order-12345",
"client_id": "customer-67890"
}
Пример запроса — прямой крипто
Токен и сеть фиксированы при создании; адрес назначается сразу.
{
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"amount": "50.00",
"currency": "USDT",
"network": "TRC20",
"external_order_id": "order-12345",
"client_id": "customer-67890"
}
Примеры кода
В каждой вкладке используется официальный SDK соответствующего языка. Подпись, сериализацию, безопасные повторы и типизированные ошибки обрабатывает SDK.
API_KEY_ID="pk_live_xxxxxxxxxxxx"
API_SECRET="sk_live_xxxxxxxxxxxx"
BODY='{"project_id":"prj_xxxxxxxxxxxx","amount":"100.00","currency":"USD","external_order_id":"order-123","client_id":"customer-456"}'
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/invoices '' "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)
curl -sS https://api.paymos.io/v1/invoices \
-H "Authorization: HMAC-SHA256 $API_KEY_ID:$SIGNATURE" \
-H "X-Request-Timestamp: $TS" \
-H "Content-Type: application/json" \
-d "$BODY"
import { Paymos, externalOrderId } from '@paymos/sdk';
const paymos = new Paymos({
apiKey: process.env.PAYMOS_API_KEY,
apiSecret: process.env.PAYMOS_API_SECRET,
});
const invoice = await paymos.invoices.create({
projectId: 'prj_xxxxxxxxxxxx',
amount: '100.00',
currency: 'USD',
externalOrderId: externalOrderId('order'),
clientId: 'customer-456',
});
console.log(invoice.invoiceId, invoice.paymentUrl);
import os
from paymos import Paymos
paymos = Paymos(
api_key=os.environ["PAYMOS_API_KEY"],
api_secret=os.environ["PAYMOS_API_SECRET"],
)
invoice = paymos.invoices.create(
project_id="prj_xxxxxxxxxxxx",
amount="100.00",
currency="USD",
external_order_id="order-123",
client_id="customer-456",
)
print(invoice["invoice_id"], invoice["payment_url"])
<?php
use Paymos\Client;
use Paymos\ClientConfig;
use Paymos\IdempotencyKey;
$paymos = new Client(new ClientConfig(
getenv('PAYMOS_API_KEY'),
getenv('PAYMOS_API_SECRET')
));
$invoice = $paymos->invoices()->create(array(
'project_id' => 'prj_xxxxxxxxxxxx',
'amount' => '100.00',
'currency' => 'USD',
'external_order_id' => IdempotencyKey::externalOrderId('order'),
'client_id' => 'customer-456',
));
echo $invoice['invoice_id'] . ' ' . $invoice['payment_url'];
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)
}
invoice, err := client.Invoices.Create(context.Background(), paymos.CreateInvoiceParams{
ProjectID: "prj_xxxxxxxxxxxx",
Amount: "100.00",
Currency: "USD",
ExternalOrderID: "order-123",
ClientID: stringPointer("customer-456"),
})
if err != nil {
panic(err)
}
fmt.Println(invoice.InvoiceID, invoice.PaymentURL)
}
func stringPointer(value string) *string { return &value }
using Paymos;
using var paymos = new PaymosClient(
Environment.GetEnvironmentVariable("PAYMOS_API_KEY")!,
Environment.GetEnvironmentVariable("PAYMOS_API_SECRET")!);
var invoice = await paymos.Invoices.CreateAsync(new CreateInvoiceRequest(
ProjectId: "prj_xxxxxxxxxxxx",
Amount: "100.00",
Currency: "USD",
ExternalOrderId: "order-123",
ClientId: "customer-456"));
Console.WriteLine($"{invoice.InvoiceId} {invoice.PaymentUrl}");
import io.paymos.CreateInvoiceRequest;
import io.paymos.Invoice;
import io.paymos.PaymosClient;
PaymosClient paymos = new PaymosClient(
System.getenv("PAYMOS_API_KEY"),
System.getenv("PAYMOS_API_SECRET"));
Invoice invoice = paymos.invoices.create(
CreateInvoiceRequest.builder()
.projectId("prj_xxxxxxxxxxxx")
.amount("100.00")
.currency("USD")
.externalOrderId("order-123")
.clientId("customer-456")
.build());
System.out.println(invoice.invoiceId() + " " + invoice.paymentUrl());
require 'paymos'
paymos = Paymos::Client.new(
api_key: ENV.fetch('PAYMOS_API_KEY'),
api_secret: ENV.fetch('PAYMOS_API_SECRET')
)
invoice = paymos.invoices.create(
project_id: 'prj_xxxxxxxxxxxx',
amount: '100.00',
currency: 'USD',
external_order_id: 'order-123',
client_id: 'customer-456'
)
puts "#{invoice.invoice_id} #{invoice.payment_url}"
use paymos::{CreateInvoiceRequest, PaymosClient};
let paymos = PaymosClient::new(
std::env::var("PAYMOS_API_KEY")?,
std::env::var("PAYMOS_API_SECRET")?,
)?;
let invoice = paymos
.invoices()
.create(&CreateInvoiceRequest {
project_id: "prj_xxxxxxxxxxxx".to_owned(),
amount: "100.00".to_owned(),
currency: "USD".to_owned(),
external_order_id: "order-123".to_owned(),
network: None,
allow_multiple_payments: None,
customer_fee_percent: None,
client_id: Some("customer-456".to_owned()),
})
.await?;
println!("{} {}", invoice.invoice_id, invoice.payment_url);
Ответ (201 Created / 200 при идемпотентном совпадении)
{
"invoice_id": "inv_5CcyDYmMUGtzYL10q0Iimr",
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"status": "awaiting_client",
"is_final": false,
"is_test": true,
"payment_url": "https://checkout.paymos.io/invoice/inv_5CcyDYmMUGtzYL10q0Iimr",
"order": {
"external_id": "order-12345",
"client_id": "customer-67890",
"amount": "50.00",
"currency": "USD"
},
"created_at": 1743594000,
"updated_at": 1743594000,
"expires_at": 1743597600
}
Поля ответа
| Поле | Тип | Описание |
|---|---|---|
invoice_id |
string | Идентификатор инвойса с префиксом (inv_...) |
project_id |
string | Идентификатор проекта с префиксом (prj_...) |
status |
string | Статус инвойса (snake_case — см. Статусы инвойса) |
is_final |
boolean | true, когда инвойс достиг терминального состояния |
is_test |
boolean | true для инвойсов в тестовом режиме |
payment_url |
string | URL оплаты: ссылка на бота Paymos у проекта «Telegram-бот», платёжная страница у любого другого |
order |
object | Данные заказа со стороны мерчанта |
order.external_id |
string | ID заказа мерчанта |
order.client_id |
string? | ID клиента мерчанта |
order.amount |
string | Запрошенная сумма. У фиатного инвойса знаков после точки столько, сколько ISO 4217 отводит этой валюте: "100.00" для USD, "23" для JPY, "1.500" для KWD — таблица в разделе Точность фиатной суммы. У токена такого стандарта нет, поэтому крипто-инвойс отдаёт сумму без нулей в конце: "10.00" USDT вернётся как "10". Нули только дописывают, не округляют: сумму, которая не укладывается в минорную единицу, отклоняют ещё при создании. В списке поле amount работает по тому же правилу |
order.currency |
string | Фиатная валюта или символ криптоактива, указанный мерчантом |
order.network |
string? | Сеть, запрошенная для крипто-инвойса с фиксированной сетью |
payment |
object? | Детали платежа после выбора токена и сети |
created_at |
integer | Unix timestamp, секунды |
updated_at |
integer | Unix timestamp, секунды |
expires_at |
integer | Unix timestamp, секунды. Задаётся при создании, поэтому есть у любого инвойса в любом статусе. Когда токен и сеть подтверждены, срок продлевается до полного окна оплаты от этого момента и назад уже не сдвигается. После подтверждения перечитайте поле, а не держите значение с момента создания |
completed_at |
integer? | Время завершения |
paid_late_at |
integer? | Отметка поздней оплаты (Unix-время, секунды): инвойс уже закрылся как expired или underpaid, а деньги в его токене после этого набрали ожидаемую сумму — в пределах допуска по недоплате, сохранённого в инвойсе. status и is_final сохраняют терминальный смысл — отметка идёт рядом с ними, а payment.paid по-прежнему считает только своевременные переводы. Пока отметки нет, поля в ответе нет; null читайте так же — отметки нет |
Статусы инвойса
У каждого инвойса есть поле status. Одно и то же значение приходит в ответе API, в потоке SSE на странице оплаты и в вебхуке — источник один. Таблица ниже — справочник по статусам: когда статус наступает, терминальный он или нет и при каком условии возникает. Схему переходов и оба потока создания смотрите в разделе Платёжный процесс.
Терминальный статус — конечный: инвойс на нём закрывается и больше не меняется. Терминальных пять — paid, paid_over, underpaid, expired, cancelled.
| Статус | Терминальный | Описание |
|---|---|---|
awaiting_client |
Нет | Стартовый статус любого инвойса. Клиент ещё не выбрал токен/сеть, поэтому адрес для оплаты не назначен. Отменить инвойс можно только в этом статусе |
awaiting_payment |
Нет | Адрес назначен, ожидает входящий перевод |
confirming |
Нет | Перевод виден в блокчейне и набирает число подтверждений, положенное его сумме |
underpaid_waiting |
Нет | Получена частичная оплата, ожидает остаток (только при allow_multiple_payments = true) |
paid |
Да | Ожидаемая сумма получена полностью — либо в пределах допуска по недоплате, заданного в проекте |
paid_over |
Да | Полученная сумма превышает ожидаемую (зачисляется полностью) |
underpaid |
Да | Инвойс закрылся с недоплатой: либо истёк, не добрав до ожидаемой суммы, либо единственный платёж не покрыл её при allow_multiple_payments = false |
expired |
Да | Срок инвойса вышел: платежа не было либо токен и сеть так и не выбрали |
cancelled |
Да | Инвойс отменён мерчантом (только из статуса awaiting_client) |
Идемпотентность
Всегда передавайте external_order_id при создании инвойса. Повторный вызов с тем же external_order_id возвращает существующий инвойс — дубля не будет. HTTP-статус: 200 OK для идемпотентного совпадения, 201 Created для нового инвойса.
Повтор должен быть тем же запросом. Если amount, currency или network отличаются от инвойса, уже созданного под этим id, или запрос подписан ключом другого режима, API ответит 409 с кодом invoice_idempotency_conflict и ничего не создаст. Id уникален в проекте сразу для тестового и рабочего режимов: реальный заказ не может взять id, который уже был в тестах.
Это страховка от ненадёжной сети, сбоя фоновых процессов и повторных запросов. Один и тот же id — на одно бизнес-событие; новый id — на новое событие.
Используйте UUID v4 или свой ID заказа — что угодно стабильное и уникальное для каждого бизнес-события. Случайный повтор ID второго инвойса не создаст: на тот же запрос вернётся уже созданный (200 OK), на отличающийся — 409. ID с префиксом wgt_ зарезервированы за инвойсами Low-Code SDK и отклоняются с field_invalid_format.
Ошибки
См. Коды ошибок — полный каталог. URI в поле type каждой ошибки ведёт сразу на нужную строку.