> ## 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 money movement lifecycle events

## What are Webhooks?

Webhooks let you receive real-time HTTP notifications when funding account, deposit, transfer, withdrawal, or customer verification lifecycle events occur, instead of polling the API for status changes.

<Note>
  Webhooks are the recommended way to track funding account provisioning, deposit settlement, merchant-initiated bank transfers, crypto withdrawal progress, and customer verification changes.
</Note>

## Supported Events

**Deposit Events:**

| Event                     | Resource | Description                                 |
| ------------------------- | -------- | ------------------------------------------- |
| `deposit.received`        | Deposit  | Deposit received (NGN or crypto)            |
| `deposit.processing`      | Deposit  | Deposit settlement has started              |
| `deposit.requires_review` | Deposit  | Deposit needs review before it can continue |
| `deposit.completed`       | Deposit  | Deposit reached its settlement destination  |
| `deposit.failed`          | Deposit  | Deposit failed                              |
| `deposit.reversed`        | Deposit  | Completed deposit reversed                  |

**Funding Account Events:**

| Event                      | Resource        | Description                                     |
| -------------------------- | --------------- | ----------------------------------------------- |
| `funding_account.created`  | Funding Account | Funding account record created in pending state |
| `funding_account.active`   | Funding Account | Receive instructions provisioned successfully   |
| `funding_account.failed`   | Funding Account | Receive instruction provisioning failed         |
| `funding_account.disabled` | Funding Account | Funding account disabled                        |

**Withdrawal Events:**

| Event                  | Resource   | Description                   |
| ---------------------- | ---------- | ----------------------------- |
| `withdrawal.created`   | Withdrawal | Withdrawal request accepted   |
| `withdrawal.submitted` | Withdrawal | Withdrawal submitted to chain |
| `withdrawal.completed` | Withdrawal | Withdrawal confirmed on-chain |
| `withdrawal.failed`    | Withdrawal | Withdrawal failed             |

**Transfer Events:**

| Event                      | Resource | Description                                                     |
| -------------------------- | -------- | --------------------------------------------------------------- |
| `transfer.created`         | Transfer | Transfer record created                                         |
| `transfer.processing`      | Transfer | Funds locked and transfer submission queued                     |
| `transfer.requires_review` | Transfer | Transfer flagged for manual review                              |
| `transfer.submitted`       | Transfer | Provider accepted the transfer submission                       |
| `transfer.completed`       | Transfer | Transfer settled after provider success and ledger finalization |
| `transfer.failed`          | Transfer | Transfer failed terminally                                      |
| `transfer.reversed`        | Transfer | Completed transfer reversed                                     |

**Customer Verification Events:**

| Event                             | Resource | Description                       |
| --------------------------------- | -------- | --------------------------------- |
| `customer.verification.submitted` | Customer | Verification submitted for review |
| `customer.verification.approved`  | Customer | Verification approved             |
| `customer.verification.rejected`  | Customer | Verification rejected             |

<Info>
  Use `transfer.*` events for `POST /v1/transfers`. For deposits, track settlement with `deposit.*` events; settlement delivery work that Daya performs in the background is not surfaced as a separate payout webhook.
</Info>

## Webhook Configuration

