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

# Webhooks Overview

> Real-time event notifications for trading activity on Daya Pro

## What are Webhooks?

Webhooks allow you to receive real-time HTTP notifications when trading events occur on your account, eliminating the need to poll the API for order status updates.

<Note>
  Webhooks are the **recommended** way to track order execution and trade activity. They provide real-time updates and reduce API load.
</Note>

## Supported Events

| Event                      | Description             | When Triggered                                                     |
| -------------------------- | ----------------------- | ------------------------------------------------------------------ |
| `order.created`            | Order submitted         | New order accepted by the matching engine                          |
| `order.filled`             | Order completely filled | All quantity executed                                              |
| `order.partially_filled`   | Order partially filled  | Some quantity executed, order still open                           |
| `order.cancelled`          | Order cancelled         | User cancelled or system cancelled                                 |
| `order.rejected`           | Order rejected          | Validation failed or insufficient balance                          |
| `trade.executed`           | Trade executed          | A trade matched involving your order                               |
| `deposit.completed`        | NGN deposit settled     | A bank-transfer deposit reached your Daya Pro balance              |
| `crypto.deposit.completed` | Crypto deposit settled  | A confirmed on-chain deposit was credited to your Daya Pro balance |

## Webhook Configuration

Webhook setup for Daya Pro is support-managed. Contact [support@daya.co](mailto:support@daya.co) to set up or update Pro webhook endpoints and event subscriptions.

When requesting setup, include:

1. Webhook URL (must be HTTPS)
2. Events to subscribe to
3. Environment and Pro account details

<Warning>
  Webhook URLs **must** use HTTPS in production. HTTP is only allowed for local development testing.
</Warning>

## HTTP Headers

All webhook requests include the following headers:

| Header                | Description                                 | Example                                |
| --------------------- | ------------------------------------------- | -------------------------------------- |
| `Content-Type`        | Always `application/json`                   | `application/json`                     |
| `X-Webhook-Signature` | HMAC-SHA256 signature with `sha256=` prefix | `sha256=a8f5f167f44f...`               |
| `X-Webhook-Event`     | Event type that triggered this webhook      | `order.filled`                         |
| `X-Webhook-ID`        | Unique event identifier (UUID)              | `550e8400-e29b-41d4-a716-446655440000` |
| `X-Webhook-Timestamp` | When the event occurred (RFC3339)           | `2026-01-14T15:08:15Z`                 |
| `User-Agent`          | Identifies Daya as the sender               | `Daya-Webhook/1.0`                     |

## Webhook Payload

All webhook events follow this structure:

```json theme={null}
{
  "event_id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "order.filled",
  "timestamp": "2026-01-14T15:08:15Z",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "symbol": "USD-NGN",
    "side": "buy",
    "type": "limit",
    "status": "filled",
    "price": "1545.00",
    "quantity": "100.00",
    "filled_quantity": "100.00",
    "executed_price": "1545.00",
    "base_asset": "USD",
    "quote_asset": "NGN",
    "created_at": "2026-01-14T15:06:30Z",
    "updated_at": "2026-01-14T15:08:15Z"
  }
}
```

### Common Fields

<ParamField body="event_id" type="string">
  Unique identifier for this event (UUID format)

  **Use for:** Idempotency (deduplicate multiple deliveries)
</ParamField>

<ParamField body="type" type="string">
  Event type

  **Values:** `order.created`, `order.filled`, `order.partially_filled`, `order.cancelled`, `order.rejected`, `trade.executed`, `deposit.completed`, `crypto.deposit.completed`
</ParamField>

<ParamField body="timestamp" type="string">
  When event occurred (RFC3339 timestamp)
</ParamField>

<ParamField body="data" type="object">
  Event-specific data (varies by event type)
</ParamField>

## Event-Specific Payloads

