Event types
Three event types are available today. One endpoint can subscribe to any combination of them.
For the full payload of each event, see the Webhooks group in the API reference.
Envelope
Every event is wrapped in the same envelope:Endpoints
A webhook endpoint is a URL on your server, an event-type subscription list, and a signing secret. Each endpoint is scoped to a single organization. You can have multiple endpoints - for example, separate URLs for staging and production, or per-team fan-out.Create an endpoint
From the dashboard, open Webhooks → Endpoints, click New endpoint, paste your HTTPS URL, name it, and check the events you want to receive. Copy thesigning_secret from the success screen - it is shown once. Store it as FOIL_WEBHOOK_SECRET (or similar) on the receiving service.
To create an endpoint via the API, POST /v1/organizations/{organizationId}/webhooks/endpoints with an sk_* key that has the webhooks:manage scope:
signing_secret exactly once. Persist it before discarding the response.
URL requirements
Foil validates every endpoint URL on create, on update, and again before each delivery attempt:- HTTPS only in production. Plain HTTP is allowed only against
localhost/127.0.0.1in non-production environments. - No credentials in the URL -
https://user:pass@host/pathis rejected. - Public IPs only. Foil resolves the hostname and refuses to deliver to private, reserved, or link-local ranges. This protects your infrastructure from SSRF-style misuse if a webhook secret leaks.
- Reachable. DNS must resolve and the request must complete within the per-attempt timeout (10 seconds).
Event subscriptions
A subscription is the(endpoint, event_type) pair. When you PATCH .../endpoints/{endpointId} with a new event_types array, Foil replaces the endpoint’s subscriptions - pass the full desired set, not just additions.
Disable, re-enable, or delete
- Disable -
PATCH .../endpoints/{endpointId}with{"status": "disabled"}. New events are not queued for the endpoint; in-flight deliveries finish inskippedstate. - Re-enable -
PATCH .../endpoints/{endpointId}with{"status": "active"}. Past skipped deliveries are not replayed; only new events are delivered. - Delete -
DELETE .../endpoints/{endpointId}soft-disables the endpoint. The endpoint and its event history remain visible for auditing.
Send a test event
POST .../endpoints/{endpointId}/test (or Send test event in the dashboard) enqueues a delivery with type: "webhook.test" and a tiny payload. The signature flow is identical to production events, so a passing test verifies your verification code as well as connectivity.
Authentication
Every Foil webhook is signed with HMAC-SHA256. Verify the signature before reading the body - without it, anyone who knows your URL can post arbitrary payloads.What gets signed
For every request, Foil computes:X-Foil-Signature. The signing secret has the format whsec_<base64url> and is shown once when you create or rotate the endpoint.
The timestamp is the Unix epoch in seconds, as a string. Both headers are required; missing or malformed values must be rejected as 401.
Verify the signature
crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal, Rack::Utils.secure_compare, hash_equals) - naive == leaks information about partial matches.
Replay protection
The timestamp is signed, so an attacker cannot reuse a captured request with a forged body. To also reject replays of the original request, reject anything whose timestamp is more than a few minutes old:id to drop legitimate retries - see Idempotency.
Rotating secrets
POST .../endpoints/{endpointId}/rotations (or Rotate secret in the dashboard) returns a new signing_secret and immediately uses it for outgoing deliveries. Rotate when:
- a teammate with access to the secret leaves
- the secret may have been logged or committed
- on a regular schedule (annually is a reasonable default)
- Add the new secret as a second verifier in your code, alongside the current one.
- Deploy. Both signatures now verify.
- Rotate via the API. Foil signs new requests with the new secret.
- After the longest possible retry window (≥ 10 minutes covers all delivery attempts), drop the old secret from your code and redeploy.
Delivery
The request
Every webhook is an HTTPPOST with a JSON body and these headers:
There is no fixed source IP range and no IP allowlist. Authenticate via the signature, not the network.
What counts as success
- 2xx - delivery succeeds. The endpoint will not be retried for this event.
- anything else, plus connection errors and timeouts - delivery fails and is retried (up to the limit below).
Retries
If a delivery is not 2xx, Foil retries up to 5 attempts total. The first retry waits at least one minute; subsequent retries use exponential backoff and run on a worker queue, so the actual delay can extend during high load. Treat all timing as a lower bound - design for “delivered eventually” rather than “delivered every minute on the dot.” After the 5th failed attempt the delivery moves to terminalfailed status and stops retrying. Re-deliver manually from the dashboard or by sending a fresh test event.
Delivery status values you’ll see in the dashboard and the API:
Idempotency
You may receive the same event ID more than once - retries after a transient 5xx, network blips, or worker reschedules. Make your handler idempotent onX-Foil-Event:
(event_id, endpoint_id), so the same event never produces parallel deliveries to one endpoint. But your handler must still tolerate retries of the same event ID over time.
For Gate’s gate.session.approved event, also key on data.gate_session_id - the Gate webhook quickstart shows the pattern.
Event log
Every event is recorded with its delivery attempts. View it in two places:- Dashboard - Webhooks → Events lists recent events with delivery status, attempt count, response code, and the captured response body. Filter by endpoint or event type to debug a specific receiver.
- API -
GET /v1/organizations/{organizationId}/eventsreturns event resources with nested delivery attempts. Retrieve one event withGET /v1/organizations/{organizationId}/events/{eventId}. Both requirewebhooks:read.
Each event resource looks like:
Local development
Foil refuses to deliver to private IPs in production. To develop against your laptop, use a tunnel (ngrok, cloudflared, etc.) and register the tunnel’s public URL as the endpoint. The signature flow and payloads are identical to production.