Configure webhook endpoints in your [Daya Dashboard](https://dashboard.daya.co/webhooks):

1. Navigate to **Webhooks**
2. Add your webhook URL
3. Generate or copy your webhook secret
4. Subscribe to the events you want to receive

The `event_types` setting controls which events an endpoint receives. Omit `event_types` or set it to `[]` to receive all events. Use a non-empty array, such as `["deposit.received", "deposit.completed"]`, to receive only those exact event names. Unknown event names are rejected.

<Warning>
  Use HTTPS in production. HTTP should be limited to local or sandbox development.
</Warning>

## Webhook Payload

All merchant webhooks use the same envelope:

```json theme={null}
{
  "event": "transfer.completed",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "id": "650e8400-e29b-41d4-a716-446655440000",
    "status": "SETTLED",
    "rail": "NGN_BANK",
    "reference": "txn_ngn_001"
  },
  "timestamp": "2026-03-10T09:03:00Z"
}
```

### Common Fields

<ParamField body="event" type="string">
  Event type, such as `deposit.completed`, `transfer.completed`, or `withdrawal.failed`.
</ParamField>

<ParamField body="id" type="string">
  Unique webhook event identifier. Store this value to deduplicate deliveries.
</ParamField>

<ParamField body="data" type="object">
  Resource payload for the event. Funding account events send a funding account object, deposit events send a deposit object, transfer events send a transfer object, withdrawal events send a withdrawal object, and customer verification events send a customer object.
</ParamField>

<ParamField body="timestamp" type="string">
  RFC3339 timestamp for when the event was emitted.
</ParamField>

<Tip>
  See [Webhook Events](/api-reference/webhooks/events) for the full event list and resource payload mapping.
</Tip>

## Delivery Guarantees

<AccordionGroup>
  <Accordion title="At-least-once delivery">
    Webhooks may be delivered more than once. Your handler must deduplicate repeated deliveries.
  </Accordion>

  <Accordion title="Order not guaranteed">
    Events can arrive out of order. Use the `timestamp` field to order updates client-side.
  </Accordion>

  <Accordion title="Retry behavior">
    If your endpoint returns a non-2xx response or times out, Daya retries with backoff. Build your handler to be safe for repeated delivery.
  </Accordion>

  <Accordion title="Timeout">
    Respond quickly and offload heavy work asynchronously. Slow handlers increase duplicate deliveries.
  </Accordion>
</AccordionGroup>

## Webhook Verification

All webhook requests include an HMAC-SHA256 signature in the `X-Daya-Signature` header.

See [Webhook Verification](/api-reference/webhooks/verification) for implementation details.

## Implementing a Webhook Endpoint

Your endpoint should:

1. Verify the signature
2. Build a stable deduplication key from the payload
3. Return `2xx` quickly
4. Process the event asynchronously if work is non-trivial

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

  const app = express();
  app.use(express.json());

  function verifySignature(payload, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');

    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  }

  function webhookSubjectId(event) {
    return event.data.id || event.data.deposit_id;
  }

  function dedupeKey(event) {
    return event.id || `${event.event}:${webhookSubjectId(event)}:${event.timestamp}`;
  }

  app.post('/webhooks/daya', (req, res) => {
    const signature = req.headers['x-daya-signature'];
    const payload = JSON.stringify(req.body);

    if (!verifySignature(payload, signature, process.env.DAYA_WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }

    const key = dedupeKey(req.body);
    if (isProcessed(key)) {
      return res.status(200).send('Already processed');
    }

    queue.add('process-webhook', req.body);
    markAsProcessed(key);

    res.status(200).send('OK');
  });
  ```

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

  app = Flask(__name__)

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

  def webhook_subject_id(event):
      return event["data"].get("id") or event["data"].get("deposit_id")

  def dedupe_key(event):
      return event.get("id") or f'{event["event"]}:{webhook_subject_id(event)}:{event["timestamp"]}'

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

      if not verify_signature(payload, signature, DAYA_WEBHOOK_SECRET):
          return jsonify({'error': 'Invalid signature'}), 401

      event = request.json
      key = dedupe_key(event)

      if is_processed(key):
          return jsonify({'status': 'already_processed'}), 200

      queue.enqueue('process_webhook', event)
      mark_as_processed(key)

      return jsonify({'status': 'ok'}), 200
  ```
</CodeGroup>

## Best Practices

<Steps>
  <Step title="Verify signatures">
    Always verify `X-Daya-Signature` before trusting the payload.
  </Step>

  <Step title="Deduplicate deliveries">
    Store the webhook event `id` as your primary deduplication key.

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

  <Step title="Return 200 quickly">
    Acknowledge receipt immediately and queue heavier downstream work.
  </Step>

  <Step title="Handle out-of-order events">
    Use `timestamp` and the underlying resource state to reconcile event order safely.
  </Step>

  <Step title="Monitor webhook health">
    Track failed deliveries and alert on sustained retries.
  </Step>
</Steps>

## Testing Webhooks

For deposit webhook flows:

1. Create a sandbox receive instruction
2. Trigger a sandbox deposit with [Create a sandbox deposit](/api-reference/sandbox/create-sandbox-deposit)
3. Observe the resulting deposit lifecycle webhooks

For withdrawal webhook flows:

1. Fund merchant balance through a merchant-balance settlement flow
2. Create a withdrawal
3. Observe `withdrawal.created`, `withdrawal.submitted`, `withdrawal.completed`, or `withdrawal.failed`

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhooks not received">
    Check endpoint reachability, TLS configuration, and whether your handler is returning non-2xx responses.
  </Accordion>

  <Accordion title="Duplicate deliveries">
    Duplicate delivery is expected under at-least-once semantics. Deduplicate using the webhook event `id`.
  </Accordion>

  <Accordion title="Out-of-order events">
    Sort or reconcile events using `timestamp` instead of arrival order.
  </Accordion>

  <Accordion title="Signature verification fails">
    Verify you are using the correct webhook secret and the raw request body.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhook Events" icon="bell" href="/api-reference/webhooks/events">
    Event inventory and resource payload mapping
  </Card>

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