Webhooks let your services react to changes in an Envless workspace the moment they happen (a new project, a rotated key, a removed member, a failed delivery) without polling the API.
How it works
- You configure up to 20 webhook endpoints per workspace (name +
httpsURL + selected event types). Envless generates the signing secret itself (anev_whsec_value) and returns it in the create response. It is stored encrypted, so the dashboard can reveal it again after a step-up verification; the public API only ever returns it at creation or after a rotation. In the dashboard every endpoint change is guarded that way, creating and editing included, while an API key with thewebhooksscope faces no such check. - When a matching mutation succeeds, Envless writes a
WorkspaceEventrow, then creates oneWebhookDeliveryper enabled endpoint that subscribes to that event type. Emission runs after the mutation, not inside its transaction, and swallows its own errors so a webhook problem can never fail the mutation. - If the event belongs to a private product, only endpoints whose creator can still access that product receive a delivery. Everyone else gets nothing, and the
WorkspaceEventrow is still written. - A background worker ticks every 5 seconds, claims up to 25 pending deliveries (at most 5 per workspace on the first pass, so one noisy workspace cannot starve the queue), signs each request with the endpoint's secret, and POSTs the payload to your URL.
- Failed deliveries retry on a fixed backoff schedule. After the final attempt, the delivery is marked failed and (if subscribed) a
webhook.delivery_failedmeta-event is fanned out. Awebhook.delivery_faileddelivery that itself fails does not emit another one.
Delivery guarantees
- At-least-once. A receiver may see the same event more than once; verify the
webhook-idheader and dedupe. - Order is not guaranteed. Deliveries are processed in parallel and may arrive out of order.
- Per-endpoint isolation. Each endpoint signs with its own secret; a compromised receiver can't impersonate another.
Retry policy
| Attempt that failed | Delay before the next attempt |
|---|---|
| 1st | 30 s |
| 2nd | 2 min |
| 3rd | 10 min |
| 4th | 30 min |
A delivery counts as successful only when the receiver answers with a 2xx status within 10 seconds. Any other status, a redirect (redirects are not followed), a timeout, or a host that no longer resolves to a public address counts as a failure. Response bodies are recorded but truncated at 4,000 characters.
There are 4 retries, so 5 attempts in total. After the 5th unsuccessful attempt the delivery is marked failed and no further attempts are made. The endpoint's consecutiveFailures counter is incremented on each failure and reset to 0 on the next successful delivery.
Request shape
POST https://your-receiver.example.com/webhooks
Content-Type: application/json
User-Agent: Envless-Webhooks/1.0
Envless-Event: project.created
Envless-Delivery: 019e7d98-71a4-7c19-9f0e-2b3c5d6e7f80
webhook-id: <event uuid>
webhook-timestamp: <unix seconds>
webhook-signature: v1,<base64 hmac>webhook-id is the event id and is stable across retries, so it is the value to dedupe on. Envless-Delivery is the id of this delivery row, which is different per endpoint.
{
"id": "019e7d98-6b82-7305-ac80-478fa6557995",
"type": "project.created",
"apiVersion": "2026-05-31",
"createdAt": "2026-05-31T10:33:20.123Z",
"workspaceId": "...",
"actor": {
"userId": "...",
"name": "Jane Doe",
"email": "jane@example.com",
"key": null
},
"data": {
"project": {
"id": "...",
"name": "billing-service",
"slug": "billing-service",
"icon": null,
"createdAt": "2026-05-31T10:33:20.123Z"
},
"product": {
"id": "...",
"slug": "payments",
"name": "Payments"
}
}
}actor is always an object. For system-driven events (for example subscription.*, driven by Polar, and webhook.delivery_failed, emitted by the worker) its userId, name and email are each null.
actor.key is null when a person made the change from the dashboard or the CLI. When the change came from the API, it names the key that made it, and the userId/name/email fields identify that key's owner rather than the actor:
"actor": {
"userId": "...",
"name": "Jane Doe",
"email": "jane@example.com",
"key": {
"id": "...",
"name": "Envless - Production",
"prefix": "ev_sk_a3f"
}
}The key name and prefix are recorded at the time of the event, so renaming, rotating or revoking a key later does not rewrite the events it already produced. Filter on actor.key !== null to isolate every machine-originated change.
Security
- Always verify the signature before processing. See Signature Verification.
- Treat the secret like a password: store in your secret manager, never commit to git, rotate if leaked.
- Endpoint URLs must use
https. Plainhttpis rejected, URLs carrying credentials are rejected, and any host that resolves to a private or reserved address (includinglocalhost) is refused both when you save the endpoint and again on every delivery attempt. A local receiver therefore needs a public tunnel.