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 Verne.
Supported Events
| Event | When it fires |
|---|---|
identity.created | A new user registered (password or social login) |
identity.deleted | A user identity was deleted via the API |
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_gate_live_sk_9f8a7...'
{
"webhooks": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://yourapp.com/webhooks/gate",
"events": ["identity.created", "identity.login"],
"active": true,
"created_at": "2026-04-11T14:00:00Z"
}
]
}
Create endpoint
curl -X POST https://api.vernesoft.com/v1/gate/settings/webhooks \
-H 'Authorization: Bearer vrn_gate_live_sk_9f8a7...' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://yourapp.com/webhooks/gate",
"events": ["identity.created", "identity.login", "identity.password_changed"]
}'
{
"id": "550e8400-...",
"url": "https://yourapp.com/webhooks/gate",
"secret": "whsec_a3f9c2...",
"events": ["identity.created", "identity.login", "identity.password_changed"],
"active": true,
"created_at": "2026-04-11T14:00:00Z"
}
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_gate_live_sk_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_gate_live_sk_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" or "oidc") |
identity.login | email |
identity.state_changed | state ("active" or "inactive") |
identity.password_changed | method ("password") |
identity.email_verified | — |
identity.deleted | — |
Verifying Signatures
Every request includes an X-Verne-Signature header:
X-Verne-Signature: t=1744380300,v1=3b5a...
t— Unix timestamp of the deliveryv1— HMAC-SHA256 hex digest
Signed string: <timestamp>.<raw_body>
Verification algorithm:
const crypto = require('crypto')
function verifyWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
)
const timestamp = parts['t']
const expectedSig = parts['v1']
if (!timestamp || !expectedSig) return false
// Reject events older than 5 minutes (replay protection)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const signedPayload = `${timestamp}.${rawBody}`
const computedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(computedSig),
Buffer.from(expectedSig)
)
}
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts.get("t")
expected = parts.get("v1")
if not timestamp or not expected:
return False
# Replay protection: reject events older than 5 minutes
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.{raw_body.decode()}"
computed = hmac.new(secret.encode(), signed_payload.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, expected)
Delivery & Retry
Gate delivers each event asynchronously with up to 3 attempts:
| Attempt | Delay after previous failure |
|---|---|
| 1 | Immediate |
| 2 | 1 second |
| 3 | 2 seconds |
An attempt is considered successful when your endpoint returns any 2xx status. After 3 failures the delivery is dropped and a warning is logged.
Recommendations:
- Respond quickly with
200 OKand process the event asynchronously. - Make your handler idempotent using
event_id— the same event may be delivered more than once in rare failure scenarios. - Use the timestamp in
X-Verne-Signaturefor replay attack protection (reject events older than 5 minutes).