Payments
Most integrations use Checkout Sessions and never touch the Payments API directly. Use it when your platform renders its own UI (mobile, in-terminal, back-office) and already knows the method — the headless mode.
Create a Payment directly
POST /api/v1/payments
Authorization: Bearer sk_test_...
Idempotency-Key: your-deposit-id
{
"amount": "500.00",
"asset_code": "USDT.TRC20",
"payment_method": "crypto",
"country": "AE",
"customer": "cust_01J9ZR2KD7VE",
"metadata": { "crm_transaction_id": "DEP-448901" }
}201 Created
{
"id": "pay_01J9ZX8KT2M1",
"object": "payment",
"status": "requires_action",
"amount": "500.00",
"asset_code": "USDT.TRC20",
"payment_method": "crypto",
"country": "AE",
"customer": "cust_01J9ZR2KD7VE",
"provider_account": "pacc_01J8XN5QW9BC",
"next_action": {
"type": "crypto_deposit",
"deposit_address": "TWd4gh8N1tK9qkGf2vX7...",
"network": "TRC20",
"expires_at": "2026-07-05T11:41:22Z"
},
"metadata": { "crm_transaction_id": "DEP-448901" },
"created_at": "2026-07-05T09:41:22Z"
}Open-amount works the same as sessions: omit amount, send "open_amount": true. asset_code stays required and the two are mutually exclusive.
Card payments (Stripe)
Card is available via Stripe: send "payment_method": "card" with a fiat asset_code (USD and EUR to start). Card details are entered only on Stripe's own surfaces — PayXiro never sees the card number. Each Stripe provider account picks one of two presentations:
Redirect (default) — next_action.type is redirect; send the customer to redirect_url (Stripe's hosted Checkout page).
Embedded — next_action.type is provider_component, carrying { "provider": "stripe", "client_secret", "publishable_key" }. The hosted Cashier mounts Stripe's embedded Checkout in place; a headless client loads Stripe.js and calls stripe.initEmbeddedCheckout({ clientSecret }).
As always, credit only on the signed webhook — never on the browser return.
The Payment state machine
These are the only Payment states. requires_action waits on the customer or a provider action; under_review waits on a risk decision and resolves to processing or failed.
| Status | Meaning |
|---|---|
| created | Payment recorded, not yet routed to a provider action |
| requires_action | Waiting on the customer or a provider redirect/action (e.g. send crypto to an address) |
| under_review | Held for a risk decision (rule outcome or the manual review queue) |
| processing | Provider is settling; awaiting confirmation |
| succeeded | Terminal — funds confirmed; the credit-worthy signal |
| failed | Terminal — declined, routing_exhausted, or a risk block |
| cancelled | Terminal — withdrawn before processing |
| expired | Terminal — the action window elapsed with no completion |
Legal transitions: created → requires_action | under_review | processing | failed | cancelled | expired; requires_action → under_review | processing | cancelled | expired; under_review → processing | failed; processing → succeeded | failed. Every transition is recorded on the transaction timeline and emitted as a payment.* event.
Manual methods: submit-form
For manual methods (e.g. manual bank), post the dynamic-form values the method defines:
POST /api/v1/payments/{id}/submit-form
Authorization: Bearer sk_test_...The values are validated against the method's form_definition, stored as form_values, and the Payment transitions requires_action → under_review. This call is not money-creating (no Idempotency-Key) and is idempotent on state. Fetch the method's form definition first — fields are data, not code.
Poll status (reconciliation fallback)
If a webhook is delayed or missed, reconcile by fetching the Payment:
GET /api/v1/payments/{id}
Authorization: Bearer sk_test_...Returns 404 (never 403) if the Payment is unknown or belongs to another tenant. Poll as a scheduled sweep over Payments still pending after N minutes — not as the primary channel. When a poll shows succeeded, run the same idempotent credit path the webhook uses (keyed on payment.id), so a poll and a late webhook cannot double-credit.
See Webhooks for the source-of-truth channel.