# Webhooks Setup
URL: https://www.quiltt.dev/webhooks/setup
Description: Subscribe to Quiltt webhook events in the Dashboard or via API, then verify signatures and handle incoming event payloads at your endpoint.
Navigation: webhooks → setup
Tags: Webhooks
Content Length: 9k characters

## How to Subscribe to Events

Manage Webhook subscriptions in the [Quiltt Dashboard](https://dashboard.quiltt.dev). You can also interact with your Webhook subscriptions via API, using [the `/webhooks/subscriptions` endpoint](https://www.quiltt.dev/api-reference/rest#tag/Subscriptions).

### Subscribing in the Dashboard

1. Log into the [Quiltt Dashboard](https://dashboard.quiltt.dev).
2. Click on the **Webhooks** link within your Environment.
3. Click on the **Create Webhook Subscription** button.

### Subscribing via API

Use the following endpoint to create a new Webhook subscription via API:

API Endpoint - post https://api.quiltt.io/v1/webhooks/subscriptions:

You must provide the following HTTP headers:

```http
Authorization: Bearer <API_SECRET_KEY>
Content-Type: application/json
```

Requests must include a JSON body with the `name` of your subscription, a list of `eventTypes`, and the `targetUrl` that receives the webhooks.

```json
["name": "Connection Created",
  "targetUrl": "https://example.com/webhooks/quiltt",
  "eventTypes": ["connection.created"]]
```

#### Example

This example request creates a subscription to `account.verified` events.

Code Examples:

  ```sh
    curl --request POST \
      --url 'https://api.quiltt.io/v1/webhooks/subscriptions' \
      --header 'Authorization: Bearer <API_SECRET_KEY>' \
      --header 'Content-Type: application/json' \
      --data-raw '["name": "Account Verified",
        "targetUrl": "https://example.com/webhooks/quiltt",
        "eventTypes": ["account.verified", "connection.synced.successful"]]'
    ```
  ```json
    ["id": "wh_14OYtCKXE7UeehJCdfPuV6e",
      "name": "Account Verified",
      "targetUrl": "https://example.com/webhooks/quiltt",
      "eventTypes": ["account.verified", "connection.synced.successful"],
      "disabled": false,
      "createdAt": "2025-09-03T18:33:03Z",
      "updatedAt": "2025-09-03T18:33:03Z"]
    ```

## How to Handle Incoming Events

When a Webhook event occurs, Quiltt sends a POST request to the `targetUrl` specified in the subscription. The request includes a JSON payload with the details of the event.

### Examples
The following examples show how to parse the received JSON payload and decide how to process the data of each event, based on its `type`.

Code Examples:

  ```ruby
    require 'sinatra'
    require 'json'
    require 'net/http'
    require 'openssl'

    QUILTT_WEBHOOK_SECRET  = '<QUILTT_WEBHOOK_SUBSCRIPTION_SECRET>'
    QUILTT_WEBHOOK_VERSION = 1
    QUILTT_WEBHOOK_WINDOW  = 300 # Five minutes

    # Using Sinatra
    post '/quiltt_webhook' do

      # Ensure the Unix Epoch time 'Quiltt-Timestamp' is current (within 5 minutes)
      if (timestamp = request.env['HTTP_QUILTT_TIMESTAMP'].to_i) < (Time.now.to_i - QUILTT_WEBHOOK_WINDOW)
        halt 204
      end

      # Validate 'Quiltt-Signature' matches a Base64 encoded HMAC-SHA256 of version+timestamp+payload.
      payload   = request.body.read
      signature = OpenSSL::HMAC.base64digest(
        'SHA256', QUILTT_WEBHOOK_SECRET,
        "#{QUILTT_WEBHOOK_VERSION}#{timestamp}#{payload}"
      )

      # @note Abort if payload fails signature verification
      if request.env['HTTP_QUILTT_SIGNATURE'] != signature
        halt 204
      end

      # Parse verified webhook payload
      webhook = JSON.parse(payload)

      # Handle each event
      webhook['events'].each do |event|
        case event['type']
        when /^connection\.synced\.successful(\.\w+)*/ # Connection has synced successfully
          connection = event['record']

          puts "Connection with ID #[connection['id']] has synced successfully!"
          # Fetch the latest data on associated accounts or transactions
          # ...
        when 'account.verified' # Account has been verified for payment processing
          account    = event['record']
          account_id = account['id']
          puts "Account with ID #{account_id} is ready for payment processing!"

          # Fetch the ACH numbers to pass to your payment provider
          endpoint = URI("https://api.quiltt.io/v1/accounts/#{account_id}/ach_numbers")
          response = Net::HTTP.get_response(endpoint, Authorization: "Bearer #[ENV['QUILTT_API_KEY_SECRET']]")
        else
          puts "Unhandled event #[event['type']]"
        end
      end

      status 204
    end
    ```
  ```typescript
    import express, [Request, Response] from 'express';
    import crypto from 'crypto';
    import https from 'https';

    const app = express();
    const PORT = 3000;

    const QUILTT_WEBHOOK_SECRET = '<QUILTT_WEBHOOK_SUBSCRIPTION_SECRET>';
    const QUILTT_WEBHOOK_VERSION = 1;
    const QUILTT_WEBHOOK_WINDOW = 300; // Five minutes

    // Capture the raw request body — the signature is computed over the exact
    // bytes Quiltt sent, so re-serializing the parsed object would not match.
    app.use(express.json({ verify: (req, _res, buf) => [(req as any).rawBody = buf;] }));

    interface WebhookEvent [type: string;
      record: any; // Use a more specific type if the structure of record is known]

    app.post('/quiltt_webhook', (req: Request, res: Response) => {
      const timestamp = req.header('Quiltt-Timestamp');
      if (!timestamp || (Date.now() / 1000 - Number(timestamp)) > QUILTT_WEBHOOK_WINDOW) [return res.status(204).send();]

      const payload = (req as any).rawBody.toString('utf8');
      const signature = crypto.createHmac('sha256', QUILTT_WEBHOOK_SECRET)
        .update(`${QUILTT_WEBHOOK_VERSION}${timestamp}${payload}`)
        .digest('base64');

      if (req.header('Quiltt-Signature') !== signature) [return res.status(204).send();]

      const webhook: [events: WebhookEvent[]] = req.body;

      webhook.events.forEach(event => {
        if (event.type.startsWith('connection.synced.successful')) {
          const connection = event.record;
          console.log(`Connection with ID $[connection.id] has synced successfully!`);
          // Additional logic to fetch latest data...
        } else if (event.type === 'account.verified') {
          const account = event.record;
          const account_id = account.id;
          console.log(`Account with ID ${account_id} is ready for payment processing!`);

          const options = {
            hostname: 'api.quiltt.io',
            path: `/v1/accounts/${account_id}/ach_numbers`,
            method: 'GET',
            headers: { Authorization: `Bearer $[process.env.QUILTT_API_KEY_SECRET]` },
          };

          https.get(options, response => [// Handle the HTTP response
            // Note: Add error handling and response parsing as needed.]);
        } else {
          console.log(`Unhandled event $[event.type]`);
        }
      });

      res.status(204).send();
    });

    app.listen(PORT, () => {
      console.log(`Server running on port ${PORT}`);
    });
    ```
  ```python
    from flask import Flask, request, jsonify
    from datetime import datetime
    import hmac
    import hashlib
    import base64
    import requests

    app = Flask(__name__)

    QUILTT_WEBHOOK_SECRET = '<QUILTT_WEBHOOK_SUBSCRIPTION_SECRET>'
    QUILTT_WEBHOOK_VERSION = '1'
    QUILTT_WEBHOOK_WINDOW = 300 # Five minutes

    @app.route('/quiltt_webhook', methods=['POST'])
    def quiltt_webhook():
      timestamp = request.headers.get('Quiltt-Timestamp')
      if not timestamp or (datetime.now().timestamp() - int(timestamp)) > QUILTT_WEBHOOK_WINDOW:
        return '', 204

      payload = request.get_data(as_text=True)
      signature = hmac.new(
        QUILTT_WEBHOOK_SECRET.encode(),
        f'{QUILTT_WEBHOOK_VERSION}{timestamp}{payload}'.encode(),
        hashlib.sha256
      ).digest()
      encoded_signature = base64.b64encode(signature).decode()

      if request.headers.get('Quiltt-Signature') != encoded_signature:
        return '', 204

      webhook = request.json

      for event in webhook.get('events', []):
        if event['type'].startswith('connection.synced.successful'):
          connection = event['record']
          print(f"Connection with ID [connection['id']] has synced successfully!")
          # Additional logic to fetch latest data...
        elif event['type'] == 'account.verified':
          account = event['record']
          account_id = account['id']
          print(f"Account with ID {account_id} is ready for payment processing!")

          response = requests.get(f"https://api.quiltt.io/v1/accounts/{account_id}/ach_numbers", headers={"Authorization": f"Bearer {YOUR_QUILTT_API_KEY_SECRET}"})
          # Ensure to handle the response properly here
        else:
          print(f"Unhandled event [event['type']]")

      return '', 204

    if __name__ == '__main__':
      app.run(debug=True)
    ```
  ```php
    <?php

    $QUILTT_WEBHOOK_SECRET = '<QUILTT_WEBHOOK_SUBSCRIPTION_SECRET>';
    $QUILTT_WEBHOOK_VERSION = '1';
    $QUILTT_WEBHOOK_WINDOW = 300; // Five minutes

    // Ensure the request is a POST
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') [http_response_code(405);
      exit;]

    // Verify timestamp is present and within the window
    $timestamp = isset($_SERVER['HTTP_QUILTT_TIMESTAMP']) ? $_SERVER['HTTP_QUILTT_TIMESTAMP'] : 0;
    if (time() - (int)$timestamp > $QUILTT_WEBHOOK_WINDOW) [http_response_code(204);
      exit;]

    // Read the payload
    $payload = file_get_contents('php://input');

    // Validate signature
    $signature = base64_encode(
      hash_hmac(
        'sha256',
        $QUILTT_WEBHOOK_VERSION . $timestamp . $payload,
        $QUILTT_WEBHOOK_SECRET,
        true
      )
    );

    if ($_SERVER['HTTP_QUILTT_SIGNATURE'] !== $signature) [http_response_code(204);
      exit;]

    // Parse the payload
    $webhook = json_decode($payload, true);

    // Handle the events
    foreach ($webhook['events'] as $event) {
      if (strpos($event['type'], 'connection.synced.successful') === 0) {
        $connection = $event['record'];
        error_log("Connection with ID [$connection['id']] has synced successfully!");
        // Additional logic here...
      } elseif ($event['type'] === 'account.verified') {
        $account = $event['record'];
        $account_id = $account['id'];
        error_log("Account with ID [$account_id] is ready for payment processing!");

        // Use cURL to fetch ACH numbers
        $ch = curl_init("https://api.quiltt.io/v1/accounts/[$account_id]/ach_numbers");
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer YOUR_QUILTT_API_KEY_SECRET'));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $response = curl_exec($ch);
        curl_close($ch);

        // Handle the response
      } else {
        error_log("Unhandled event [$event['type']]");
      }
    }

    http_response_code(204);
    ```