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

EventWhen it fires
identity.createdA new user registered (password or social login)
identity.deletedA user identity was deleted via the API
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_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.

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_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:

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

AttemptDelay after previous failure
1Immediate
21 second
32 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 OK and 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-Signature for replay attack protection (reject events older than 5 minutes).