<Tabs>
  <Tab title="order.created">
    **Sent when:** New order accepted by matching engine

    ```json theme={null}
    {
      "event_id": "550e8400-e29b-41d4-a716-446655440000",
      "type": "order.created",
      "timestamp": "2026-01-14T15:06:30Z",
      "data": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "symbol": "USD-NGN",
        "side": "buy",
        "type": "limit",
        "status": "open",
        "price": "1545.00",
        "quantity": "100.00",
        "filled_quantity": "0.00",
        "base_asset": "USD",
        "quote_asset": "NGN",
        "created_at": "2026-01-14T15:06:30Z",
        "updated_at": "2026-01-14T15:06:30Z"
      }
    }
    ```

    **Next steps:** Monitor for `order.filled`, `order.partially_filled`, or `order.cancelled`
  </Tab>

  <Tab title="order.filled">
    **Sent when:** Order completely filled

    ```json theme={null}
    {
      "event_id": "550e8400-e29b-41d4-a716-446655440001",
      "type": "order.filled",
      "timestamp": "2026-01-14T15:08:15Z",
      "data": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "symbol": "USD-NGN",
        "side": "buy",
        "type": "limit",
        "status": "filled",
        "price": "1545.00",
        "quantity": "100.00",
        "filled_quantity": "100.00",
        "executed_price": "1545.00",
        "base_asset": "USD",
        "quote_asset": "NGN",
        "created_at": "2026-01-14T15:06:30Z",
        "updated_at": "2026-01-14T15:08:15Z"
      }
    }
    ```

    **Action:** Update UI to show completed order, refresh balances
  </Tab>

  <Tab title="order.partially_filled">
    **Sent when:** Order partially executed, still open

    ```json theme={null}
    {
      "event_id": "550e8400-e29b-41d4-a716-446655440002",
      "type": "order.partially_filled",
      "timestamp": "2026-01-14T15:07:30Z",
      "data": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "symbol": "USD-NGN",
        "side": "buy",
        "type": "limit",
        "status": "partially_filled",
        "price": "1545.00",
        "quantity": "100.00",
        "filled_quantity": "50.00",
        "executed_price": "1545.00",
        "base_asset": "USD",
        "quote_asset": "NGN",
        "created_at": "2026-01-14T15:06:30Z",
        "updated_at": "2026-01-14T15:07:30Z"
      }
    }
    ```

    **Action:** Update order status in UI, show partial fill progress
  </Tab>

  <Tab title="order.cancelled">
    **Sent when:** Order cancelled by user or system

    ```json theme={null}
    {
      "event_id": "550e8400-e29b-41d4-a716-446655440003",
      "type": "order.cancelled",
      "timestamp": "2026-01-14T15:10:00Z",
      "data": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "symbol": "USD-NGN",
        "side": "buy",
        "type": "limit",
        "status": "cancelled",
        "price": "1545.00",
        "quantity": "100.00",
        "filled_quantity": "50.00",
        "base_asset": "USD",
        "quote_asset": "NGN",
        "created_at": "2026-01-14T15:06:30Z",
        "updated_at": "2026-01-14T15:10:00Z",
        "cancelled_at": "2026-01-14T15:10:00Z"
      }
    }
    ```
  </Tab>

  <Tab title="order.rejected">
    **Sent when:** Order rejected by validation or matching engine

    ```json theme={null}
    {
      "event_id": "550e8400-e29b-41d4-a716-446655440004",
      "type": "order.rejected",
      "timestamp": "2026-01-14T15:06:31Z",
      "data": {
        "id": "550e8400-e29b-41d4-a716-446655440001",
        "symbol": "USD-NGN",
        "side": "buy",
        "type": "limit",
        "status": "rejected",
        "price": "1545.00",
        "quantity": "100.00",
        "filled_quantity": "0.00",
        "base_asset": "USD",
        "quote_asset": "NGN",
        "created_at": "2026-01-14T15:06:31Z",
        "updated_at": "2026-01-14T15:06:31Z",
        "rejection_reason": "insufficient_balance"
      }
    }
    ```

    **Rejection reasons:** `insufficient_balance`, `invalid_price`, `invalid_quantity`, `market_closed`, `rate_limit_exceeded`
  </Tab>

  <Tab title="trade.executed">
    **Sent when:** A trade is executed involving your order

    ```json theme={null}
    {
      "event_id": "660e8400-e29b-41d4-a716-446655440000",
      "type": "trade.executed",
      "timestamp": "2026-01-14T15:08:15Z",
      "data": {
        "id": "660e8400-e29b-41d4-a716-446655440000",
        "order_id": "550e8400-e29b-41d4-a716-446655440000",
        "symbol": "USD-NGN",
        "side": "buy",
        "price": "1545.00",
        "quantity": "50.00",
        "total_value": "77250.00",
        "fee": "15.45",
        "is_maker": false,
        "base_asset": "USD",
        "quote_asset": "NGN",
        "counterparty_order_id": "770e8400-e29b-41d4-a716-446655440000",
        "executed_at": "2026-01-14T15:08:15Z"
      }
    }
    ```

    **is\_maker:** `true` if your order was resting on the book (maker), `false` if your order matched existing orders (taker)
  </Tab>

  <Tab title="deposit.completed">
    **Sent when:** An NGN bank-transfer deposit settles to your Daya Pro balance

    ```json theme={null}
    {
      "event_id": "770e8400-e29b-41d4-a716-446655440000",
      "type": "deposit.completed",
      "timestamp": "2026-05-06T14:00:09Z",
      "data": {
        "id": "11111111-1111-1111-1111-111111111111",
        "type": "deposit",
        "method": "bank_transfer",
        "status": "completed",
        "amount_ngn": "500000.00",
        "currency": "NGN",
        "payment_provider": "flutterwave",
        "provider_transaction_id": "FLW-RFR-9001",
        "originator_name": "Jane Doe",
        "matching_reference": "DAYA-REF-001",
        "reference": "INV-9001",
        "narration": "Customer top-up",
        "created_at": "2026-05-06T14:00:00Z",
        "completed_at": "2026-05-06T14:00:09Z"
      }
    }
    ```

    Only fires on the terminal `completed` state. On-chain deposits use `crypto.deposit.completed`.
  </Tab>

  <Tab title="crypto.deposit.completed">
    **Sent when:** A confirmed on-chain deposit is credited to your Daya Pro balance

    ```json theme={null}
    {
      "event_id": "880e8400-e29b-41d4-a716-446655440000",
      "type": "crypto.deposit.completed",
      "timestamp": "2026-06-05T12:00:09Z",
      "data": {
        "transaction_id": "11111111-1111-1111-1111-111111111111",
        "status": "completed",
        "asset": "USDT",
        "amount": "50.000000",
        "fee_amount": "0.000000",
        "fee_asset": "USDT",
        "blockchain": "ethereum",
        "transaction_hash": "0xhash456",
        "from_address": "0xsource",
        "to_address": "0x1234567890abcdef1234567890abcdef12345678",
        "created_at": "2026-06-05T11:59:30Z",
        "completed_at": "2026-06-05T12:00:09Z"
      }
    }
    ```

    The payload uses normalized Daya fields and does not include raw provider references.
  </Tab>
