Skip to content
Platform

Platform Webhooks

Platform Webhooks send an HTTP request to a URL you control when something happens on your organization or one of its projects. For example, a project pausing and a member joining both raise events. Use them to build event-driven integrations without polling the Management API.

For the complete API reference, see Organization Webhooks and Project Webhooks in the Management API reference.

Concepts#

Endpoints#

An endpoint is a configuration for where and how to deliver events. It has a URL, a signing secret, the event types it subscribes to, and optional custom headers.

You register an endpoint against an organization or a specific project within an organization. Each endpoint subscribes to one or more event types, or to * to receive all events.

An endpoint is scoped to either an organization or a single project:

  • An organization-scoped endpoint receives every organization event and every project event across the organization.
  • A project-scoped endpoint receives events for that project only.

Endpoint URLs must be publicly reachable and must resolve to a non-reserved IP address.

Events#

An event represents something that happened in your organization or project. Each event has an ID, a type string such as v1.project.paused, a timestamp, and a payload.

Each event type starts with a version, for example v1, that represents the specific payload shape. Different versions of the same event can have different payload shapes.

A project event is delivered to both a matching project-scoped endpoint and any organization-scoped endpoints in the same organization. Each endpoint gets its own copy of the event, with its own id.

For the full list of event types and their payloads, see Platform Webhook Events.

Deliveries#

A delivery is one HTTP POST attempt for a specific event to a specific endpoint. Supabase records every delivery attempt along with its outcome, response status, and response body.

Envelope and payload#

Every delivery sends a JSON body with the following structure regardless of the type:

{
"id": "<event UUID>",
"type": "<event-type>",
"timestamp": "<ISO 8601 UTC>",
"payload": {
"organization_slug": "<org-slug>",
"project_ref": "<project-ref>"
// event-specific fields
}
}

For example:

{
"id": "01941e0e-e04e-7e43-b8e5-c4d79862b8df",
"type": "v1.project.paused",
"timestamp": "2025-01-01T12:00:00.000Z",
"payload": {
"organization_slug": "my-org",
"project_ref": "abcdefghijklmnoprstu",
"reason": "inactivity",
"actor": null
}
}

The payload object is event-specific. For organization-scoped events, project_ref is always null. For details on each event's payload fields, see Platform Webhook Events.

Versioning#

Each event type embeds a version prefix that identifies the payload schema. The v1 in v1.project.paused means the payload follows the version 1 shape for that event. A new version is introduced when the payload changes in a breaking way — fields are removed, renamed, or given a different type.

When Supabase introduces a new version, both versions are emitted simultaneously for a transition period. A handler built for v1 keeps receiving v1 events uninterrupted while you add support for v2. The transition window is finite, so treat an unrecognized version as a signal to upgrade your handler.

New fields can be added to an existing version without a version bump. Design your handler to ignore fields it does not recognize so that it stays forward compatible within a version.

Set up an endpoint#

Create an endpoint through the Management API:

  • To create an organization-scoped endpoint: POST https://api.supabase.com/v2/organizations/{organization_slug}/webhooks/endpoints
  • To create a project-scoped endpoint: POST https://api.supabase.com/v2/projects/{project_ref}/webhooks/endpoints

An endpoint takes these attributes:

AttributeRequiredNotes
urlYesMust use a domain that resolves to a non-reserved IP address. The API rejects a URL that resolves to a reserved range.
event_typesYesAt least one type. An endpoint receives only the types you list. Use * to receive every event.
signing_secretYes8 to 64 characters. Every delivery is signed with it.
custom_headersNoExtra request headers. Keys and values are strings, and each has to be a valid HTTP header.
descriptionNoFree text to identify the endpoint.
enabledNoDefaults to true. When disabled, events already enqueued get a delivery record with skipped status but nothing is sent; events that arrive after disabling are dropped with no delivery record.
curl -X POST 'https://api.supabase.com/v2/organizations/{your-org-slug}/webhooks/endpoints' \
-H 'Authorization: Bearer <personal-access-token>' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"type": "endpoint",
"attributes": {
"url": "https://your-domain.com/webhooks/supabase",
"signing_secret": "your-signing-secret",
"description": "Track pause and restore events for all projects",
"event_types": [
{ "type": "v1.project.paused" },
{ "type": "v1.project.restored" }
]
}
}
}'

