Overview
Webhooks allow your application to receive real-time notifications when events occur in Phoenix. When an order is created or its status changes, Phoenix can automatically send an HTTP POST request to your specified webhook endpoints.
Getting Started
1. Obtain Webhook API Key
Before setting up webhooks, you need to obtain a webhook API key from Phoenix:
Contact our support team at main@phoenix.market
Request webhook access for your integration
2. Define Your Webhook Endpoints
Your webhook endpoints should:
Accept POST requests
Return a 200
status code to acknowledge receipt
Process the webhook payload within 30 seconds
Validate the HMAC signature in the X-Webhook-Signature
header
Handle duplicate events gracefully (use id
for idempotency)
HMAC Signature
Phoenix provides HMAC signatures to help you verify the authenticity of webhook requests. The signature is calculated using your client_secret
and the payload of the request, allowing you to validate that the webhook came from Phoenix.
The signature is sent in the X-Webhook-Signature
header.
The signature’s timestamp is sent in the X-Webhook-Timestamp
header.
The signature is calculated using the HMAC-SHA256 algorithm.
Signature Verification Example
Here’s how to verify the webhook signature in your endpoint:
const crypto = require ( 'crypto' );
// In your webhook endpoint
app . post ( '/webhook' , ( req , res ) => {
const signature = req . headers [ 'x-webhook-signature' ]; // Or your custom header
const timestamp = req . headers [ 'x-webhook-timestamp' ];
const rawBody = req . body ; // Buffer, not parsed JSON!
const endpointSecret = 'Your-clientsecret-key'
// Create the signed payload string
const signedPayload = ` ${ timestamp } . ${ JSON . stringify ( rawBody ) } ` ;
// Compute the HMAC
const expectedSignature = crypto
. createHmac ( 'sha256' , endpointSecret )
. update ( signedPayload )
. digest ( 'hex' );
console . log ( expectedSignature );
console . log ( signature );
// Compare signatures (use timingSafeEqual in production)
if ( crypto . timingSafeEqual (
Buffer . from ( expectedSignature , 'hex' ),
Buffer . from ( signature , 'hex' )
)) {
// Signature is valid, process the event
console . log ( 'Webhook verified!' );
res . status ( 200 ). send ( 'Webhook verified!' );
} else {
console . log ( 'Invalid signature' );
// Invalid signature
res . status ( 400 ). send ( 'Invalid signature' );
}
});
3. Share Your Webhook Endpoint URLs
Once you’ve defined your webhook endpoints, you need to share the exact URLs with Phoenix:
Determine the full URL path where you want to receive webhooks (e.g., https://yourdomain.com/webhooks/phoenix-status-updates
)
Contact our support team at main@phoenix.market with your webhook endpoint URL
Phoenix will configure the system to send webhook events to your specified URL
Webhook Events
Each webhook event contains a unique id
field for deduplication. Implement idempotency using this id
as Phoenix may retry events if your endpoint doesn’t return a 200
status code or if the request fails.
Current events:
payment_link_created
payment_link_status_updated
order_created
order_status_updated
Event Payload Structure
This structure may change over time, so do stay up to date with the docs.
{
"event" : "" ,
"id" : "xxx" ,
"timestamp" : "1970-01-01T00:00:00.001Z" ,
"data" : {
"paymentLink" || "order" : {
"id" : "xxx"
}
}
}
Payment Link Created Example Event
Method : POST
URL : / webhook
Headers : {
host : '***' ,
'user-agent' : '***'
'content-length' : '***'
accept : 'application/json, text/plain, */*' ,
'accept-encoding' : 'gzip, compress, deflate, br' ,
'content-type' : 'application/json' ,
'x-forwarded-for' : '***' ,
'x-forwarded-host' : '***' ,
'x-forwarded-proto' : 'https' ,
'x-webhook-signature' : '3db257d7f7d709065c1ff85ed5616f1e4284e6863de0332d72099d8018f8d44b' ,
'x-webhook-timestamp' : '1751933312535'
}
Body : {
event : 'payment_link_created' ,
id : 'evt_1751933312535_ubzthgo3p' ,
timestamp : '2025-07-08T00:08:32.494Z' ,
data : {
paymentLink : {
id : 'd1Rd7fLBg6oX1ipSm2FKb5' ,
integrator : {
id : '***'
},
lifespan : 86400000 ,
paymentLinkURL : 'https://app-partner.phoenix.market/?payment_id=d1Rd7fLBg6oX1ipSm2FKb5&address=0x00000000000000000000000000&amount=1.45&shareOn=true&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJ0bmVyIjp7ImNsaWVudF9pZCI6IjBmMmM0NTZlLTUxZDAtNDNmOS1hZGM4LTgwMTNmNjFjODZmMCJ9LCJzY29wZSI6WyJpZnJhbWUiXSwidmVyc2lvbiI6IjEiLCJpYXQiOjE3NTE5Mjk3MzQsImV4cCI6MTc1MTkzMzMzNCwianRpIjoiZTA2MmVhODUtYzg0OS00ODAzLWIxNGEtNTAyZWUzYWEwMGQzIn0.okL0XnEQVRHdk0ClpWBEHpq63Kj1s6AI1gyiTTeuyNQ' ,
status : 'pending' ,
urlParams : 'address=0x00000000000000000000000000&amount=1.45&shareOn=true&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJ0bmVyIjp7ImNsaWVudF9pZCI6IjBmMmM0NTZlLTUxZDAtNDNmOS1hZGM4LTgwMTNmNjFjODZmMCJ9LCJzY29wZSI6WyJpZnJhbWUiXSwidmVyc2lvbiI6IjEiLCJpYXQiOjE3NTE5Mjk3MzQsImV4cCI6MTc1MTkzMzMzNCwianRpIjoiZTA2MmVhODUtYzg0OS00ODAzLWIxNGEtNTAyZWUzYWEwMGQzIn0.okL0XnEQVRHdk0ClpWBEHpq63Kj1s6AI1gyiTTeuyNQ'
},
timestamp : '2025-07-08T00:08:32.494Z'
}
}
Payment Link Updated Example Event
Method : POST
URL : / webhook
Headers : {
host : '***' ,
'user-agent' : '***'
'content-length' : '***'
accept : 'application/json, text/plain, */*' ,
'accept-encoding' : 'gzip, compress, deflate, br' ,
'content-type' : 'application/json' ,
'x-forwarded-for' : '***' ,
'x-forwarded-host' : '***' ,
'x-forwarded-proto' : 'https' ,
'x-webhook-signature' : '3db257d7f7d709065c1ff85ed5616f1e4284e6863de0332d72099d8018f8d44b' ,
'x-webhook-timestamp' : '1751933312535'
}
Body : {
event : 'payment_link_status_updated' ,
id : 'evt_1751933531477_ftpul3jhi' ,
timestamp : '2025-07-08T00:12:11.448Z' ,
data : {
paymentLink : {
id : 'd1Rd7fLBg6oX1ipSm2FKb5' ,
lifespan : 86400000 ,
order : [ Object ],
urlParams : 'address=0x00000000000000000000000000&amount=1.45&shareOn=true&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJ0bmVyIjp7ImNsaWVudF9pZCI6IjBmMmM0NTZlLTUxZDAtNDNmOS1hZGM4LTgwMTNmNjFjODZmMCJ9LCJzY29wZSI6WyJpZnJhbWUiXSwidmVyc2lvbiI6IjEiLCJpYXQiOjE3NTE5Mjk3MzQsImV4cCI6MTc1MTkzMzMzNCwianRpIjoiZTA2MmVhODUtYzg0OS00ODAzLWIxNGEtNTAyZWUzYWEwMGQzIn0.okL0XnEQVRHdk0ClpWBEHpq63Kj1s6AI1gyiTTeuyNQ' ,
paymentLinkURL : 'https://app-partner.phoenix.market/?payment_id=d1Rd7fLBg6oX1ipSm2FKb5&address=0x00000000000000000000000000&amount=1.45&shareOn=true&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJ0bmVyIjp7ImNsaWVudF9pZCI6IjBmMmM0NTZlLTUxZDAtNDNmOS1hZGM4LTgwMTNmNjFjODZmMCJ9LCJzY29wZSI6WyJpZnJhbWUiXSwidmVyc2lvbiI6IjEiLCJpYXQiOjE3NTE5Mjk3MzQsImV4cCI6MTc1MTkzMzMzNCwianRpIjoiZTA2MmVhODUtYzg0OS00ODAzLWIxNGEtNTAyZWUzYWEwMGQzIn0.okL0XnEQVRHdk0ClpWBEHpq63Kj1s6AI1gyiTTeuyNQ' ,
status : 'consumed' ,
integrator : [ Object ],
createdAt : '2025-07-08T00:08:32.494Z' ,
updatedAt : '2025-07-08T00:12:11.469Z'
},
paymentLinkId : 'd1Rd7fLBg6oX1ipSm2FKb5' ,
paymentLinkStatus : 'consumed' ,
timestamp : '2025-07-08T00:12:11.448Z'
}
}
Order Created Example Event
Method : POST
URL : / webhook
Headers : {
host : '***' ,
'user-agent' : '***'
'content-length' : '***'
accept : 'application/json, text/plain, */*' ,
'accept-encoding' : 'gzip, compress, deflate, br' ,
'content-type' : 'application/json' ,
'x-forwarded-for' : '***' ,
'x-forwarded-host' : '***' ,
'x-forwarded-proto' : 'https' ,
'x-webhook-signature' : '3db257d7f7d709065c1ff85ed5616f1e4284e6863de0332d72099d8018f8d44b' ,
'x-webhook-timestamp' : '1751933312535'
}
Body : {
event : 'order_created' ,
id : 'evt_1751933531144_7v7af54ad' ,
timestamp : '2025-07-08T00:12:11.101Z' ,
data : {
order : {
autofill : true ,
escrowContract : [ Object ],
id : '2WsGirssj5sy63fdKQkF7z' ,
inputAmount : 1.52 ,
integratorId : '0f2c456e-51d0-43f9-adc8-8013f61c86f0' ,
lifespan : 1500000 ,
minimumAmountOut : '1450000' ,
outputToken : [ Object ],
recipient : [ Object ],
seller : [ Object ],
uniquenessAmount : 0 ,
verificationMethod : 'teller-zelle-payment-verified-by-seller'
},
timestamp : '2025-07-08T00:12:11.101Z'
}
}
Order Updated Example Event
Method : POST
URL : / webhook
Headers : {
host : '***' ,
'user-agent' : '***'
'content-length' : '***'
accept : 'application/json, text/plain, */*' ,
'accept-encoding' : 'gzip, compress, deflate, br' ,
'content-type' : 'application/json' ,
'x-forwarded-for' : '***' ,
'x-forwarded-host' : '***' ,
'x-forwarded-proto' : 'https' ,
'x-webhook-signature' : '3db257d7f7d709065c1ff85ed5616f1e4284e6863de0332d72099d8018f8d44b' ,
'x-webhook-timestamp' : '1751933312535'
}
Body : {
event : 'order_status_change' ,
id : 'evt_1751934563333_hekop4cga' ,
timestamp : '2025-07-08T00:29:23.280Z' ,
data : {
order : {
id : '2WsGirssj5sy63fdKQkF7z' ,
minimumAmountOut : '1450000' ,
outputToken : [ Object ],
recipient : [ Object ],
inputAmount : '1.52' ,
escrowContract : [ Object ],
autofill : true ,
uniquenessAmount : '0' ,
verificationMethod : 'teller-zelle-payment-verified-by-seller' ,
lifespan : 1500000 ,
external_partner_id : null ,
integrator : [ Object ],
createdAt : '2025-07-08T00:12:11.101Z' ,
orderStatus : [ Object ],
orderVerification : [ Object ],
seller : [ Object ],
orderFulfillment : null
},
orderId : '2WsGirssj5sy63fdKQkF7z' ,
orderStatus : 'fulfilled' ,
timestamp : '2025-07-08T00:29:23.280Z'
}
}
Querying Orders
You can query order details at any time using the Phoenix API. This is useful for getting the complete order information or checking the current status.
You can use this endpoint to test your connection to the Phoenix service before setting up webhooks.
Endpoint
GET https://prod-phoenix-api-kwr2.encr.app/orders/:orderId
Example Request
const orderId = "tpT9KpKVyHU1P24RjZcW1o" ;
const response = await fetch ( `https://prod-phoenix-api-kwr2.encr.app/orders/ ${ orderId } ` );
const orderData = await response . json ();
Example Response
{
"id" : "mX9nR4sK2vL7pQ6tF8wY3z" ,
"autofill" : true ,
"createdAt" : "2025-05-28T22:52:18.751Z" ,
"inputAmount" : "10.1" ,
"minimumAmountOut" : "10100000" ,
"uniquenessAmount" : "0" ,
"verificationMethod" : "teller-zelle-payment-verified-by-seller" ,
"orderStatus" : {
"status" : "fulfilled" ,
"updatedAt" : "2025-05-28T22:54:32.459Z"
},
"orderFulfillment" : {
"createdAt" : "2025-05-28T22:54:32.459Z" ,
"transaction" : {
"hash" : "0x7f3e8d2a9c5b1f4e6a8d3c7b9e2f5a8c4d6b1e9f3a7c2d8b5e1f4a9c6d3b7e2a"
}
}
}
Order Status Update Webhook
Endpoint Configuration
Endpoint : /phoenix-order-status-updated
Event Type : order.status.change
Triggered : When an order’s status changes
Common Status Values
fulfilled
- Order has been completed successfully
expired
- Order expired before completion
pending
- Order is awaiting processing
Implementation Example
app . post ( '/phoenix-order-status-updated' , ( req , res ) => {
const { event , id , timestamp , data } = req . body ;
const { order , orderId , orderStatus } = data ;
// Validate required fields
if ( ! event || ! data || ! data . orderId || ! data . orderStatus ) {
return res . status ( 400 ). json ({
error: 'Missing required fields' ,
required: [ 'event' , 'data.orderId' , 'data.orderStatus' ]
});
}
// Validate authorization header
const authHeader = req . headers . authorization ;
if ( ! authHeader || ! authHeader . startsWith ( 'Bearer ' )) {
return res . status ( 401 ). json ({
error: 'Missing or invalid authorization header'
});
}
// Process the status update
console . log ( `Order ${ data . orderId } status changed to: ${ data . orderStatus } ` );
// Add your business logic here
res . json ({
success: true ,
message: 'Order status webhook received' ,
external_partner_id: "your-internal-order-123" // Optional: your own order ID
});
});
Payload Structure
{
"event" : "order.status.change" ,
"id" : "evt_2847593016742_xm8pqw3nz" ,
"timestamp" : "2025-05-28 22:52:18.751" ,
"data" : {
"order" : {
"id" : "mX9nR4sK2vL7pQ6tF8wY3z" ,
"inputAmount" : "10.1" ,
"minimumAmountOut" : "10100000" ,
"verificationMethod" : "teller-zelle-payment-verified-by-seller" ,
"createdAt" : "2025-05-28T22:52:18.751Z" ,
"orderStatus" : {
"status" : "fulfilled" ,
"updatedAt" : "2025-05-28T22:54:32.459Z"
},
"orderFulfillment" : {
"transaction" : {
"hash" : "0x4a7c9e2d5f8b1a6c3e9d7f2a5c8b4e1d6f9a3c7e2b5d8a1f4c9e6b3d7a2e5c8"
},
"createdAt" : "2025-05-28T22:54:32.459Z"
}
},
"orderId" : "mX9nR4sK2vL7pQ6tF8wY3z" ,
"orderStatus" : "fulfilled" ,
"timestamp" : "2025-05-28 22:52:18.751"
}
}
Webhook Response
Your webhook endpoint should return a JSON response with a 200
status code. You can optionally include an external_partner_id
field to associate your own order ID with the Phoenix order:
// Basic response
res . json ({
success: true ,
message: 'Order status webhook received'
});
// Response with optional external partner ID
res . json ({
success: true ,
message: 'Order status webhook received' ,
external_partner_id: "your-internal-order-123" // Optional: your own order ID
});
Optional Response Field:
external_partner_id
(string): Your internal order ID or reference that you want to associate with the Phoenix order. This helps you link Phoenix orders to your own system’s order tracking.
Authentication
Phoenix uses Bearer token authentication for webhooks:
Header : Authorization: Bearer <webhook_secret>
Webhook Secret : Provided by Phoenix support team
Validation : Always verify the authorization header exists and contains a valid Bearer token
View Authentication Code Example
// Authentication validation example
const authHeader = req . headers . authorization ;
if ( ! authHeader || ! authHeader . startsWith ( 'Bearer ' )) {
return res . status ( 401 ). json ({
error: 'Missing or invalid authorization header' ,
expected: 'Bearer <webhook_secret>'
});
}
const webhookSecret = authHeader . replace ( 'Bearer ' , '' );
// Verify this matches your provided webhook secret
Best Practices
Validate Authentication : Always check the Authorization
header
Validate Required Fields : Ensure all required fields are present
Implement Idempotency : Use id
to handle duplicate events
Respond Quickly : Process webhooks within 30 seconds
Log Events : Keep logs for debugging and monitoring
Use HTTPS : Only accept webhooks over secure HTTPS connections
Handle Errors Gracefully : Return appropriate HTTP status codes
Support
For webhook-related questions or to request webhook access: