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

EventWhen it fires
identity.createdA new user registered (password, one-time code or social login)
identity.deletedA user identity was deleted, via the API or by an automatic deletion rule
identity.loginA user successfully logged in
identity.password_changedA user changed their password via the settings flow
identity.email_verifiedA user's email address was verified
identity.state_changedA 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.

FieldTypeDescription
urlstringNew endpoint URL
eventsstring[]Replace the subscribed event list
activebooleanPause (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:

EventExtra payload fields
identity.createdemail, method ("password", "code" or "oidc")
identity.loginemail
identity.state_changedstate ("active" or "inactive")
identity.password_changedmethod ("password")
identity.email_verified—
identity.deletedreason, 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 of v1,<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:

AttemptAfter
1immediately
25 seconds
35 minutes
430 minutes
52 hours
65 hours
710 hours
810 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-id and event_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 OK and process the event asynchronously. A slow handler eats into the delivery timeout.
  • Make your handler idempotent using svix-id (or the event_id in the body) — a delivery can arrive more than once, and a retry after a timeout certainly will.
  • Reject deliveries whose svix-timestamp is far in the past; the official libraries already do this for you.