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

# Webhooks

> Create endpoints to receive asynchronous events from your Prequel integration, such as transfer failures.

You may wish to create endpoints to receive asynchronous events from your Prequel integration, such as transfer failures. Webhook endpoints created using the `POST /webhooks` [endpoint](/export/api-reference/webhooks/create-webhook) can subscribe to any of Prequel's [Event Types](/export/monitoring/webhooks#event-types) and configure delivery via HTTPS, Slack, or Pagerduty.

Choose a delivery method to get started:

<CardGroup cols={3}>
  <Card title="Slack webhooks" icon="slack" href="/export/monitoring/slack">
    Deliver events into a Slack channel for the team to triage.
  </Card>

  <Card title="Datadog webhooks" icon="dog" href="/export/monitoring/datadog">
    Forward events into Datadog as monitor-ready events.
  </Card>

  <Card title="Generic HTTP webhooks" icon="webhook" href="#http-post-and-get">
    Send `HTTPS` POST or GET callbacks to your own receiver.
  </Card>
</CardGroup>

For receiving sensitive trace alerts during transfers, see [Sensitive Trace Webhooks](/export/error-handling/sensitive-trace-webhook).

## Delivery methods

### HTTP POST and GET

Prequel supports `HTTPS` callbacks to your custom webhook receiver. Creating a webhook endpoint as the `generic_post` type will cause payloads to be sent as a JSON payload. The `generic_get` type will deliver payloads as URL parameters. The payloads are defined in Event Types.

### PagerDuty, Slack & Datadog

You may want to have events sent to specific vendors for special handling of webhook requests. Currently, PagerDuty, Slack and Datadog are supported, with specific payload shapes according to the vendor specifications.

For detailed instructions on integrating Prequel webhooks with Slack or Datadog please visit the following pages:

* [Slack Integration](/export/monitoring/slack)
* [Datadog Integration](/export/monitoring/datadog)

## Authentication

