Guide to Envoy Webhooks: Features and Best Practices
Envoy webhooks tell your systems what is happening at the front door and around the office: a visitor signs in or out, an invite is created or cancelled, someone matches a blocklist rule, an employee arrives, a desk is assigned or a location's capacity changes. If your workplace runs on Envoy, webhooks are how those moments reach your security, facilities, HR and notification tools as they happen.
This guide covers how Envoy webhooks are configured and signed, the events and their payloads, the documentation quirks that trip up new integrations, and the practices that keep an Envoy receiver reliable.
What are Envoy webhooks?
Envoy webhooks belong to an Envoy app. In the dashboard, you choose the event types you want and the URLs to post them to, and Envoy sends each event to those URLs as a JSON body with three keys: event, meta and payload.
Each delivery carries an X-Envoy-Signature header. In Envoy's words, it is "a Base64-encoded string that represents a SHA-256 HMAC of the request body, using your app's client secret as the key". There is no separate webhook secret: the key is the client secret from your app's credentials. Envoy's official SDKs verify the signature automatically.
You can also add HTTP Basic credentials to a webhook, as username:password, and Envoy includes them in the request headers. That is a separate, optional check on top of the signature.
Envoy webhook features
| Feature | Details |
|---|---|
| Configuration | Envoy dashboard: Apps, then Configure on the Webhooks card |
| Subscription | Event types chosen per app, delivered to one or more webhook URLs |
| Payload | { event, meta, payload }, with JSON:API-style resources |
| Signature header | X-Envoy-Signature |
| Scheme | HMAC-SHA256, base64, no prefix |
| Signed content | The raw request body, nothing else |
| Secret | Your Envoy app's client secret |
| Extra authentication | Optional HTTP Basic credentials (username:password) |
| Success | Any 2xx |
| Retries | 5 times over 10 minutes |
| Replay window | None. No timestamp is sent or signed |
Envoy doesn't document a response timeout, a delivery ID header or source IPs for webhooks.
Common events
Envoy calls event types topics. It lists 20, split between visitor and workplace products.
| Area | Topics | Fires when |
|---|---|---|
| Visitor entries | entry_sign_in, entry_sign_out, entry_manually_updated | A visitor signs in or out, or an entry is edited in the visitor dashboard |
| Visitor blocklist | entry_blacklist_review, entry_blacklist_deny | A visitor matches a blocklist rule and needs admin approval, or is denied |
| Invites | invite_created, invite_updated, invite_removed, upcoming_visit, qr_code_sent | An invite is created, changed or deleted, a visit is coming up, or the visitor is emailed their QR code |
| Employee invites | employee_invite_created, employee_invite_updated, employee_upcoming_visit | An employee invite is created or updated, or an employee is due on site in an hour |
| Employee entries | employee_entry_sign_in, employee_entry_sign_out, employee_, employee_ | An employee signs in or out, or matches a blocklist rule |
| Workplace | location_, desk_sign_in, ticket_created | Capacity changes, a desk is reserved and assigned, or an issue is reported through Envoy Mobile |
desk_sign_in and ticket_created are in beta and may not be available on your account. The topic names keep the older word "blacklist" even though Envoy's product now calls the feature a blocklist.
Here is Envoy's entry_sign_in example, heavily trimmed:
{
"event": "entry_sign_in",
"meta": {
"location": {
"id": "94082",
"type": "locations",
"attributes": { "name": "HQ", "time-zone": "America/Los_Angeles", "city": "San Francisco" }
},
"company": {
"id": "75254",
"type": "companies",
"attributes": { "name": "Envoy", "active": true }
}
},
"payload": {
"id": "80078985",
"type": "entries",
"attributes": {
"full-name": "John Doe",
"email": "integrations@envoy.com",
"signed-in-at": "2020-08-31 22:09:50 UTC",
"signed-in-via": "iPad",
"flow-name": "Visitor",
"user-data": [
{ "field": "Purpose of visit", "value": "Visitor" }
],
"approval-status": { "status": "approved", "auto-approved": false }
},
"relationships": {
"location": { "data": { "type": "locations", "id": "94082" } }
}
}
}
eventis the topic. Route on it.metadescribes where it happened: thelocationand thecompany.payloadis the entry, invite or other resource, in JSON:API form withid,type,attributesandrelationships.- Attribute names use dashes (
full-name,signed-in-at), so most languages need bracket access rather than dot access. signed-in-atin this example is2020-08-31 22:09:50 UTC, not ISO 8601, whilecreated-atelsewhere is ISO 8601. Parse dates field by field.- The answers a visitor gave at sign-in arrive in
user-dataasfieldandvaluepairs, labelled with your own sign-in flow's questions.
Envoy publishes no example for entry_manually_updated, and its location_ example has an unterminated string, so it isn't valid JSON. Log the first real delivery of those two before relying on their shape.
Setting up Envoy webhooks
- Open the Apps page in the Envoy dashboard and click Configure on the Webhooks card.
- Under Events, select the event types you want.
- Enter the full URL of your endpoint in the Webhook URLs field. To add HTTP Basic authentication, include the credentials as
username:passwordas described on Envoy's setup page. - Click Complete Setup.
- Copy your Envoy app's client secret from the app's credentials and store it server-side, for example as
ENVOY_CLIENT_SECRET.
If you receive Envoy webhooks through Hookdeck, add the Hookdeck source URL as a webhook URL and paste your Envoy app's client secret into the Envoy source. Envoy signs with that secret; there is no other webhook secret to look for.
Securing Envoy webhooks
Compute an HMAC-SHA256 of the raw request body with the client secret as the key, base64-encode it, and compare it with X-Envoy-Signature in constant time. Compare the base64 strings exactly: unlike hex, base64 is case-sensitive.
const crypto = require('crypto');
const express = require('express');
function verifyEnvoySignature(rawBody, signatureHeader, clientSecret) {
if (!signatureHeader || !clientSecret) return false;
const expected = crypto.createHmac('sha256', clientSecret).update(rawBody).digest('base64');
const received = Buffer.from(signatureHeader.trim(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
app.post('/webhooks/envoy', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.ENVOY_CLIENT_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyEnvoySignature(req.body, req.get('x-envoy-signature'), secret)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Route on event.event, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
def verify_envoy_signature(raw_body: bytes, signature_header, client_secret: str) -> bool:
if not signature_header or not client_secret:
return False
digest = hmac.new(client_secret.encode("utf-8"), raw_body, hashlib.sha256).digest()
return hmac.compare_digest(signature_header.strip().encode("utf-8"), base64.b64encode(digest))
# In FastAPI: raw_body = await request.body()
# verify_envoy_signature(raw_body, request.headers.get("x-envoy-signature"),
# os.environ["ENVOY_CLIENT_SECRET"])
If you build on Envoy's Node SDK, @envoy/envoy-integrations-sdk, its EnvoySignatureVerifier does the same check, with defaults of sha256, base64, the x-envoy-signature header and your client secret.
Ignore the code block on Envoy's "Set up webhooks" page. It shows a Ruby OpenSSL::HMAC.hexdigest over a timestamp and a token, keyed with an API key. That contradicts the prose above it and the SDK, and Envoy sends no timestamp or token to sign, so code copied from it never verifies.
To check your verifier, use the client secret envoy-guide-test-client-secret and this exact body:
{"event":"entry_sign_in","payload":{"id":"80078985","type":"entries"}}
The X-Envoy-Signature value should be:
VOM4k1zDvnXGWaHQvZb8UCAIB7oRjZ7xJ8HrSprUfq8=
Make Envoy webhooks production-ready. Hookdeck Event Gateway verifies the
X-Envoy-Signatureheader on every delivery, acknowledges Envoy straight away, and holds each visitor and workplace event until your service has processed it.
Envoy webhook limitations and pain points
Ten minutes of retries
The Problem: Sign-ins that happen during a short outage never reach your systems.
Why It Happens: Envoy retries a non-2xx response 5 times over 10 minutes. A deploy, a database restart or an expired certificate that lasts longer than that loses events for good.
Workarounds:
- Return a
2xxas soon as the signature is verified and the event is stored. - After any outage, reconcile through the Envoy API, for example by listing the day's entries and invites.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, then delivers to your service with retries you configure, so your downtime no longer has to fit inside Envoy's 10 minutes.
A misleading code sample
The Problem: Verification code copied from Envoy's docs never matches.
Why It Happens: The setup page's only code sample describes a different scheme: hex output, an API key as the key, and a timestamp and token as the message. The real scheme is in the paragraph above it.
Workarounds:
- Verify a base64 HMAC-SHA256 of the raw body, keyed with the client secret, as shown in this guide.
- Or use Envoy's SDK, which implements the documented scheme.
How Hookdeck Can Help: Hookdeck's Envoy source type implements the documented scheme, so verification works once you paste the client secret.
The key is your app's client secret
The Problem: Rotating the client secret for API reasons breaks webhook verification, or a leaked webhook key exposes your API credentials.
Why It Happens: Envoy uses one credential for both: the app's client secret is the HMAC key.
Workarounds:
- Treat webhook verification as a consumer of the client secret, and update it in the same change as any rotation.
- Keep the secret in a secrets manager rather than in each service's configuration.
How Hookdeck Can Help: With Hookdeck in front, only the Envoy source holds the client secret for webhooks, so a rotation means updating one place.
No replay protection or delivery ID
The Problem: A captured request verifies again later, and retries can't be told apart from new events.
Why It Happens: Only the body is signed. Envoy sends no timestamp and documents no delivery ID header.
Workarounds:
- Build an idempotency key from
eventandpayload.id, adding a timestamp attribute such assigned-in-atwhere one exists. - Use HTTP Basic credentials as an extra check, and keep the endpoint on HTTPS.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries based on the body fields you choose.
Best practices
Verify against the raw body
The signature covers the exact bytes. Use express.raw({ type: '*/*' }) in Express, await request.text() in a Next.js route handler and await request.body() in FastAPI, and parse only after verification.
Route on event, and expect new topics
Switch on event. Envoy adds products and beta topics over time, so log and acknowledge anything you don't recognise.
Use meta for multi-location logic
One company can have many locations. Read meta.location.id and its time zone before acting, especially for sign-in notifications and capacity changes.
Handle blocklist reviews quickly
entry_blacklist_review means a visitor is waiting for an admin decision. If you route it to a person, for example through Slack or a pager, do that from a fast queue rather than a batch job.
Acknowledge fast and process asynchronously
Verify, store, return 200, and do the work afterwards. See why to process webhooks asynchronously and our guide to webhook idempotency.
Conclusion
Envoy signs every webhook with X-Envoy-Signature, a base64 HMAC-SHA256 of the raw body keyed with your app's client secret. Verify that scheme rather than the code sample on the setup page, route on event, read the dashed JSON:API attributes carefully, and acknowledge quickly, because Envoy stops retrying after 10 minutes.
Hookdeck Event Gateway sits in front of your receiver, checks every Envoy signature, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Envoy webhooks reliably in minutes.