Deposits
API reference for creating and managing deposits.
Create Deposit
POST /api/depositsCreates a new deposit intent and initiates the first attempt with the selected PSP. The response tells your application what to do next — see Actions.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
reference_id | string | Yes | Your unique identifier for this payment. Must be unique within your tenant. |
amount | integer or string | Yes | Amount in cents (smallest currency unit). 5000 = 50.00. Decimal strings like "50.00" are also accepted and auto-converted to cents. |
currency | string | Yes | Currency code (e.g., "USDT", "ETB", "BTC"). |
channel | string | Yes | Payment channel. Determines the flow type. See Channels. |
method | string | No | Sub-method within the channel (e.g., "telebirr" for ussd_push). |
fields | object | No | Additional fields required by the channel (e.g., {"phone": "+251911234567"}). |
Amount format: 5000 and "5000" both mean 50.00 (5000 cents). But "50.00" is also accepted as a convenience — it's auto-converted to 5000 cents. Pick one format and be consistent.
Channels
| Channel | PSP | Flow | When to Use |
|---|---|---|---|
crypto_address | NOWPayments | User sends crypto to a generated address | Crypto deposits (USDT, BTC, ETH, etc.) |
checkout | Chapa | User is redirected to hosted checkout page | Fiat deposits (ETB) — simplest integration |
ussd_push | Chapa | PSP sends USSD push to user's phone | Telebirr deposits — better mobile UX. Requires method: "telebirr". |
otp | Chapa | Multi-step: initial request → OTP verification | Telebirr direct charge. Requires method: "telebirr" and fields.phone. |
Examples
TIMESTAMP=$(date +%s)
BODY='{"reference_id":"order-12345","amount":5000,"currency":"USDT","channel":"crypto_address"}'
curl -X POST https://pay.phoenixverse.io/api/deposits \
-H "Content-Type: application/json" \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: <signature-of-$TIMESTAMP.$BODY>" \
-d "$BODY"{
"intent_id": "01912e4a-7b3c-7def-8a90-1234567890ab",
"action": "await",
"message": "Send payment to TXrk4d5x7Bj3e6g7Y8Zw9AbCdEf1234567",
"pay_address": "TXrk4d5x7Bj3e6g7Y8Zw9AbCdEf1234567",
"pay_currency": "usdttrc20",
"pay_amount": 50.0,
"expires_at": "2026-03-11T14:30:00Z"
}Display pay_address and pay_amount to your user. They send exactly pay_amount in pay_currency to pay_address before expires_at.
TIMESTAMP=$(date +%s)
BODY='{"reference_id":"order-12346","amount":100000,"currency":"ETB","channel":"checkout"}'
curl -X POST https://pay.phoenixverse.io/api/deposits \
-H "Content-Type: application/json" \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: <signature-of-$TIMESTAMP.$BODY>" \
-d "$BODY"{
"intent_id": "01912e4b-8c4d-7def-9b01-2345678901bc",
"action": "redirect",
"url": "https://checkout.chapa.co/checkout/payment/abc123xyz"
}Redirect the user to url. They complete payment on Chapa's hosted checkout page.
TIMESTAMP=$(date +%s)
BODY='{"reference_id":"order-12347","amount":100000,"currency":"ETB","channel":"ussd_push","method":"telebirr","fields":{"phone":"+251911234567"}}'
curl -X POST https://pay.phoenixverse.io/api/deposits \
-H "Content-Type: application/json" \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: <signature-of-$TIMESTAMP.$BODY>" \
-d "$BODY"{
"intent_id": "01912e4c-9d5e-7def-ac12-3456789012cd",
"action": "await",
"message": "Payment request sent"
}The user receives a USSD prompt on their phone. Wait for the webhook — no redirect needed.
TIMESTAMP=$(date +%s)
BODY='{"reference_id":"order-12348","amount":100000,"currency":"ETB","channel":"otp","method":"telebirr","fields":{"phone":"+251911234567"}}'
curl -X POST https://pay.phoenixverse.io/api/deposits \
-H "Content-Type: application/json" \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: <signature-of-$TIMESTAMP.$BODY>" \
-d "$BODY"{
"intent_id": "01912e4d-ae6f-7def-bd23-4567890123de",
"action": "collect",
"attempt_id": "01912e4e-bf70-7def-ce34-5678901234ef",
"collect": {
"type": "otp",
"hint": "Enter the OTP sent to your phone"
}
}Show an OTP input form, then submit via POST /api/attempts/:attempt_id/step. See Multi-Step Flows.
Errors
| Status | Error | Cause |
|---|---|---|
400 | "missing required parameter: reference_id" | A required field is missing or empty |
400 | "missing required parameter: channel" | The channel field is required |
409 | "duplicate_reference" | The reference_id already exists for your tenant. Response includes intent_id of the existing intent. |
422 | PSP-specific error | The PSP rejected the request. Response includes error code and message. |
{
"error": "duplicate_reference",
"intent_id": "01912e4a-7b3c-7def-8a90-1234567890ab"
}Multi-Step Flows
POST /api/attempts/:attempt_id/stepSubmit user input for a multi-step payment flow (e.g., OTP verification). Only valid when the current attempt has action: "collect".
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
input | object | Yes | User-supplied data. Key names depend on collect.type (e.g., {"otp": "123456"}). |
TIMESTAMP=$(date +%s)
BODY='{"input":{"otp":"123456"}}'
curl -X POST https://pay.phoenixverse.io/api/attempts/01912e4e-bf70.../step \
-H "Content-Type: application/json" \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: <signature-of-$TIMESTAMP.$BODY>" \
-d "$BODY"Response
The response has the same action-based format as the create response:
{
"intent_id": "01912e4d-ae6f-...",
"action": "await",
"message": "Payment is being processed"
}Or another collect step, or action: "completed" if the payment finished.
Errors
| Status | Error | Cause |
|---|---|---|
404 | "not_found" | Invalid attempt ID |
409 | "attempt_not_awaiting_input" | The attempt is not in a state that accepts input |
Get Deposit
GET /api/deposits/:idRetrieve a deposit by its Phoenix Pay UUID.
curl https://pay.phoenixverse.io/api/deposits/01912e4a-7b3c-7def-8a90-1234567890ab \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: <signature>"{
"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",
"checkout_url": null,
"pay_address": "TXrk4d5x7Bj3e6g7Y8Zw9AbCdEf1234567",
"pay_currency": "usdttrc20",
"pay_amount": 50.0,
"expires_at": "2026-03-11T14:30:00Z",
"inserted_at": "2026-03-11T12:30:00.000000Z",
"updated_at": "2026-03-11T12:45:00.000000Z"
}The GET response shape is different from the create response. Create returns an action telling you what to do. GET returns the full intent with the latest attempt's PSP details.
Response Fields
| Field | Type | Description |
|---|---|---|
id | string (UUID v7) | Unique intent identifier |
reference_id | string | Your reference ID from the create request |
display_ref | string | Auto-generated human-readable reference |
type | string | Always "deposit" |
status | string | Current status: created, pending, completed, failed, expired |
amount | integer | Amount in cents (5000 = 50.00) |
currency | string | Currency code |
channel | string | The channel used (e.g., "crypto_address", "checkout") |
payment_method | string or null | Sub-method if specified (e.g., "telebirr") |
error_code | string or null | Error code if failed |
error_detail | string or null | Human-readable failure reason |
psp | string or null | PSP handling the latest attempt |
psp_external_id | string or null | PSP's own transaction ID |
checkout_url | string or null | Redirect URL (for checkout channel) |
pay_address | string or null | Crypto address (for crypto_address channel) |
pay_currency | string or null | Specific crypto currency code from PSP |
pay_amount | number or null | Exact amount to send in pay_currency |
expires_at | string (ISO 8601) or null | When the payment window expires |
inserted_at | string (ISO 8601) | When the intent was created |
updated_at | string (ISO 8601) | When the intent was last updated |
Errors
| Status | Error | Cause |
|---|---|---|
404 | "not_found" | No deposit with this ID exists for your tenant |
Get Deposit by Reference
GET /api/deposits/ref/:reference_idRetrieve a deposit using your original reference_id. Useful when you need to look up a payment using your own system's identifier.
curl https://pay.phoenixverse.io/api/deposits/ref/order-12345 \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: <signature>"The response format is identical to Get Deposit.
List Deposits
GET /api/depositsRetrieve a paginated list of deposits for your tenant.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 25 | Results per page (max 100) |
status | string | — | Filter by status: created, pending, completed, failed, expired |
currency | string | — | Filter by currency code |
from | string (date) | — | Filter by creation date on or after (ISO 8601 date, e.g., 2026-03-01) |
to | string (date) | — | Filter by creation date on or before |
search | string | — | Search by reference ID (partial match) |
Both from and to must be provided for date filtering to take effect.
curl "https://pay.phoenixverse.io/api/deposits?status=completed&page=1&limit=10" \
-H "X-Key-Id: your-key-id" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: <signature>"Response
{
"data": [
{
"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",
"checkout_url": null,
"pay_address": "TXrk4d5x7Bj3e6g7...",
"pay_currency": "usdttrc20",
"pay_amount": 50.0,
"expires_at": "2026-03-11T14:30:00Z",
"inserted_at": "2026-03-11T12:30:00.000000Z",
"updated_at": "2026-03-11T12:45:00.000000Z"
}
],
"meta": {
"page": 1,
"limit": 10,
"total": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
}Results are sorted by creation date, newest first.
Get Intent Timeline
GET /api/intents/:intent_id/eventsReturns the full timeline for a deposit: all attempts and inbound webhook events. See Payment Lifecycle — Intent Timeline for the response format.