Webhook API
API reference for webhook verification and payload format.
Webhook API
Phoenix Pay delivers outbound webhooks to your configured callback URL whenever a payment status changes. This page covers the API endpoints and payload format. For a comprehensive guide on implementation, see Webhooks.
Public Signing Key
GET /api/.well-known/signing-keyReturns the Ed25519 public key used to sign all outbound webhooks. This is a public endpoint -- no authentication required.
curl https://pay.phoenixverse.io/api/.well-known/signing-key{
"algorithm": "Ed25519",
"public_key": "MCowBQYDK2VwAyEAx1Fz..."
}| Field | Type | Description |
|---|---|---|
algorithm | string | Always "Ed25519" |
public_key | string | Base64-encoded Ed25519 public key |
Cache this key in your application. It changes only during key rotation, which is an infrequent, coordinated operation.
Webhook Delivery
Phoenix Pay sends an HTTP POST request to your tenant's configured callback_url whenever a payment transitions to a new status.
HTTP Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Phoenix-Pay-Signature | Base64-encoded Ed25519 signature |
X-Phoenix-Pay-Timestamp | Unix timestamp (seconds) when the signature was created |
Payload Format
{
"event": "payment.status_changed",
"payment_id": "01912e4a-7b3c-7def-8a90-1234567890ab",
"reference_id": "order-12345",
"type": "deposit",
"status": "settled",
"amount": "50.00",
"received_amount": "50.00",
"currency": "USDT",
"psp": "nowpayments",
"timestamp": "2026-03-11T12:45:00Z"
}Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | Event type. Currently always "payment.status_changed". |
payment_id | string | Phoenix Pay payment UUID (UUID v7) |
reference_id | string | Your original reference ID from the create request |
type | string | Payment type: "deposit" or "payout" |
status | string | The new payment status. See Payment Lifecycle for all values. |
amount | string or null | The originally requested amount (decimal string) |
received_amount | string or null | The actual amount received or sent (may differ for partial payments) |
currency | string | Currency code (e.g., "USDT", "ETB") |
psp | string | The PSP that processed the payment ("nowpayments" or "chapa") |
timestamp | string | ISO 8601 timestamp of when Phoenix Pay generated this webhook |
Events
Currently, Phoenix Pay emits a single event type:
| Event | Description | Trigger |
|---|---|---|
payment.status_changed | A payment's status has changed | Any status transition on a deposit or payout |
This event fires for every status transition, including:
awaiting_payment-- deposit created, waiting for customerprocessing-- payment detected, being processedpartial-- partial amount received (crypto)settled-- payment completed successfullyfailed-- payment failedexpired-- payment window expiredcancelled-- payment cancelled
You will typically receive multiple webhooks for a single payment as it progresses through states (e.g., awaiting_payment then processing then settled).
Signature Verification
Every webhook is signed using the Ed25519 private key. Verify by:
Extract the signature and timestamp
Read X-Phoenix-Pay-Signature and X-Phoenix-Pay-Timestamp from the request headers.
Reconstruct the signing input
Concatenate the timestamp and raw body with a period separator:
{timestamp}.{raw_json_body}Verify the Ed25519 signature
Use the public key from /api/.well-known/signing-key to verify the base64-decoded signature against the signing input.
Always verify using the raw request body as a string. Do not parse the JSON and re-serialize it -- JSON key ordering and whitespace are not guaranteed to be preserved, which will cause verification to fail.
For complete code examples in Node.js, Python, Elixir, and Go, see the Webhook Verification Guide.
Retry Behavior
| Attempt | Delay | Description |
|---|---|---|
| 1st | Immediate | Initial delivery attempt |
| 2nd | 5 seconds | First retry after failure |
| 3rd | 30 seconds | Second retry |
| 4th (final) | 3 minutes | Final retry |
A delivery is considered successful if your endpoint responds with any 2xx HTTP status code within 10 seconds.
A delivery is considered failed if:
- Your endpoint returns a non-2xx status code
- Your endpoint does not respond within 10 seconds
- The connection cannot be established
After all retry attempts are exhausted, the webhook delivery is abandoned. The payment's callback_delivered field remains false. You can use the polling endpoints to check for missed updates.
Configuring Your Callback URL
Set your webhook callback URL via the tenant configuration endpoint:
curl -X PUT https://pay.phoenixverse.io/api/config \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://myapp.example.com/webhooks/phoenix-pay"
}'Your callback URL must use HTTPS. Phoenix Pay will not deliver webhooks to HTTP endpoints in production.
Requirements for Your Endpoint
- Must be publicly accessible over HTTPS
- Must respond with a 2xx status code within 10 seconds
- Must accept POST requests with
Content-Type: application/json - Should be idempotent (may receive the same event more than once)