API Overview
Phoenix Pay REST API conventions, authentication, error handling, and pagination.
Base URL
https://pay.phoenixverse.io/apiAll endpoint paths in this documentation are relative to this base URL.
Authentication
Every API request (except /api/.well-known/signing-key) must include three headers:
| Header | Description |
|---|---|
X-Key-Id | Your API key ID |
X-Timestamp | Current Unix timestamp (seconds) |
X-Signature | Base64-encoded Ed25519 signature of "{timestamp}.{body}" |
curl https://pay.phoenixverse.io/api/deposits \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: <signature>" \
-H "Content-Type: application/json"See the Authentication guide for signing examples in Node.js, Python, Elixir, and Go.
Request Format
- Content-Type:
application/jsonfor all POST requests - Character encoding: UTF-8
- Amounts: Integer cents (smallest currency unit).
5000= 50.00.
{
"reference_id": "order-12345",
"amount": 5000,
"currency": "USDT",
"channel": "crypto_address"
}Amounts are always in the smallest currency unit (cents). 5000 means 50.00 in the given currency. Decimal strings like "50.00" are accepted as a convenience and auto-converted to 5000, but integer format is recommended.
Response Format
Phoenix Pay has two response patterns:
Create Responses (Action-Based)
When you create a deposit or withdrawal, you get an action telling your application what to do next:
{
"intent_id": "01912e4a-7b3c-7def-8a90-1234567890ab",
"action": "redirect",
"url": "https://checkout.chapa.co/checkout/payment/abc123"
}See Payment Lifecycle — Actions for all action types.
GET Responses (Full Resource)
When you retrieve a payment, you get the full intent object with the latest attempt details:
{
"id": "01912e4a-7b3c-7def-8a90-1234567890ab",
"reference_id": "order-12345",
"display_ref": "DEP-20260311-A1B2C3",
"type": "deposit",
"status": "completed",
"amount": 5000,
"currency": "USDT",
"channel": "crypto_address",
"payment_method": null,
"error_code": null,
"error_detail": null,
"psp": "nowpayments",
"psp_external_id": "np_5012345678",
"inserted_at": "2026-03-11T12:30:00.000000Z",
"updated_at": "2026-03-11T12:45:00.000000Z"
}List Responses (Paginated)
List endpoints return a data array with a meta pagination object:
{
"data": [ ... ],
"meta": {
"page": 1,
"limit": 25,
"total": 142,
"total_pages": 6,
"has_next": true,
"has_prev": false
}
}Pagination
List endpoints use offset-based pagination:
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
page | integer | 1 | — | Page number (1-indexed) |
limit | integer | 25 | 100 | Results per page |
The meta object contains:
| Field | Type | Description |
|---|---|---|
page | integer | Current page number |
limit | integer | Results per page |
total | integer | Total matching records |
total_pages | integer | Total pages |
has_next | boolean | Whether there is a next page |
has_prev | boolean | Whether there is a previous page |
Error Format
{
"error": "missing required parameter: reference_id"
}PSP errors may include an additional message field:
{
"error": "insufficient_liquidity",
"message": "PSP does not support this currency in your region"
}Common HTTP Status Codes
| Status | Meaning |
|---|---|
200 OK | Request succeeded |
201 Created | Deposit or withdrawal created successfully |
400 Bad Request | Missing or invalid request parameters |
401 Unauthorized | Missing or invalid request signature, or stale timestamp |
403 Forbidden | Authenticated but insufficient permissions |
404 Not Found | Resource does not exist or belongs to a different tenant |
409 Conflict | Duplicate reference_id — the existing intent_id is returned |
422 Unprocessable Entity | PSP rejected the request |
429 Too Many Requests | Rate limit exceeded |
500 Internal Server Error | Unexpected server error |
Rate Limiting
| Endpoint Type | Limit |
|---|---|
Create endpoints (POST) | 100 requests/minute per tenant |
Read endpoints (GET) | 300 requests/minute per tenant |
| Webhook endpoints | Unlimited (PSP-initiated) |
Rate limits are per tenant (organization), not per API key.