Node.js Examples

Node.js examples for Loom API v1 using fetch.

Node.js Examples

Official SDK available. If you'd rather not write the HTTP layer yourself, install loomapi-js — it covers retries, typed errors, and webhook verification out of the box. See SDKs.

The examples below show the raw fetch API (Node 18+). Auth: x-tenant-api-key header. Base URL: https://api.loomapi.com.

Configuration

const API_BASE = 'https://api.loomapi.com'
const API_KEY = process.env.LOOM_TENANT_API_KEY

function apiHeaders() {
  return {
    'x-tenant-api-key': API_KEY,
    'Content-Type': 'application/json',
  }
}

Start a verification

The request body is optional. Only userAgent and ip are accepted (unknown fields are rejected).

async function startVerification(opts = {}) {
  const res = await fetch(`${API_BASE}/verify/start`, {
    method: 'POST',
    headers: apiHeaders(),
    body: JSON.stringify(opts),  // e.g. { userAgent: '...', ip: '...' }
  })

  if (!res.ok) {
    const err = await res.json().catch(() => ({}))
    throw new Error(err.message || res.statusText)
  }

  return res.json()
  // Returns: { verificationId, status: 'started', provider: 'idenfy', sessionUrl, config }
}

// Usage
const { verificationId, sessionUrl } = await startVerification()
console.log('Send user to:', sessionUrl)

Get verification status

async function getVerificationStatus(verificationId) {
  const res = await fetch(
    `${API_BASE}/verify/status?verificationId=${encodeURIComponent(verificationId)}`,
    { headers: apiHeaders() }
  )

  if (!res.ok) throw new Error(await res.text())
  return res.json()
  // Returns: { verificationId, status, confidence, createdAt, completedAt }
}

// Usage
const status = await getVerificationStatus('cjld2cjxh0000qzrmn831i7rn')
// status.status is one of: pending | started | submitted | approved | denied | resubmission | rejected
if (status.status === 'approved') {
  console.log('User verified with confidence:', status.confidence)
}

Validate a token

async function validateToken(token) {
  const res = await fetch(`${API_BASE}/tokens/validate`, {
    method: 'POST',
    headers: apiHeaders(),
    body: JSON.stringify({ token }),
  })

  if (!res.ok) {
    const err = await res.json().catch(() => ({}))
    throw new Error(err.message || 'Validation failed')
  }

  return res.json()
  // Returns: { valid: boolean, over18: boolean, reason: string }
}

// Usage
const result = await validateToken(jwtFromSession)
if (result.valid && result.over18) {
  // Grant access
} else {
  console.log('Rejected:', result.reason)
  // reason: TOKEN_EXPIRED | TOKEN_REVOKED | TOKEN_NOT_FOUND | UNDERAGE_OR_UNKNOWN
}

Express: redirect to verification

const express = require('express')
const app = express()

app.get('/verify', async (req, res) => {
  try {
    const { verificationId, sessionUrl } = await startVerification({
      userAgent: req.headers['user-agent'],
      ip: req.ip,
    })
    // Store verificationId in session if needed
    res.redirect(sessionUrl)
  } catch (err) {
    res.status(500).json({ error: err.message })
  }
})

Webhook handler (signature verification)

LoomAPI sends X-Webhook-Signature (raw HMAC-SHA256 hex) and X-Webhook-Event. Always verify on the raw request body before parsing JSON.

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')
  )
}

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

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

  const payload = JSON.parse(req.body)
  // payload: { event, tenantId, data: { verificationId, status, confidence, provider }, timestamp }

  if (payload.event === 'verification.completed') {
    const { verificationId, status } = payload.data
    if (status === 'approved') {
      // Grant access — user is verified
    } else {
      // status is denied, resubmission, rejected, etc.
    }
  }

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

Error handling

async function startVerificationWithRetry(opts = {}, maxRetries = 3) {
  let lastErr
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await startVerification(opts)
    } catch (err) {
      lastErr = err
      if (err.message?.includes('429')) {
        await new Promise((r) => setTimeout(r, 5000))
        continue
      }
      throw err
    }
  }
  throw lastErr
}

More