Phoenix Pay
API Reference

Webhook API

API reference for webhook verification and payload format.


Public Signing Key

GET /api/.well-known/signing-key

Returns the Ed25519 public key used to sign all outbound webhooks. This is a public endpoint — no authentication required.

Fetch signing key
curl https://pay.phoenixverse.io/api/.well-known/signing-key
200 OK
{
  "algorithm": "Ed25519",
  "public_key": "MCowBQYDK2VwAyEAx1Fz..."
}
FieldTypeDescription
algorithmstringAlways "Ed25519"
public_keystringBase64-encoded Ed25519 public key

Cache this key in your application. It changes only during key rotation, which is infrequent.


Webhook Delivery

Phoenix Pay sends an HTTP POST to your tenant's callback_url whenever a payment transitions to a new status.

HTTP Headers

HeaderDescription
Content-Typeapplication/json
X-Phoenix-Pay-SignatureBase64-encoded Ed25519 signature
X-Phoenix-Pay-TimestampUnix timestamp (seconds) when the signature was created

Payload Format

Webhook payload
{
  "event": "payment.status_changed",
  "intent_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,
  "timestamp": "2026-03-11T12:45:00Z"
}

Payload Fields

FieldTypeDescription
eventstringAlways "payment.status_changed"
intent_idstringPhoenix Pay intent UUID (UUID v7)
reference_idstringYour original reference ID from the create request
display_refstringAuto-generated human-readable reference
typestring"deposit" or "withdrawal"
statusstringThe new status. See Payment Lifecycle.
amountintegerAmount in cents (5000 = 50.00)
currencystringCurrency code
channelstringThe channel used for this payment
payment_methodstring or nullSub-method if specified
error_codestring or nullError code (present when status is "failed")
error_detailstring or nullHuman-readable failure reason
timestampstringISO 8601 timestamp of when Phoenix Pay generated this webhook

Events

Currently, Phoenix Pay emits a single event type:

EventDescriptionTrigger
payment.status_changedA payment's status has changedAny status transition on a deposit or withdrawal

This fires for every transition:

  • createdpending — PSP accepted the request
  • pendingcompleted — payment succeeded
  • pendingfailed — payment failed
  • created / pendingexpired — payment window expired

You may receive multiple webhooks for a single payment as it progresses through states (e.g., createdpendingcompleted).


Signature Verification

Every webhook is signed. 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 — key ordering and whitespace may differ, causing verification to fail.

For complete verification code in Node.js, Python, Elixir, and Go, see the Webhook Verification Guide.


Retry Behavior

AttemptDelayDescription
1stImmediateInitial delivery
2nd5 secondsFirst retry
3rd30 secondsSecond retry
4th (final)3 minutesFinal retry

A delivery is successful if your endpoint responds with any 2xx HTTP status code within 10 seconds.

A delivery fails if your endpoint returns non-2xx, doesn't respond within 10 seconds, or is unreachable.

After all retries are exhausted, the webhook is abandoned. Use the polling endpoints to check for missed updates.


Configuring Your Callback URL

Set your webhook callback URL via the tenant configuration endpoint:

Set callback URL
TIMESTAMP=$(date +%s)
BODY='{"callback_url":"https://myapp.example.com/webhooks/phoenix-pay"}'

curl -X PUT https://pay.phoenixverse.io/api/config \
  -H "X-Key-Id: your-key-id" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Signature: <signature-of-$TIMESTAMP.$BODY>" \
  -H "Content-Type: application/json" \
  -d "$BODY"

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)

For PSP-facing webhooks, Phoenix Pay also treats duplicate provider deliveries as successful no-ops. If NOWPayments or Chapa sends the same webhook again, Phoenix Pay acknowledges it instead of re-processing the payment.