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

# Webhooks

> Receive real-time event notifications from Thiqwave when transactions complete, compliance status changes, or accounts are created.

Webhooks let Thiqwave push event notifications to your server the moment something happens — a transaction completes, a KYB check passes, or a new account is created. Instead of repeatedly polling the API for status updates, you receive an HTTP `POST` to your endpoint with the event payload.

## Available events

### Transfer events

| Event                       | When it fires                                    |
| --------------------------- | ------------------------------------------------ |
| `transfer.pending`          | Transfer created, awaiting processing or deposit |
| `transfer.awaiting_deposit` | Fiat source: waiting for bank deposit            |
| `transfer.processing`       | Funds received, conversion/routing in progress   |
| `transfer.settling`         | On-chain settlement or fiat delivery in progress |
| `transfer.completed`        | Transfer complete — funds delivered              |
| `transfer.failed`           | Transfer failed                                  |

### Granular endpoint events

| Event              | When it fires                            |
| ------------------ | ---------------------------------------- |
| `payin.completed`  | A pay-in has been received and confirmed |
| `payout.completed` | A payout has been delivered              |
| `payout.failed`    | A payout failed                          |

### Account & compliance events

| Event                 | When it fires                      |
| --------------------- | ---------------------------------- |
| `compliance.approved` | A KYB submission has been approved |
| `compliance.rejected` | A KYB submission was rejected      |
| `account.created`     | A new account was created          |
| `account.funded`      | Funds arrived at an account        |

## Set up a webhook endpoint

<Steps>
  <Step title="Create a public HTTPS endpoint">
    Your endpoint must:

    * Accept `POST` requests
    * Be reachable over the public internet via HTTPS
    * Return a `2xx` status code within **10 seconds** to acknowledge receipt

    Any response outside the `2xx` range — or no response within 10 seconds — is treated as a delivery failure and triggers the retry policy.

    <Tip>
      During development, use a tool like [ngrok](https://ngrok.com) or [Hookdeck](https://hookdeck.com) to expose a local server for testing.
    </Tip>
  </Step>

  <Step title="Register your endpoint in the dashboard">
    1. Log in to the [Thiqwave Dashboard](https://console.thiqwave.com).
    2. Navigate to **Settings → Webhooks**.
    3. Click **Add endpoint**.
    4. Enter your endpoint URL.
    5. Select the events you want to receive (or choose **All events**).
    6. Click **Save**.

    Thiqwave generates a **webhook secret** for the endpoint. Copy and store it securely — you use it to verify incoming payloads.
  </Step>

  <Step title="Verify webhook signatures">
    Every webhook request includes a `X-Thiqwave-Signature` header. Verify this signature before processing the payload to ensure it came from Thiqwave and was not tampered with.

    The signature is an HMAC-SHA256 hex digest of the raw request body, computed using your webhook secret as the key.

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

      function verifyWebhookSignature(rawBody, signature, webhookSecret) {
        const expectedSignature = crypto
          .createHmac('sha256', webhookSecret)
          .update(rawBody)
          .digest('hex');

        // Use timingSafeEqual to prevent timing attacks
        const expected = Buffer.from(expectedSignature, 'hex');
        const received = Buffer.from(signature, 'hex');

        if (expected.length !== received.length) {
          return false;
        }

        return crypto.timingSafeEqual(expected, received);
      }

      // Express.js example
      app.post('/webhooks/thiqwave', express.raw({ type: 'application/json' }), (req, res) => {
        const signature = req.headers['x-thiqwave-signature'];
        const webhookSecret = process.env.THIQWAVE_WEBHOOK_SECRET;

        if (!verifyWebhookSignature(req.body, signature, webhookSecret)) {
          return res.status(401).json({ error: 'Invalid signature' });
        }

        const event = JSON.parse(req.body);
        console.log('Received event:', event.event, event.data.id);

        // Process the event...

        res.status(200).json({ received: true });
      });
      ```

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

      app = Flask(__name__)

      def verify_webhook_signature(raw_body: bytes, signature: str, webhook_secret: str) -> bool:
          expected = hmac.new(
              webhook_secret.encode('utf-8'),
              raw_body,
              hashlib.sha256
          ).hexdigest()

          # Use compare_digest to prevent timing attacks
          return hmac.compare_digest(expected, signature)

      @app.route('/webhooks/thiqwave', methods=['POST'])
      def handle_webhook():
          signature = request.headers.get('X-Thiqwave-Signature', '')
          webhook_secret = os.environ['THIQWAVE_WEBHOOK_SECRET']
          raw_body = request.get_data()

          if not verify_webhook_signature(raw_body, signature, webhook_secret):
              return jsonify({'error': 'Invalid signature'}), 401

          event = request.get_json()
          print(f"Received event: {event['event']} {event['data']['id']}")

          # Process the event...

          return jsonify({'received': True}), 200
      ```
    </CodeGroup>

    <Warning>
      Always parse the raw request body **before** parsing as JSON when computing the signature. If you parse JSON first and re-serialise, byte-level differences will cause signature verification to fail.
    </Warning>
  </Step>

  <Step title="Process the event payload">
    After verifying the signature, parse the JSON body and handle the event based on the `event` field:

    ```json theme={null}
    {
      "event": "payout.completed",
      "id": "evt_01HXYZEVT1234567890AB",
      "timestamp": "2026-04-08T10:05:23Z",
      "signature": "a3f1c2d4e5b6...",
      "data": {
        "id": "payout_01HXYZABCDEF1234567890",
        "type": "payout",
        "status": "completed",
        "amount": 500000,
        "currency": "AED",
        "recipient": {
          "account_number": "123456789012",
          "bank_code": "050"
        },
        "completed_at": "2026-04-08T10:05:20Z"
      }
    }
    ```

    | Field       | Type   | Description                                                  |
    | ----------- | ------ | ------------------------------------------------------------ |
    | `event`     | string | The event type (e.g., `transaction.completed`).              |
    | `id`        | string | Unique identifier for this event delivery.                   |
    | `timestamp` | string | ISO 8601 timestamp of when the event occurred.               |
    | `signature` | string | HMAC-SHA256 signature for verification (also in the header). |
    | `data`      | object | The full resource object that triggered the event.           |

    Respond with a `2xx` status as quickly as possible. Move any slow processing (database writes, downstream API calls) to a background job to avoid timeouts.
  </Step>
</Steps>

## Retry policy

If your endpoint does not return a `2xx` response within 10 seconds, Thiqwave retries the delivery with exponential backoff:

| Attempt   | Delay      |
| --------- | ---------- |
| 1st retry | 1 minute   |
| 2nd retry | 10 minutes |
| 3rd retry | 1 hour     |

After 3 failed retries, the delivery is marked as failed. You can view failed deliveries and manually replay them in the dashboard under **Settings → Webhooks → Delivery logs**.

<Note>
  Make your webhook handler idempotent. Because retries can deliver the same event more than once, use the `id` field to deduplicate events before processing them.
</Note>

## Testing webhooks

Use the **Send test event** button in the dashboard to fire a sample payload to your endpoint at any time. This lets you verify your handler logic without waiting for a real transaction.

You can also replay any past delivery from **Settings → Webhooks → Delivery logs**, which is useful for debugging failed handlers.

## Next steps

* [Settling with Stablecoins](/guides/crypto-settlement) — track settlement progress with webhook events
* [Receiving Payments](/guides/receiving-payments) — listen for `payin.completed` to react when customers pay
* [KYB Verification](/guides/kyb-verification) — handle `compliance.approved` for onboarding flows