</Tabs>

## Delivery Guarantees

<AccordionGroup>
  <Accordion title="At-least-once delivery">
    Webhooks may be delivered **multiple times**. Your endpoint must handle duplicate deliveries using `event_id` for idempotency.
  </Accordion>

  <Accordion title="Order not guaranteed">
    Events may arrive out of order. Use `created_at` timestamps to order events client-side.
  </Accordion>

  <Accordion title="Retry behavior">
    If your endpoint returns non-2xx status or times out, Daya retries with exponential backoff:

    | Attempt | Delay After Previous |
    | ------- | -------------------- |
    | 1       | 10 seconds           |
    | 2       | 30 seconds           |
    | 3       | 1 minute             |
    | 4       | 5 minutes            |
    | 5       | 15 minutes           |
    | 6       | 30 minutes           |
    | 7       | 1 hour               |
    | 8       | 2 hours              |
    | 9       | 4 hours              |
    | 10      | 8 hours              |

    After 10 failed attempts, delivery is marked as failed and the webhook may be automatically disabled.
  </Accordion>

  <Accordion title="Timeout">
    Your endpoint must respond within **30 seconds**. Longer responses will timeout and trigger retries.
  </Accordion>

  <Accordion title="Auto-disable">
    Webhooks are automatically disabled after **10 consecutive delivery failures**. Contact support to re-enable a disabled Pro webhook endpoint.
  </Accordion>
