> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phoenix.market/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Set up webhooks to receive real-time notifications about order & payment-link events

## 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:

1. Contact our support team at [main@phoenix.market](mailto:main@phoenix.market)
2. 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:

```javascript theme={null}
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:

1. Determine the full URL path where you want to receive webhooks (e.g., `https://yourdomain.com/webhooks/phoenix-status-updates`)
2. Contact our support team at [main@phoenix.market](mailto:main@phoenix.market) with your webhook endpoint URL
3. 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.

```javascript theme={null}
{
  "event": "",
  "id": "xxx",
  "timestamp": "1970-01-01T00:00:00.001Z",
  "data": {
    "paymentLink" || "order": {
      "id": "xxx"
    }
  }
}
```

<Accordion title="Payment Link Created Example Event">
  ```javascript theme={null}
  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'
    }
  }
  ```
</Accordion>

<Accordion title="Payment Link Updated Example Event">
  ```javascript theme={null}
  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'
    }
  }
  ```
</Accordion>

<Accordion title="Order Created Example Event">
  ```javascript theme={null}
  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'
    }
  }
  ```
</Accordion>

<Accordion title="Order Updated Example Event">
  ```javascript theme={null}
  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'
    }
  }
  ```
</Accordion>

## 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.

<Note>
  You can use this endpoint to test your connection to the Phoenix service before setting up webhooks.
</Note>

### Endpoint

```
GET https://prod-phoenix-api-kwr2.encr.app/orders/:orderId
```

### Example Request

```javascript theme={null}
const orderId = "tpT9KpKVyHU1P24RjZcW1o";
const response = await fetch(`https://prod-phoenix-api-kwr2.encr.app/orders/${orderId}`);
const orderData = await response.json();
```

### Example Response

<Accordion title="View Example Response">
  ```json theme={null}
  {
    "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"
      }
    }
  }
  ```
</Accordion>

## 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

```javascript theme={null}
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

<Accordion title="View Payload Structure">
  ```json theme={null}
  {
    "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"
    }
  }
  ```
</Accordion>

### 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:

```javascript theme={null}
// 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

<Accordion title="View Authentication Code Example">
  ```javascript theme={null}
  // 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
  ```
</Accordion>

## Best Practices

1. **Validate Authentication**: Always check the `Authorization` header
2. **Validate Required Fields**: Ensure all required fields are present
3. **Implement Idempotency**: Use `id` to handle duplicate events
4. **Respond Quickly**: Process webhooks within 30 seconds
5. **Log Events**: Keep logs for debugging and monitoring
6. **Use HTTPS**: Only accept webhooks over secure HTTPS connections
7. **Handle Errors Gracefully**: Return appropriate HTTP status codes

## Support

For webhook-related questions or to request webhook access:

* **Email**: [main@phoenix.market](mailto:main@phoenix.market)