You can provide an API key for the webhook to use if the destination requires one. Additionally, Prequel signs every payload and passes the signature through the `X-Prequel-Webhook-Signature` header. See [Verifying Webhooks](/export/monitoring/webhooks#verifying-webhooks) for more information on handling the signature.

## Versioning

Prequel uses a top-level `api_version` field. A version is represented by the date it was released. Currently, all webhooks use the version `2023-10-15`.

## Contents & structure

All event types follow this structure:

### Headers

| Header                        | Description                                                                                                                                                     |
| :---------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`                | Always `application/json`                                                                                                                                       |
| `X-Prequel-Webhook-Timestamp` | Timestamp generated when the event is sent.                                                                                                                     |
| `X-Prequel-Webhook-Signature` | Signature generated by Prequel using `SHA-256` and `RSA PKCS1 v1.5` signature scheme. See [Verifying Webhooks](/export/monitoring/webhooks#verifying-webhooks). |
| `X-Prequel-Webhook-Digest`    | Optional utility for signature verification. See [Verifying Webhooks](/export/monitoring/webhooks#verifying-webhooks).                                          |

### Body

```json title="Body structure" icon="brackets-curly" expandable theme={null}
{
  "type": "resource.event",
  "api_version": "XXXX-XX-XX",
  "created_at": ..., // timestamp generated when the event is created
  "data": {
    // event specific
  }
}
```

## Event types

When creating a webhook, you must specify which events it should listen to. Webhooks created before October 31 2023 will continue to receive only transfer errors.

Webhooks are available for the following event types:

* `transfer.success`
* `transfer.error`
* `transfer.cancelled`
* `export_source.created`
* `export_source.updated`
* `export_source.deleted`
* `export_destination.created`
* `export_destination.updated`
* `export_destination.deleted`
* `recipient.created`
* `recipient.updated`
* `recipient.deleted`
* `export_magic_link.created`
* `export_magic_link.deleted`

Each event type's payload is shown below. Select a tab to see the example for a given event.

### Transfer events

<Tabs>
  <Tab title="transfer.success">
    This event has type `transfer.success`. It is sent whenever Prequel identifies a successful transfer.

    ```json title="transfer.success payload" icon="brackets-curly" expandable theme={null}
    {
      "type":"transfer.success",
      "api_version": "2023-10-15",
      "created_at": "2023-10-15T19:19:08+00:00",
      "data":{
        "destination_id":"00000000-0000-0000-0000-000000000000",
        "destination_name":"Foo Bar",
        "id_in_provider_system":"acme",
        "in_app_url":"https://app.prequel.co/destinations/00000000-0000-0000-0000-000000000000",
        "is_full_refresh":true,
        "models":["users"],
        "rows_transferred":1000,
        "transfer_id":"00000000-0000-0000-0000-000000000000",
        "transfer_log":"users - succeeded",
        "transfer_log_pretty":"users - succeeded",
        "transfer_started_at": "2023-10-15T16:19:08+00:00",
        "transfer_ended_at": "2023-10-15T18:19:08+00:00",
        "transfer_status":"SUCCESS"
      },
    }
    ```
  </Tab>

  <Tab title="transfer.error">
    This event has type `transfer.error`. It is sent whenever Prequel identifies a partial or full transfer failure. Note the following fields which do not appear on the transfer object itself:

    * `transfer_blamed_party`: May be any of `first_party`, `third_party`, `prequel`, `host`, or `unknown`. Defaults to `prequel`. `first_party` indicates an error with your Prequel integration. `third_party` indicates an error caused by your customer; for example, a change in existing credentials for a destination. `host` is only applicable if you are self-hosting Prequel.
    * `transfer_log`: Per-model breakdown of the transfer result. Each line follows the format `{model_name} - {status}`, where status is one of `succeeded`, `skipped, no updates`, or a raw error message including a `PRQL-XXXX` coded error code.
    * `transfer_log_pretty`: If `transfer_blamed_party` is `third_party`, a modified error string suitable for displaying to customers. Otherwise empty.

    ```json title="transfer.error payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "transfer.error",
      "api_version": "2023-10-15",
      "created_at": "2023-10-15T19:19:08+00:00",
      "data": {
        "destination_id": "00000000-0000-0000-0000-000000000000",
        "destination_name": "Foo Bar",
        "id_in_provider_system": "acme",
        "models": ["users", "orders", "products", "reports"],
        "rows_transferred": 1179,
        "transfer_blamed_party": "third_party",
        "is_full_refresh": true,
        "transfer_id": "00000000-0000-0000-0000-000000000000",
        "transfer_status": "PARTIAL_FAILURE",
        "transfer_log": "users - succeeded\norders - succeeded\nproducts - skipped, no updates\nreports - unable to execute operation \"athena.source.read\": unable to read athena source: unable to create athena source query view: Driver Error: unable to execute create view query: Query execution timed out [PRQL-XIEA]: coded error message: The Athena query execution timed out. If this error persists, please contact support.",
        "transfer_log_pretty": "users - succeeded\norders - succeeded\nproducts - skipped, no updates\nreports - The Athena query execution timed out. If this error persists, please contact support.",
        "transfer_started_at": "2023-10-15T16:19:08+00:00",
        "transfer_ended_at": "2023-10-15T18:19:08+00:00",
        "in_app_url": "https://app.prequel.co/destinations/00000000-0000-0000-0000-000000000000"
      }
    }
    ```
  </Tab>

  <Tab title="transfer.cancelled">
    This event has type `transfer.cancelled`. It is sent whenever Prequel identifies a cancelled or killed transfer. A transfer could be cancelled by the user when it is in the `PENDING` state, or it could be killed by the system or user if it is in the `RUNNING` state. Both actions will trigger a `transfer.cancelled` event.

    ```json title="transfer.cancelled payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "transfer.cancelled",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T20:54:51.971902Z",
      "data": {
        "destination_id": "00000000-0000-0000-0000-000000000000",
        "destination_name": "Foo Bar",
        "id_in_provider_system": "acme",
        "in_app_url": "https://app.prequel.co/destinations/00000000-0000-0000-0000-000000000000",
        "is_full_refresh": false,
        "models": ["users"],
        "rows_transferred": 0,
        "transfer_ended_at": "2025-02-20T20:54:51.971902Z",
        "transfer_id": "00000000-0000-0000-0000-000000000000",
        "transfer_log": "Pending transfer cancelled by user.",
        "transfer_log_pretty": "",
        "transfer_started_at": null,
        "transfer_status": "CANCELLED"
      }
    }
    ```
  </Tab>
</Tabs>

### Source events

<Tabs>
  <Tab title="export_source.created">
    This event has type `export_source.created`. It is sent whenever Prequel identifies that a new source has been created. This example shows a new Snowflake source.

    ```json title="export_source.created payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "export_source.created",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T17:54:18.453379Z",
      "data": {
        "current_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "name": "Acme Inc",
          "port": 443,
          "vendor": "snowflake",
          "database": "source_database",
          "username": "snowflake_user",
          "created_at": "2025-02-20T12:54:18.445223-05:00",
          "updated_at": "2025-02-20T12:54:18.445223-05:00",
          "disable_ssl": false,
          "use_ssh_tunnel": false,
          "ssh_tunnel_host": "",
          "ssh_tunnel_port": 22,
          "ssh_tunnel_username": "",
          "gcp_iam_role_metadata": {
            "type": "",
            "audience": "",
            "token_url": "",
            "credential_source": {
              "url": "",
              "region_url": "",
              "environment_id": "",
              "regional_cred_verification_url": ""
            },
            "subject_token_type": "",
            "service_account_impersonation_url": ""
          },
          "max_concurrent_transfers": 1,
          "is_bucket_credentials_implicit": false,
          "max_concurrent_queries_per_transfer": 1
        },
        "previous_resource": null
      }
    }
    ```
  </Tab>

  <Tab title="export_source.updated">
    This event has type `export_source.updated`. It is sent whenever Prequel identifies that an existing source has been updated. In this example, you can see the `database` field has been updated to `source_db_updated`.

    ```json title="export_source.updated payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "export_source.updated",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T17:57:04.201273Z",
      "data": {
        "current_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "name": "Acme Inc",
          "port": 443,
          "vendor": "snowflake",
          "database": "source_db_updated",
          "username": "snowflake_user",
          "created_at": "2025-02-20T12:54:18.445223-05:00",
          "updated_at": "2025-02-20T12:57:04.195446-05:00",
          "disable_ssl": false,
          "use_ssh_tunnel": false,
          "ssh_tunnel_host": "",
          "ssh_tunnel_port": 22,
          "ssh_tunnel_username": "",
          "gcp_iam_role_metadata": {
            "type": "",
            "audience": "",
            "token_url": "",
            "credential_source": {
              "url": "",
              "region_url": "",
              "environment_id": "",
              "regional_cred_verification_url": ""
            },
            "subject_token_type": "",
            "service_account_impersonation_url": ""
          },
          "max_concurrent_transfers": 1,
          "is_bucket_credentials_implicit": false,
          "max_concurrent_queries_per_transfer": 1
        },
        "previous_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "name": "Acme Inc",
          "port": 443,
          "vendor": "snowflake",
          "database": "source_db",
          "username": "snowflake_user",
          "created_at": "2025-02-20T12:54:18.445223-05:00",
          "updated_at": "2025-02-20T12:54:18.445223-05:00",
          "disable_ssl": false,
          "use_ssh_tunnel": false,
          "ssh_tunnel_host": "",
          "ssh_tunnel_port": 22,
          "bucket_secret_key": "[redacted]",
          "ssh_tunnel_username": "",
          "gcp_iam_role_metadata": {
            "type": "",
            "audience": "",
            "token_url": "",
            "credential_source": {
              "url": "",
              "region_url": "",
              "environment_id": "",
              "regional_cred_verification_url": ""
            },
            "subject_token_type": "",
            "service_account_impersonation_url": ""
          },
          "max_concurrent_transfers": 1,
          "is_bucket_credentials_implicit": false,
          "max_concurrent_queries_per_transfer": 1
        }
      }
    }
    ```
  </Tab>

  <Tab title="export_source.deleted">
    This event has type `export_source.deleted`. It is sent whenever Prequel identifies that an existing source has been deleted.

    ```json title="export_source.deleted payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "export_source.deleted",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T17:59:09.416441Z",
      "data": {
        "current_resource": null,
        "previous_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "name": "Acme Inc",
          "port": 443,
          "vendor": "snowflake",
          "database": "source_db_updated",
          "username": "snowflake_user",
          "created_at": "2025-02-20T12:54:18.445223-05:00",
          "updated_at": "2025-02-20T12:57:04.195446-05:00",
          "disable_ssl": false,
          "use_ssh_tunnel": false,
          "ssh_tunnel_host": "",
          "ssh_tunnel_port": 22,
          "ssh_tunnel_username": "",
          "gcp_iam_role_metadata": {
            "type": "",
            "audience": "",
            "token_url": "",
            "credential_source": {
              "url": "",
              "region_url": "",
              "environment_id": "",
              "regional_cred_verification_url": ""
            },
            "subject_token_type": "",
            "service_account_impersonation_url": ""
          }
        }
      }
    }
    ```
  </Tab>
</Tabs>

### Destination events

<Tabs>
  <Tab title="export_destination.created">
    This event has type `export_destination.created`. It is sent whenever Prequel identifies that a new destination has been created.

    ```json title="export_destination.created payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "export_destination.created",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T18:12:45.409267Z",
      "data": {
        "current_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "name": "Acme Inc",
          "port": 443,
          "schema": "webhook_schema",
          "vendor": "snowflake",
          "database": "destination_db",
          "products": ["billing", "invoices"],
          "username": "snowflake_user",
          "created_at": "2025-02-20T13:12:45.360283-05:00",
          "is_enabled": true,
          "updated_at": "2025-02-20T13:12:45.360283-05:00",
          "disable_ssl": false,
          "recipient_id": "00000000-0000-0000-0000-000000000000",
          "enabled_models": ["*"],
          "use_ssh_tunnel": false,
          "ssh_tunnel_host": "",
          "ssh_tunnel_port": 0,
          "frequency_minutes": 0,
          "ssh_tunnel_username": "",
          "gcp_iam_role_metadata": {
            "type": "",
            "audience": "",
            "token_url": "",
            "credential_source": {
              "url": "",
              "region_url": "",
              "environment_id": "",
              "regional_cred_verification_url": ""
            },
            "subject_token_type": "",
            "service_account_impersonation_url": ""
          },
          "id_in_provider_system": "tenant_id",
          "max_concurrent_transfers": 1,
          "is_bucket_credentials_implicit": false,
          "last_successful_transfer_ended_at": null,
          "max_concurrent_queries_per_transfer": 1
        },
        "previous_resource": null
      }
    }
    ```
  </Tab>

  <Tab title="export_destination.updated">
    This event has type `export_destination.updated`. It is sent whenever Prequel identifies that an existing destination has been updated.

    ```json title="export_destination.updated payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "export_destination.updated",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T18:15:11.411273Z",
      "data": {
        "current_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "name": "Acme Inc",
          "port": 443,
          "schema": "new_webhook_schema",
          "vendor": "snowflake",
          "database": "destination_db",
          "products": ["billing", "invoices"],
          "username": "snowflake_user",
          "created_at": "2025-02-20T13:12:45.360283-05:00",
          "is_enabled": true,
          "updated_at": "2025-02-20T13:15:11.398688-05:00",
          "disable_ssl": false,
          "recipient_id": "00000000-0000-0000-0000-000000000000",
          "enabled_models": ["*"],
          "use_ssh_tunnel": false,
          "ssh_tunnel_host": "",
          "ssh_tunnel_port": 0,
          "ssh_tunnel_username": "",
          "gcp_iam_role_metadata": {
            "type": "",
            "audience": "",
            "token_url": "",
            "credential_source": {
              "url": "",
              "region_url": "",
              "environment_id": "",
              "regional_cred_verification_url": ""
            },
            "subject_token_type": "",
            "service_account_impersonation_url": ""
          },
          "id_in_provider_system": "tenant_id",
          "max_concurrent_transfers": 1,
          "is_bucket_credentials_implicit": false,
          "last_successful_transfer_ended_at": null,
          "max_concurrent_queries_per_transfer": 1
        },
        "previous_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "name": "Acme Inc",
          "port": 443,
          "schema": "webhook_schema",
          "vendor": "snowflake",
          "database": "destination_db",
          "products": ["billing", "invoices"],
          "username": "snowflake_user",
          "created_at": "2025-02-20T13:12:45.360283-05:00",
          "is_enabled": true,
          "updated_at": "2025-02-20T13:12:45.360283-05:00",
          "disable_ssl": false,
          "recipient_id": "00000000-0000-0000-0000-000000000000",
          "enabled_models": ["*"],
          "use_ssh_tunnel": false,
          "ssh_tunnel_host": "",
          "ssh_tunnel_port": 0,
          "ssh_tunnel_username": "",
          "gcp_iam_role_metadata": {
            "type": "",
            "audience": "",
            "token_url": "",
            "credential_source": {
              "url": "",
              "region_url": "",
              "environment_id": "",
              "regional_cred_verification_url": ""
            },
            "subject_token_type": "",
            "service_account_impersonation_url": ""
          },
          "id_in_provider_system": "tenant_id",
          "max_concurrent_transfers": 1,
          "is_bucket_credentials_implicit": false,
          "last_successful_transfer_ended_at": null,
          "max_concurrent_queries_per_transfer": 1
        }
      }
    }
    ```
  </Tab>

  <Tab title="export_destination.deleted">
    This event has type `export_destination.deleted`. It is sent whenever Prequel identifies that an existing destination has been deleted.

    ```json title="export_destination.deleted payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "export_destination.deleted",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T18:17:31.963946Z",
      "data": {
        "current_resource": null,
        "previous_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "name": "Acme Inc",
          "port": 443,
          "schema": "new_webhook_schema",
          "vendor": "snowflake",
          "database": "destination_db",
          "products": ["billing", "invoices"],
          "username": "snowflake_user",
          "created_at": "2025-02-20T13:12:45.360283-05:00",
          "is_enabled": true,
          "updated_at": "2025-02-20T13:15:11.398688-05:00",
          "disable_ssl": false,
          "recipient_id": "00000000-0000-0000-0000-000000000000",
          "enabled_models": ["*"],
          "use_ssh_tunnel": false,
          "ssh_tunnel_host": "",
          "ssh_tunnel_port": 0,
          "ssh_tunnel_username": "",
          "gcp_iam_role_metadata": {
            "type": "",
            "audience": "",
            "token_url": "",
            "credential_source": {
              "url": "",
              "region_url": "",
              "environment_id": "",
              "regional_cred_verification_url": ""
            },
            "subject_token_type": "",
            "service_account_impersonation_url": ""
          },
          "id_in_provider_system": "tenant_id",
          "max_concurrent_transfers": 1,
          "is_bucket_credentials_implicit": false,
          "last_successful_transfer_ended_at": null,
          "max_concurrent_queries_per_transfer": 1
        }
      }
    }
    ```
  </Tab>
</Tabs>

### Recipient events

<Tabs>
  <Tab title="recipient.created">
    This event has type `recipient.created`. It is sent whenever Prequel identifies that a new recipient has been created.

    ```json title="recipient.created payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "recipient.created",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T17:44:10.769913Z",
      "data": {
        "current_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "name": "Acme Inc",
          "id_in_provider_system": "acme_inc",
          "products": ["billing", "invoices"],
          "created_at": "2025-02-20T12:44:10.763224-05:00",
          "updated_at": "2025-02-20T12:44:10.763224-05:00",
        },
        "previous_resource": null
      }
    }
    ```
  </Tab>

  <Tab title="recipient.updated">
    This event has type `recipient.updated`. It is sent whenever Prequel identifies that an existing recipient has been updated. In this example, you can see that the `products` field has been updated to include `invoices`.

    ```json title="recipient.updated payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "recipient.updated",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T17:45:56.752967Z",
      "data": {
        "current_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "name": "Acme Inc",
          "id_in_provider_system": "acme_inc",
          "products": ["billing", "invoices"],
          "created_at": "2025-02-20T12:44:10.763224-05:00",
          "updated_at": "2025-02-20T12:45:56.746663-05:00",
        },
        "previous_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "name": "Acme Inc",
          "id_in_provider_system": "acme_inc",
          "products": ["billing"],
          "created_at": "2025-02-20T12:44:10.763224-05:00",
          "updated_at": "2025-02-20T12:44:10.763224-05:00",
        }
      }
    }
    ```
  </Tab>

  <Tab title="recipient.deleted">
    This event has type `recipient.deleted`. It is sent whenever Prequel identifies that an existing recipient has been deleted.

    ```json title="recipient.deleted payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "recipient.deleted",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T17:48:35.128964Z",
      "data": {
        "current_resource": null,
        "previous_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "name": "Acme Inc",
          "id_in_provider_system": "acme_inc",
          "products": ["billing", "invoices"],
          "created_at": "2025-02-20T12:44:10.763224-05:00",
          "updated_at": "2025-02-20T12:45:56.746663-05:00"
        }
      }
    }
    ```
  </Tab>
</Tabs>

### Magic link events

<Tabs>
  <Tab title="export_magic_link.created">
    This event has type `export_magic_link.created`. It is sent whenever Prequel identifies that a new magic link has been created.

    ```json title="export_magic_link.created payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "export_magic_link.created",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T21:13:53.155964Z",
      "data": {
        "current_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "link": "https://app.prequel.co/orgs/00000000-0000-0000-0000-000000000000/links/00000000-0000-0000-0000-000000000000",
          "name": "Webhook Magic Link Acme Inc",
          "vendor": "snowflake",
          "products": ["billing", "invoices"],
          "created_at": "2025-02-20T16:13:53.149884-05:00",
          "bucket_name": "",
          "recipient_id": "00000000-0000-0000-0000-000000000000",
          "has_been_used": false,
          "ssh_public_key": "ssh-rsa...prequel-ssh-tunneling-public-key",
          "available_models": ["Customers", "Invoices"],
          "set_destination_enabled": true
        },
        "previous_resource": null
      }
    }
    ```
  </Tab>

  <Tab title="export_magic_link.deleted">
    This event has type `export_magic_link.deleted`. It is sent whenever Prequel identifies that an existing magic link has been deleted.

    ```json title="export_magic_link.deleted payload" icon="brackets-curly" expandable theme={null}
    {
      "type": "export_magic_link.deleted",
      "api_version": "2023-10-15",
      "created_at": "2025-02-20T21:16:58.035652Z",
      "data": {
        "current_resource": null,
        "previous_resource": {
          "id": "00000000-0000-0000-0000-000000000000",
          "host": "snowflake-webhook.region.cloud.snowflakecomputing.com",
          "link": "https://app.prequel.co/orgs/00000000-0000-0000-0000-000000000000/links/00000000-0000-0000-0000-000000000000",
          "name": "Webhook Magic Link Acme Inc",
          "vendor": "snowflake",
          "products": ["billing", "invoices"],
          "created_at": "2025-02-20T16:13:53.149884-05:00",
          "bucket_name": "",
          "recipient_id": "00000000-0000-0000-0000-000000000000",
          "has_been_used": false,
          "ssh_public_key": "ssh-rsa...prequel-ssh-tunneling-public-key",
          "available_models": ["Customers", "Invoices"],
          "id_in_provider_system": "acme",
          "set_destination_enabled": true
        }
      }
    }
    ```
  </Tab>
</Tabs>

## Verifying webhooks

Prequel provides a unique signature in the HTTP header of each webhook request. You can use this signature and your account's webhook public key to verify that the data you receive is from Prequel.

### Webhook signatures

Prequel's approach to webhook signatures is based on asymmetric cryptography. Prequel generates an [RSA](https://en.wikipedia.org/wiki/RSA_\(cryptosystem\)) private and public key pair for your account's webhook integration. After generating a webhook, Prequel digitally signs the payload with the private key. On receiving the webhook, you can use the public key to verify the authenticity and freshness of this signature. See [Verify the signature](/export/monitoring/webhooks#verify-the-signature).

### Relevant headers

| Header                             | Description                                                                               |
| :--------------------------------- | :---------------------------------------------------------------------------------------- |
| `X-Prequel-Webhook-Timestamp`      | Timestamp the webhook was sent, in `RFC 3339` format.                                     |
| `X-Prequel-Webhook-Signature`      | Hex-encoded signature generated by Prequel using the signing data.                        |
| (Maybe) `X-Prequel-Webhook-Digest` | Hex-encoded SHA-256 hash of the payload only, provided by Prequel for debugging purposes. |

### Verify the signature

The general steps to verify a signature are outlined below.

#### 1. Retrieve your current webhook key

You can retrieve your current webhook key by hitting the following API endpoint `/public/signatures/webhook-public-key`.

#### 2. Reconstruct the signing data

Extract the timestamp provided in the `X-Prequel-Webhook-Timestamp` header. The date is in RFC 3339 format and  looks something like:
`X-Prequel-Webhook-Timestamp: 2023-10-15T14:30:00Z`

The signing data is in the format `timestamp.body`, constructed by concatenating

* The timestamp
* The character `.`
* The request body, i.e. the raw JSON payload

Finally, hash the **full signing data** using `SHA-256`.

<Warning>
  Prequel uses the raw body of the request to generate a signature. You should hash the raw body string (or bytes) before deserializing the JSON payload. Frameworks that parse the response body may introduce subtle changes, such as removing characters or changing key-sort order.
</Warning>

##### Validate the response hash

Prequel hashes the raw body only and provides the result in the `X-Prequel-Webhook-Digest` header. You can use this to validate that you are handling the response body correctly by hex-decoding the value and comparing it to your SHA-256 hash of the raw body. **You should not use this to confirm the signature.**

#### 3. Confirm the signature

Once you've reconstructed and hashed the signing data, you can sign the data with your public key and compare the output to the signature in the `X-Prequel-Webhook-Signature` header. Make sure to decode Prequel's signature from hex before comparing.

Prequel uses the PKCS1 v1.5 signature scheme.

#### 4. Verify timing

A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. The provided timestamp is verified by the signature, so an attacker can't change the timestamp without invalidating the signature. However, even if the attacker cannot alter the data, they may produce side-effects from processing the same event multiple times. We recommend defining a time window (such as 5 minutes) and rejecting events that are too old.