</AccordionGroup>

<Note>
  There is no manual redispatch endpoint in this version. Use [Get Webhook Deliveries](/pro/api-reference/get-webhook-deliveries) to inspect delivery logs and retry status.
</Note>

## Webhook Verification

All webhooks include an HMAC-SHA256 signature in the `X-Webhook-Signature` header with a `sha256=` prefix. **Always verify** signatures to prevent spoofing.

```
X-Webhook-Signature: sha256=a8f5f167f44f4964e6c998dee827110c447be52d40d67b6a60b78c1e3e01b7e8
```

See [Webhook Verification](/pro/webhooks/verification) for implementation details.

## Implementing a Webhook Endpoint

### Required Response

Your endpoint must:

1. **Verify signature** (see [Verification](/pro/webhooks/verification))
2. **Return 2xx status** to acknowledge receipt
3. **Process quickly** (\< 10 seconds) or queue for async processing

### Example Implementation

<CodeGroup>
  ```javascript Express.js theme={null}
  const express = require('express');
  const crypto = require('crypto');

  const app = express();

  app.post('/webhooks/daya-pro', express.raw({ type: 'application/json' }), (req, res) => {
    const signatureHeader = req.headers['x-webhook-signature'];
    const payload = req.body.toString('utf8');

    // 1. Verify signature (strip "sha256=" prefix)
    const signature = signatureHeader?.replace('sha256=', '');
    if (!verifySignature(payload, signature, process.env.DAYA_PRO_WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }

    // 2. Parse and check idempotency
    const event = JSON.parse(payload);
    if (isProcessed(event.event_id)) {
      return res.status(200).send('Already processed');
    }

    // 3. Handle event
    switch (event.type) {
      case 'order.filled':
        handleOrderFilled(event.data);
        break;
      case 'order.partially_filled':
        handlePartialFill(event.data);
        break;
      case 'trade.executed':
        handleTradeExecuted(event.data);
        break;
      case 'crypto.deposit.completed':
        handleCryptoDepositCompleted(event.data);
        break;
      // ... handle other events
    }

    markAsProcessed(event.event_id);
    res.status(200).send('OK');
  });

  function verifySignature(payload, signature, secret) {
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  }
  ```

  ```python Flask theme={null}
  from flask import Flask, request, jsonify
  import hmac
  import hashlib

  app = Flask(__name__)

  @app.route('/webhooks/daya-pro', methods=['POST'])
  def handle_webhook():
      signature_header = request.headers.get('X-Webhook-Signature', '')
      payload = request.get_data()

      # 1. Verify signature (strip "sha256=" prefix)
      signature = signature_header.replace('sha256=', '')
      if not verify_signature(payload, signature, DAYA_PRO_WEBHOOK_SECRET):
          return jsonify({'error': 'Invalid signature'}), 401

      # 2. Parse and check idempotency
      event = request.json
      if is_processed(event['event_id']):
          return jsonify({'status': 'already_processed'}), 200

      # 3. Handle event
      event_type = event['type']
      data = event['data']

      if event_type == 'order.filled':
          handle_order_filled(data)
      elif event_type == 'order.partially_filled':
          handle_partial_fill(data)
      elif event_type == 'trade.executed':
          handle_trade_executed(data)
      elif event_type == 'crypto.deposit.completed':
          handle_crypto_deposit_completed(data)

      mark_as_processed(event['event_id'])
      return jsonify({'status': 'ok'}), 200

  def verify_signature(payload, signature, secret):
      expected = hmac.new(
          secret.encode(),
          payload,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "encoding/json"
      "io"
      "net/http"
      "strings"
  )

  func handleWebhook(w http.ResponseWriter, r *http.Request) {
      signatureHeader := r.Header.Get("X-Webhook-Signature")
      payload, _ := io.ReadAll(r.Body)

      // 1. Verify signature (strip "sha256=" prefix)
      signature := strings.TrimPrefix(signatureHeader, "sha256=")
      if !verifySignature(payload, signature, dayaProWebhookSecret) {
          http.Error(w, "Invalid signature", http.StatusUnauthorized)
          return
      }

      // 2. Parse event
      var event WebhookEvent
      json.Unmarshal(payload, &event)

      // 3. Check idempotency
      if isProcessed(event.EventID) {
          w.WriteHeader(http.StatusOK)
          return
      }

      // 4. Handle event
      switch event.Type {
      case "order.filled":
          handleOrderFilled(event.Data)
      case "order.partially_filled":
          handlePartialFill(event.Data)
      case "trade.executed":
          handleTradeExecuted(event.Data)
      case "crypto.deposit.completed":
          handleCryptoDepositCompleted(event.Data)
      }

      markAsProcessed(event.EventID)
      w.WriteHeader(http.StatusOK)
  }

  func verifySignature(payload []byte, signature string, secret string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(payload)
      expectedSignature := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(signature), []byte(expectedSignature))
  }
  ```
