Outbound Webhooks

Receive real-time verification results via HTTP POST to your endpoint.

Outbound Webhooks

LoomAPI sends an HTTP POST to your configured webhook URL when a verification session completes. This eliminates polling and lets your application react immediately to results.

Setup

  1. Go to dashboard.loomapi.com
  2. Navigate to Settings
  3. Enter your webhook URL and save
  4. Copy the webhook secret shown — you need it to verify signatures

Webhook URL requirements

  • HTTPS only — HTTP endpoints are not accepted
  • Publicly reachable — must be accessible from the internet
  • Respond within 5 seconds — return a 2xx status quickly; do heavy work asynchronously
  • Idempotent — the same event may be delivered more than once; deduplicate by data.verificationId

Webhook payload

{
  "event": "verification.completed",
  "tenantId": "your_tenant_id",
  "data": {
    "verificationId": "cjld2cjxh0000qzrmn831i7rn",
    "status": "approved",
    "confidence": 0.97,
    "provider": "idenfy"
  },
  "timestamp": "2024-01-15T14:30:00.000Z"
}

Payload fields

FieldTypeDescription
eventstringAlways verification.completed in v1
tenantIdstringYour tenant ID — useful for routing in multi-tenant setups
data.verificationIdstringMatches the verificationId from POST /verify/start
data.statusstringOutcome: approved, denied, resubmission, submitted, rejected
data.confidencenumberConfidence score from iDenfy (0–1)
data.providerstringAlways idenfy in v1
timestampstringISO 8601 timestamp

Event types

Only verification.completed is implemented in v1. The data.status field carries the outcome — check it to distinguish approved from denied etc. Additional event types will be added in future releases.


Signature verification

Every webhook request includes two security headers:

X-Webhook-Signature: <hex-encoded HMAC-SHA256>
X-Webhook-Event: verification.completed

The signature is computed as HMAC-SHA256(raw_request_body, webhook_secret) and hex-encoded. Always verify it on the raw request bytes before parsing JSON.

JavaScript / Node.js (Express)

const crypto = require('crypto')

function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex')

  if (signature.length !== expected.length) return false
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  )
}

// Express handler — use express.raw() so you get the raw body
app.post('/webhooks/loom', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature']  // NOT x-loom-signature
  if (!verifyWebhookSignature(req.body, signature, process.env.LOOM_WEBHOOK_SECRET)) {
    return res.status(401).send('Unauthorized')
  }
  const event = JSON.parse(req.body)
  // process event...
  res.status(200).send('OK')
})

Python

import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        raw_body,          # raw bytes, before JSON parsing
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Security checklist

  • Always verify the signature before processing the payload
  • Use the raw request body — not the parsed JSON object — when computing the HMAC
  • Use constant-time comparison (timingSafeEqual / hmac.compare_digest) to prevent timing attacks
  • Store the secret in an environment variable, never in source code
  • Use HTTPS for your webhook endpoint

Handling webhooks

Basic handler

app.post('/webhooks/loom', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature']

  if (!verifyWebhookSignature(req.body, signature, process.env.LOOM_WEBHOOK_SECRET)) {
    return res.status(401).send('Unauthorized')
  }

  const payload = JSON.parse(req.body)
  const { event, data } = payload

  if (event === 'verification.completed') {
    if (data.status === 'approved') {
      // User is verified — grant access, issue session token, etc.
    } else {
      // data.status is 'denied', 'resubmission', etc. — handle accordingly
    }
  }

  res.status(200).send('OK')
})

Idempotency

Deduplicate by data.verificationId — the same event may be delivered more than once:

const processedIds = new Set()

function processWebhook(verificationId, status) {
  if (processedIds.has(verificationId)) return
  processedIds.add(verificationId)
  // ... handle event
}

In production, use a database column or Redis key instead of an in-memory set.


Retry policy

LoomAPI attempts delivery immediately. If that fails (non-2xx response, timeout, or connection error), it retries up to 2 more times3 total attempts. Delivery state is tracked in Redis for up to 24 hours.

Webhook delivery fails if:

  • HTTP status code >= 300
  • Connection timeout (> 5 seconds)
  • DNS resolution failure
  • SSL/TLS error

Manual retry is not available in v1. To re-trigger delivery, re-run the verification flow or contact support.


Testing webhooks

Use ngrok or localtunnel to expose a local endpoint during development:

ngrok http 3000
# Forwarding: https://abc123.ngrok.io -> http://localhost:3000

Set the ngrok URL as your webhook URL in the dashboard, then start a real verification via POST /verify/start and complete the iDenfy session. Your endpoint will receive the verification.completed event when iDenfy processes the result.

There is no test webhook API in v1 — trigger events via real or sandbox verifications.


Monitoring

Webhook configuration (URL and secret) is managed in the dashboard under Settings. Delivery logs are not yet available in the dashboard — check your own endpoint logs for delivery confirmation and debugging.


Troubleshooting

SymptomLikely causeFix
Signature verification failsWrong secret, or parsing JSON before hashingUse raw request body; confirm secret matches dashboard
401 from your endpointSignature check failingSee above
TimeoutsEndpoint takes > 5 sReturn 200 immediately; do work in background
Duplicate eventsNormal — retries happenImplement idempotency on data.verificationId

Support