Phoenix Pay
API Reference

Deposits

API reference for creating and managing deposits.


Create Deposit

POST /api/deposits

Creates 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

FieldTypeRequiredDescription
reference_idstringYesYour unique identifier for this payment. Must be unique within your tenant.
amountinteger or stringYesAmount in cents (smallest currency unit). 5000 = 50.00. Decimal strings like "50.00" are also accepted and auto-converted to cents.
currencystringYesCurrency code (e.g., "USDT", "ETB", "BTC").
channelstringYesPayment channel. Determines the flow type. See Channels.
methodstringNoSub-method within the channel (e.g., "telebirr" for ussd_push).
fieldsobjectNoAdditional 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

ChannelPSPFlowWhen to Use
crypto_addressNOWPaymentsUser sends crypto to a generated addressCrypto deposits (USDT, BTC, ETH, etc.)
checkoutChapaUser is redirected to hosted checkout pageFiat deposits (ETB) — simplest integration
ussd_pushChapaPSP sends USSD push to user's phoneTelebirr deposits — better mobile UX. Requires method: "telebirr".
otpChapaMulti-step: initial request → OTP verificationTelebirr direct charge. Requires method: "telebirr" and fields.phone.

Examples

Create crypto deposit
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"
201 Created
{
  "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.

Create fiat deposit
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"
201 Created
{
  "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.

Create USSD push deposit
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"
201 Created
{
  "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.

Create OTP deposit
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"
201 Created
{
  "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

StatusErrorCause
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.
422PSP-specific errorThe PSP rejected the request. Response includes error code and message.
409 Duplicate response
{
  "error": "duplicate_reference",
  "intent_id": "01912e4a-7b3c-7def-8a90-1234567890ab"
}

Multi-Step Flows

POST /api/attempts/:attempt_id/step

Submit user input for a multi-step payment flow (e.g., OTP verification). Only valid when the current attempt has action: "collect".

Request Body

FieldTypeRequiredDescription
inputobjectYesUser-supplied data. Key names depend on collect.type (e.g., {"otp": "123456"}).
Submit OTP
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:

200 OK — payment processing
{
  "intent_id": "01912e4d-ae6f-...",
  "action": "await",
  "message": "Payment is being processed"
}

Or another collect step, or action: "completed" if the payment finished.

Errors

StatusErrorCause
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/:id

Retrieve a deposit by its Phoenix Pay UUID.

Get deposit by ID
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>"
200 OK
{
  "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

FieldTypeDescription
idstring (UUID v7)Unique intent identifier
reference_idstringYour reference ID from the create request
display_refstringAuto-generated human-readable reference
typestringAlways "deposit"
statusstringCurrent status: created, pending, completed, failed, expired
amountintegerAmount in cents (5000 = 50.00)
currencystringCurrency code
channelstringThe channel used (e.g., "crypto_address", "checkout")
payment_methodstring or nullSub-method if specified (e.g., "telebirr")
error_codestring or nullError code if failed
error_detailstring or nullHuman-readable failure reason
pspstring or nullPSP handling the latest attempt
psp_external_idstring or nullPSP's own transaction ID
checkout_urlstring or nullRedirect URL (for checkout channel)
pay_addressstring or nullCrypto address (for crypto_address channel)
pay_currencystring or nullSpecific crypto currency code from PSP
pay_amountnumber or nullExact amount to send in pay_currency
expires_atstring (ISO 8601) or nullWhen the payment window expires
inserted_atstring (ISO 8601)When the intent was created
updated_atstring (ISO 8601)When the intent was last updated

Errors

StatusErrorCause
404"not_found"No deposit with this ID exists for your tenant

Get Deposit by Reference

GET /api/deposits/ref/:reference_id

Retrieve a deposit using your original reference_id. Useful when you need to look up a payment using your own system's identifier.

Get deposit by reference ID
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/deposits

Retrieve a paginated list of deposits for your tenant.

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger25Results per page (max 100)
statusstringFilter by status: created, pending, completed, failed, expired
currencystringFilter by currency code
fromstring (date)Filter by creation date on or after (ISO 8601 date, e.g., 2026-03-01)
tostring (date)Filter by creation date on or before
searchstringSearch by reference ID (partial match)

Both from and to must be provided for date filtering to take effect.

List completed deposits
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

200 OK
{
  "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/events

Returns the full timeline for a deposit: all attempts and inbound webhook events. See Payment Lifecycle — Intent Timeline for the response format.