</CodeGroup>

## Best Practices

<Steps>
  <Step title="Verify signatures">
    **Always** verify `X-Webhook-Signature` to prevent spoofing attacks. Remember to strip the `sha256=` prefix before comparing.
  </Step>

  <Step title="Handle idempotency">
    Use `event_id` to deduplicate. Store processed event IDs in your database.

    ```sql theme={null}
    CREATE TABLE processed_webhook_events (
      event_id VARCHAR(255) PRIMARY KEY,
      processed_at TIMESTAMP
    );
    ```
  </Step>

  <Step title="Return 200 quickly">
    Acknowledge receipt immediately (\< 1 second). Queue heavy processing asynchronously.
  </Step>

  <Step title="Handle out-of-order delivery">
    Events may arrive out of order. Use `timestamp` field and order status to reconcile.
  </Step>

  <Step title="Reconcile with API">
    Periodically call [List Orders](/pro/api-reference/list-orders) to reconcile state in case webhooks are missed.
  </Step>
</Steps>

## Testing Webhooks

### Local Testing

For local development, use tools like [ngrok](https://ngrok.com):

```bash theme={null}
# Start ngrok
ngrok http 3000

# Share this URL with support for webhook setup
https://abc123.ngrok.xyz/webhooks/daya-pro
```

<Tip>
  Use ngrok's web interface ([http://localhost:4040](http://localhost:4040)) to inspect webhook payloads during development.
</Tip>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhooks not received">
    **Possible causes:**

    * Firewall blocking Daya's IPs
    * Endpoint returning non-2xx status
    * SSL certificate issues

    **Fix:** Check endpoint logs, ensure HTTPS, verify firewall rules
  </Accordion>

  <Accordion title="Duplicate deliveries">
    **Expected behavior:** At-least-once delivery means duplicates are possible

    **Fix:** Implement idempotency using `event_id`
  </Accordion>

  <Accordion title="Timeouts">
    **Cause:** Endpoint taking > 30 seconds to respond

    **Fix:** Return 200 immediately, queue processing asynchronously
  </Accordion>

  <Accordion title="Signature verification fails">
    **Cause:** Wrong secret, payload manipulation, or not stripping `sha256=` prefix

    **Fix:** Verify you're using correct webhook secret and stripping the `sha256=` prefix from the `X-Webhook-Signature` header before comparing
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhook Events" icon="bell" href="/pro/webhooks/events">
    Detailed event schemas
  </Card>

  <Card title="Signature Verification" icon="shield-check" href="/pro/webhooks/verification">
    Implement HMAC verification
  </Card>
</CardGroup>