To receive all event types, set event_types to [{ "type": "*" }]. Using * also covers event types added in the future, so design your handler to handle unrecognized types gracefully — see Versioning.

A successful request returns 201 with the endpoint's ID, which you can use to send test events, list deliveries, and update or delete the endpoint.

For the full set of operations, see the Project webhooks and Organization webhooks sections of the Management API reference. Every operation exists at both scopes.

Verify webhook signature#

Every delivery follows the Standard Webhooks specification and carries these headers:

HeaderDescription
webhook-idThe event ID. It's the same across every retry of the same event.
webhook-timestampThe Unix timestamp, in seconds, of when the delivery attempt was signed.
webhook-signatureAn HMAC-SHA256 signature over {webhook-id}.{webhook-timestamp}.{raw body}, in the form v1,<base64>.

Verify against the raw request body before you parse it as JSON:

If your signing_secret is a base64-encoded value prefixed with whsec_:

import { Webhook } from 'standardwebhooks'
const webhook = new Webhook(process.env.WEBHOOK_SECRET)
try {
const event = webhook.verify(rawBody, {
'webhook-id': req.headers['webhook-id'],
'webhook-timestamp': req.headers['webhook-timestamp'],
'webhook-signature': req.headers['webhook-signature'],
})
// event is the verified, event.payload can be used
} catch {
return res.status(400).send('Invalid signature')
}

For verification libraries in other languages, see the Standard Webhooks documentation.

To rotate a signing secret without missing deliveries:

  1. Add the new secret to your listener, and accept signatures made with either the old or the new secret.
  2. Update the endpoint to use the new secret.
  3. Remove the old secret from your listener.

Send a test event#

Use a test event to verify connectivity end to end. It goes through the same pipeline as a real event and is stored in delivery history, except that automatic retries are disabled.

curl -X POST https://api.supabase.com/v2/organizations/{your-org-slug}/webhooks/endpoints/{your-endpoint-id}/test \
-H 'Authorization: Bearer <personal-access-token>' \
-H 'Content-Type: application/json' \
--data '{
"data": {
"type": "event",
"attributes": { "type": "v1.project.paused" }
}
}'

The event type in the body is optional. Without one, the API uses a type the endpoint subscribes to.

A test event's payload includes "is_test": true, so check that field before you trigger any side effect.

Delivery behavior#

Supabase considers a delivery successful when your endpoint returns a 2xx response within 20 seconds. A non-2xx response or a timeout counts as a failure and triggers a retry with exponential backoff:

AttemptWait after the previous failureApproximate time since the event
1Not applicableImmediately
210 seconds~10 seconds
330 seconds~40 seconds
41 minute 30 seconds~2 minutes 10 seconds
5 (final)4 minutes 30 seconds~6 minutes 40 seconds

After five failed attempts, the delivery is marked as failed with no further retries. Supabase does not notify you when this happens — check delivery history to find permanently failed deliveries.

You can retry a delivery manually. A manual retry is a single attempt: if it fails, no automatic retries follow.

Delivery history is retained for 14 days. An expired delivery is no longer listed and can't be retried.

Idempotency and ordering#

Deliveries are at-least-once: Supabase retries failed deliveries, so your endpoint may receive the same event more than once. Use the id field in the request body as a deduplication key. The same id always represents the same event.

Deliveries and events may also arrive out of order due to network delays or hiccups. You need to design your webhook listener to account for duplicates and out-of-order events.

Best practices#

Best practices for consuming webhook events and building a reliable, production-grade webhook listener:

  • Always verify the webhook signature before processing an event.
  • Check the event type version prefix before processing a payload. For unsupported versions, return 2xx without processing and log or alert on the unrecognized version — the transition window is finite, so upgrade your handler promptly.
  • Respond to a webhook immediately and process each event asynchronously, for example, on a job queue.
  • Keep track of already processed events in persistent storage to deduplicate them, for example, in a database.
  • Keep track of the last processed event's timestamp and discard stale events of the same type. Do not rely on the order of events.
  • Treat webhook events as a trigger for an action, rather than the state of a resource.
  • Use the webhook's automatic retries as a safeguard against network partitions or timeouts, not as a fallback for listener logic errors.
  • Periodically check delivery history for permanently failed deliveries. Supabase does not notify you when all retries are exhausted.