# Создание инвойса

> Создание платёжного инвойса для приёма криптовалютных платежей. Два потока: прямой крипто (токен указан) и фиатный (клиент выбирает токен на странице оплаты).

`POST /v1/invoices`

:::endpoint
POST /v1/invoices
:::

**API-ключ:** `Payment`

## Тело запроса

| Параметр | Тип | Обязателен | Описание |
|----------|-----|------------|----------|
| `project_id` | string | Да | Идентификатор проекта с префиксом (`prj_...`). API-ключ привязан к мерчанту; укажите, какой проект получит инвойс |
| `amount` | string | Да | Сумма платежа строкой с десятичной точкой (например, `"50.00"`) |
| `currency` | string | Да | Код фиатной валюты (`USD`, `EUR` и т.д.) или символ криптоактива. Полный список — [Поддерживаемые валюты](/ru/docs/supported-currencies.md). У криптовалюты на вкладке «Валюты» проекта должна остаться включённой хотя бы одна сеть; если выключены все, запрос вернёт `400 currency_not_enabled_for_project` |
| `network` | string | Условно | Код сети блокчейна — `TRC20`, `ERC20`, `BEP20`, `POLYGON`, `ARBITRUM`, `OPTIMISM`, `BASE`, `TON`, `AVAX`, `SOL`, `NEAR`, `SUI` или `PLASMA` (см. [Поддерживаемые валюты](/ru/docs/supported-currencies.md)). Нужна, если вы хотите зафиксировать крипто-инвойс на конкретной сети. Сеть, выключенную в проекте, 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`.

См. [Платёжный процесс → Два потока создания](/ru/docs/payment-flow.md#two-creation-flows) — полный жизненный цикл, начальные статусы и как фиксируется курс.

## Пример запроса — фиатный поток

Сумма в фиате; клиент выбирает токен на странице оплаты, курс фиксируется в момент выбора.

```json
{
  "project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
  "amount": "50.00",
  "currency": "USD",
  "external_order_id": "order-12345",
  "client_id": "customer-67890"
}
```

## Пример запроса — прямой крипто

Токен и сеть фиксированы при создании; адрес назначается сразу.

```json
{
  "project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
  "amount": "50.00",
  "currency": "USDT",
  "network": "TRC20",
  "external_order_id": "order-12345",
  "client_id": "customer-67890"
}
```

## Примеры кода

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

```curl
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"
```

```javascript
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);
```

```python
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
<?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'];
```

```go
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 }
```

```csharp
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}");
```

```java
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());
```

```ruby
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}"
```

```rust
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 при идемпотентном совпадении)

```json
{
  "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 — см. [Статусы инвойса](/ru/docs/invoices/get.md#invoice-statuses)) |
| `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 — таблица в разделе [Точность фиатной суммы](/ru/docs/invoices/create.md#fiat-amount-precision). У токена такого стандарта нет, поэтому крипто-инвойс отдаёт сумму без нулей в конце: `"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 на странице оплаты и в вебхуке — источник один. Таблица ниже — справочник по статусам: когда статус наступает, терминальный он или нет и при каком условии возникает. Схему переходов и оба потока создания смотрите в разделе [Платёжный процесс](/ru/docs/payment-flow.md).

**Терминальный** статус — конечный: инвойс на нём закрывается и больше не меняется. Терминальных пять — `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 — на новое событие.

:::tip
Используйте UUID v4 или свой ID заказа — что угодно стабильное и уникальное для каждого бизнес-события. Случайный повтор ID второго инвойса не создаст: на тот же запрос вернётся уже созданный (`200 OK`), на отличающийся — `409`. ID с префиксом `wgt_` зарезервированы за инвойсами Low-Code SDK и отклоняются с `field_invalid_format`.
:::

## Ошибки

См. [Коды ошибок](/ru/docs/errors/codes.md) — полный каталог. URI в поле `type` каждой ошибки ведёт сразу на нужную строку.
