Webhooks
Gate Identity can push real-time event notifications to your server whenever something happens to one of your end-users. Use webhooks to sync your database, trigger onboarding flows, send welcome emails, or audit user activity.
Every delivery is signed with HMAC-SHA256 so you can verify it genuinely came from Nautilus, and retried on a schedule if your endpoint is down.
Supported Events
| Event | When it fires |
|---|---|
identity.created | A new user registered (password, one-time code or social login) |
identity.deleted | A user identity was deleted, via the API or by an automatic deletion rule |
identity.login | A user successfully logged in |
identity.password_changed | A user changed their password via the settings flow |
identity.email_verified | A user's email address was verified |
identity.state_changed | A user was activated or deactivated |
Managing Endpoints
Via the Dashboard
Go to Gate Identity → Webhooks and click Add Endpoint. Paste your URL, select the events you want to receive, and click Create Endpoint. Copy the signing secret — it is shown only once.
Via the API
List endpoints
curl https://api.vernesoft.com/v1/gate/settings/webhooks \
-H 'Authorization: Bearer vrn_9f8a7...'
Create endpoint
curl -X POST https://api.vernesoft.com/v1/gate/settings/webhooks \
-H 'Authorization: Bearer vrn_9f8a7...' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://yourapp.com/webhooks/gate",
"events": ["identity.created", "identity.login", "identity.password_changed"]
}'
Save the secret. It is returned only at creation time. If you lose it, delete the endpoint and create a new one.
Update endpoint (pause, change events)
curl -X PATCH https://api.vernesoft.com/v1/gate/settings/webhooks/{webhook_id} \
-H 'Authorization: Bearer vrn_9f8a7...' \
-H 'Content-Type: application/json' \
-d '{ "active": false }'
All fields are optional — only supplied fields are updated.
| Field | Type | Description |
|---|---|---|
url | string | New endpoint URL |
events | string[] | Replace the subscribed event list |
active | boolean | Pause (false) or resume (true) delivery |
Delete endpoint
curl -X DELETE https://api.vernesoft.com/v1/gate/settings/webhooks/{webhook_id} \
-H 'Authorization: Bearer vrn_9f8a7...'
Returns 204 No Content.
Payload Format
All events share the same envelope:
{
"event_id": "a1b2c3d4-...",
"event_type": "identity.created",
"tenant_id": "550e8400-...",
"created_at": "2026-04-11T14:05:00Z",
"data": {
"identity_id": "f47ac10b-...",
"payload": {
"email": "user@example.com",
"method": "password"
}
}
}
The data.payload fields vary by event:
| Event | Extra payload fields |
|---|---|
identity.created | email, method ("password", "code" or "oidc") |
identity.login | email |
identity.state_changed | state ("active" or "inactive") |
identity.password_changed | method ("password") |
identity.email_verified | — |
identity.deleted | reason, only when an automatic deletion rule deleted the account: "unverified_account_expired" or "inactive_account_expired". Absent when you deleted it through the API. See Automatic Deletion of Inactive Accounts |
Verifying Signatures
Every request carries three headers:
svix-id: msg_2b1c...
svix-timestamp: 1744380300
svix-signature: v1,3b5a...
svix-id— the message id, unique per event. Also your idempotency key.svix-timestamp— Unix timestamp of the delivery, for replay protection.svix-signature— space-delimited list ofv1,<base64 HMAC-SHA256>. More than one appears while a secret is being rotated; a delivery is genuine if any of them matches.
Signed string: <svix-id>.<svix-timestamp>.<raw_body>
The secret is the whsec_… value returned when you created the endpoint. Note that the part after whsec_ is base64 — decode it before using it as the HMAC key.
Use the official library rather than writing this yourself. It handles the multiple-signature case, the timestamp tolerance and constant-time comparison:
// npm install svix
import { Webhook } from 'svix'
const wh = new Webhook(process.env.WEBHOOK_SECRET) // whsec_...
app.post('/hooks/nautilus', express.raw({ type: 'application/json' }), (req, res) => {
let event
try {
event = wh.verify(req.body, {
'svix-id': req.headers['svix-id'],
'svix-timestamp': req.headers['svix-timestamp'],
'svix-signature': req.headers['svix-signature'],
})
} catch {
return res.status(400).send('bad signature')
}
// event is the parsed envelope — event_id, event_type, data, …
res.sendStatus(200)
})
Libraries exist for Go, Rust, PHP, Java, Ruby, C# and Kotlin as well.
Changed on 2026-09-18. Deliveries used to carry a single
X-Verne-Signature: t=…,v1=…header, signed over<timestamp>.<body>with a hex digest. Both the header names and the signed string are different now. If you wrote verification by hand against the old format, it will reject every delivery until you switch to the library above.
Delivery & Retry
Before the first attempt. When something happens to one of your users, Gate first records the event on our side, and only then does the delivery service send it to your endpoint — so the first attempt normally follows within a few seconds, not instantly. If the delivery service is briefly unavailable the event waits and goes out once it is back, for up to 72 hours; an event it still cannot take after that is dropped. The one gap that remains is the call between Gate's API gateway and our core service: if that fails for longer than about half a second, the event is not recorded.
An attempt succeeds when your endpoint returns any 2xx. If it does not, the delivery is retried on this schedule:
| Attempt | After |
|---|---|
| 1 | immediately |
| 2 | 5 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 | 10 hours |
If every attempt to an endpoint fails for five days, that endpoint is disabled. Nothing notifies you when that happens, so check Dashboard → Users & sign-in → Webhooks: a disabled endpoint is listed as Paused, and its Resume button turns it back on once your handler is healthy again.
Replaying a delivery. Go to Dashboard → Users & sign-in → Webhooks and open the Deliveries view. It lists the events Gate has sent to your endpoints; open one to see the payload exactly as it was sent and every attempt made to deliver it, with the status code and the response body your server gave. A manual replay shows up there as a new attempt marked Manual replay.
- Resend sits on the latest failed attempt of an event to an endpoint. It sends that one event to that endpoint again, right now, with the same payload,
svix-idandevent_id. A confirmation names the event and the URL first. - Replay failed sits on each endpoint in the Endpoints view. Pick a moment and Gate sends again every event to that endpoint that failed since then. There can be hundreds; they go out in the background as soon as you confirm, they cannot be recalled, events that were already delivered are not sent again, and they are not guaranteed to arrive in the order they happened.
- The limit is 14 days. The moment picker will not accept anything earlier.
A replayed event is one your handler may already have acted on, so the idempotency advice below applies to it in full. Some payloads contain your user's email address (see Payload Format); the Deliveries view shows it because it is the same data your endpoint already receives.
Replay is a dashboard feature only. The API-key surface has no such routes: nothing under /v1/gate/* can resend a delivery or replay an endpoint, so there is no call to look for. Replay only covers deliveries that failed: an event your server answered with 2xx and then lost is not sent again, and neither is anything older than 14 days. For those, and whenever you want to recover from code, read the identity from the source of truth with GET /v1/gate/identities/{id} instead of waiting for a redelivery.
Recommendations:
- Respond quickly with
200 OKand process the event asynchronously. A slow handler eats into the delivery timeout. - Make your handler idempotent using
svix-id(or theevent_idin the body) — a delivery can arrive more than once, and a retry after a timeout certainly will. - Reject deliveries whose
svix-timestampis far in the past; the official libraries already do this for you.