Skip to content

API

On this page

Create

Create an invoice to accept a crypto payment. Two flows: direct crypto (token and network fixed) and fiat (customer picks the token on the hosted checkout).

POST/v1/invoices

API key: Payment

Request body

Parameter Type Required Description
project_id string Yes Prefixed project identifier (prj_...). API key is merchant-scoped; specify which project receives the invoice
amount string Yes Payment amount as a decimal string (e.g. "50.00")
currency string Yes Fiat currency code (USD, EUR, etc.) or stablecoin symbol. See Supported Currencies for the full list
network string Conditional Blockchain network code — TRC20, ERC20, BEP20, POLYGON, ARBITRUM, OPTIMISM, BASE, TON, AVAX, SOL, NEAR, SUI, or PLASMA (see Supported Currencies). Required when you want to lock a crypto invoice to a specific network
external_order_id string Yes Your unique order identifier (max 128 chars), unique per project. Makes invoice creation idempotent: if an invoice with this external_order_id already exists in the project, you get that existing invoice back with 200 OK — not a duplicate, not an error
client_id string No Your customer identifier (max 200 chars). Tags the invoice to a customer of yours, which lets the per-customer limit on simultaneously open invoices apply
allow_multiple_payments boolean No Whether the invoice accepts multiple partial payments. Default: true
customer_fee_percent integer No Customer-facing fee percentage (0-100). Overrides the project-level setting for this invoice

Fiat amount precision

For fiat-flow invoices, amount must fit the minor unit of currency.

Minor units Currencies
0 decimals JPY, KRW, VND, CLP
2 decimals 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, VEF
3 decimals BHD, KWD

Examples: 23.34 is valid for USD; 23.345 is rejected for USD; 23 is valid for JPY; 23.01 is rejected for JPY.

Flow determination

The flow is determined by which fields you provide. currency decides whether the amount is priced in a token or in fiat; network decides whether the chain is fixed up front or chosen by the customer.

currency network Flow What the customer still chooses
stablecoin (USDT, USDC, …) a network (TRC20, ERC20, …) Direct cryptoamount is the token amount, on a fixed chain Nothing — token and network are locked
stablecoin (USDT, USDC, …) Crypto flowamount is the token amount, chain open The network to pay on
fiat (USD, EUR, …) Fiat flowamount is a fiat amount The token and the network

Every invoice is created in awaiting_client — no payment address exists yet. The customer's choice on the hosted checkout is what assigns the address and moves the invoice to awaiting_payment. In the direct-crypto case there is nothing left to choose, so the checkout confirms the pre-set token and network and assigns the address in one step. For the fiat flow, the fiat-to-crypto rate is locked at that moment and returned as payment.exchange_rate.

See Payment flow → Two creation flows for the full lifecycle, initial statuses, and FX-rate locking semantics.

Example request — fiat flow

Amount is in fiat; the customer picks a token on the hosted page and the rate locks at selection time.

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

Example request — direct crypto

Token + network are fixed at creation; an address is assigned immediately.

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

Code examples

Each tab uses the official SDK for that language. Signing, serialization, safe retries, and typed errors are handled by the SDK.

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

Response (201 Created / 200 if idempotent match)

{
  "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
}

Response fields

Field Type Description
invoice_id string Prefixed invoice identifier (inv_...)
project_id string Prefixed project identifier (prj_...)
status string Invoice status (snake_case — see Invoice Statuses)
is_final boolean Whether the invoice is in a terminal state
is_test boolean true for sandbox invoices
payment_url string Hosted checkout page URL
order object Merchant-facing order data
order.external_id string Merchant order ID
order.client_id string? Merchant customer ID
order.amount string Requested amount
order.currency string Fiat currency code or crypto asset symbol requested by the merchant
order.network string? Requested network for direct crypto invoices
payment object? Payment details — present once the token and network are selected. Holds the deposit address, the expected crypto amount, the locked rate, and per-transfer progress. See Get Invoice → Payment fields for the full sub-field reference
created_at integer Unix timestamp, seconds
updated_at integer Unix timestamp, seconds
expires_at integer Unix timestamp, seconds. Set at creation, so it is present on every invoice in every status
completed_at integer? Completion timestamp

Invoice statuses

Every invoice reports a status string. The same value drives the merchant API response, the SSE checkout stream, and the webhook payload — there is one source of truth. The table below is the per-status reference: when the value appears, whether it is terminal, and the precise condition behind it. For the transition diagram and the two creation flows, see Payment flow.

A terminal status is final: the invoice settles there and never moves again. Five statuses are terminal — paid, paid_over, underpaid, expired, cancelled.

Status Terminal When it applies
awaiting_client No The opening status for every invoice. No token or network is chosen yet, so no deposit address exists. Cancellation is allowed only here
awaiting_payment No A token, network, and deposit address are locked in. Paymos is watching the address for an incoming transfer
confirming No A transfer landed on-chain and is accruing the confirmations required for its amount tier
underpaid_waiting No Less than the expected amount has cleared and the invoice stays open for the rest. Reached only when allow_multiple_payments is true
underpaid Yes The invoice closed short — either it expired below the expected amount, or a single payment fell short with allow_multiple_payments set to false
expired Yes The timer ran out with nothing received, or the fiat-flow token-selection window lapsed before a token was picked
cancelled Yes The merchant cancelled the invoice while it was still in awaiting_client

Idempotency

Always send external_order_id on invoice creation. A retry of the same call with the same external_order_id returns the existing invoice — never a duplicate. The response status is 200 OK for an idempotent match, 201 Created for a fresh invoice.

This is your safety net against flaky networks, crashed workers, and retry loops. Use the same id for the same business event; pick a new id for a new event.

Use a UUID v4 or your own order ID — anything stable and unique per business event. If you reuse an ID by accident, you get that ID's existing invoice back (the 200 OK idempotent match above), not a new one.

Errors

See Error Codes for the full catalogue. The type URI in every error response deep-links to the matching row.