Skip to main content
VertaaUX Docs
SDK

Webhook Methods

Register webhooks and verify signatures with the VertaaUX SDK

Webhook Methods

The SDK provides methods to manage webhooks and verify webhook signatures.

Overview

Webhooks notify your server when audits complete, eliminating the need for polling. When an audit finishes, VertaaUX sends a POST request to your registered endpoint with the results.

Webhook Management

webhooks.create()

Register a new webhook endpoint.

client.webhooks.create(params: WebhookRequest): Promise<WebhookResponse>
ParameterTypeRequiredDescription
urlstringrequiredHTTPS endpoint URL for webhook delivery
secretstringrequiredSecret for HMAC-SHA256 signature verification

Example:

import { VertaaUX } from '@vertaaux/sdk';

const client = new VertaaUX({
  apiKey: process.env.VERTAA_API_KEY!,
});

const webhook = await client.webhooks.create({
  url: 'https://api.example.com/webhooks/vertaaux',
  secret: 'wh_sec_' + crypto.randomUUID(),
});

console.log('Webhook ID:', webhook.id);
console.log('URL:', webhook.url);
console.log('Created:', webhook.created_at);

Store your webhook secret securely. You'll need it to verify incoming webhook signatures.

webhooks.list()

List all registered webhooks.

client.webhooks.list(): Promise<{ webhooks: WebhookResponse[] }>

Example:

const { webhooks } = await client.webhooks.list();

webhooks.forEach((wh) => {
  console.log(`${wh.id}: ${wh.url}`);
});

webhooks.delete()

Remove a webhook endpoint.

client.webhooks.delete(webhookId: string): Promise<void>

Example:

await client.webhooks.delete('wh_def456ghi012');
console.log('Webhook deleted');

Signature Verification

Webhooks include cryptographic signatures for security. Always verify signatures before processing webhook payloads.

verifyWebhookSignature()

Import the helper function to verify webhook signatures:

import { verifyWebhookSignature } from '@vertaaux/sdk';

Signature Format

Webhook requests include these headers:

HeaderDescription
X-Vertaa-SignatureHMAC-SHA256 signature (hex-encoded)
X-Vertaa-TimestampUnix timestamp when signature was created

The signature is computed as: HMAC-SHA256(secret, timestamp + "." + payload)

Parameters

ParameterTypeRequiredDescription
secretstringrequiredYour webhook secret (from webhook registration)
payloadstringrequiredRaw request body as string
signaturestringrequiredX-Vertaa-Signature header value
timestampstring | numberrequiredX-Vertaa-Timestamp header value
toleranceSecondsnumberoptionalMaximum age of signature in seconds. Default: 300 (5 minutes)

Express.js Handler

webhook-handler.ts
import express from 'express';
import { verifyWebhookSignature } from '@vertaaux/sdk';

const app = express();

// Important: Use raw body for signature verification
app.post(
  '/webhooks/vertaaux',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.headers['x-vertaa-signature'] as string;
    const timestamp = req.headers['x-vertaa-timestamp'] as string;
    const payload = req.body.toString('utf8');

    // Verify signature
    const isValid = await verifyWebhookSignature({
      secret: process.env.WEBHOOK_SECRET!,
      payload,
      signature,
      timestamp,
      toleranceSeconds: 300, // Reject webhooks older than 5 minutes
    });

    if (!isValid) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Parse and process the webhook
    const event = JSON.parse(payload);
    console.log('Received webhook:', event.job_id, event.status);

    if (event.status === 'completed') {
      console.log('Audit score:', event.scores?.overall);
      console.log('Issues:', event.issues?.length);
      // Process the results...
    } else if (event.status === 'failed') {
      console.error('Audit failed:', event.error);
      // Handle failure...
    }

    // Acknowledge receipt
    res.status(200).json({ received: true });
  }
);

app.listen(3000);

Next.js API Route

app/api/webhooks/vertaaux/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyWebhookSignature } from '@vertaaux/sdk';

export async function POST(request: NextRequest) {
  // Get headers
  const signature = request.headers.get('x-vertaa-signature');
  const timestamp = request.headers.get('x-vertaa-timestamp');

  if (!signature || !timestamp) {
    return NextResponse.json(
      { error: 'Missing signature headers' },
      { status: 400 }
    );
  }

  // Get raw body
  const payload = await request.text();

  // Verify signature
  const isValid = await verifyWebhookSignature({
    secret: process.env.WEBHOOK_SECRET!,
    payload,
    signature,
    timestamp,
  });

  if (!isValid) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    );
  }

  // Parse and process
  const event = JSON.parse(payload);

  switch (event.status) {
    case 'completed':
      // Store results, notify users, etc.
      console.log('Audit completed:', event.job_id);
      console.log('Score:', event.scores?.overall);
      break;

    case 'failed':
      // Log failure, retry, alert
      console.error('Audit failed:', event.job_id, event.error);
      break;
  }

  return NextResponse.json({ received: true });
}

Webhook Events

audit.completed

Sent when an audit finishes successfully.

{
  "job_id": "clu1a2b3c4d5e6f7g8h9i0j1",
  "status": "completed",
  "completed_at": "2024-01-15T10:32:30Z",
  "scores": {
    "overall": 78,
    "ux": 82,
    "accessibility": 74,
    "information_architecture": 80,
    "performance": 76
  },
  "issues": [
    {
      "id": "iss_color_contrast_001",
      "severity": "error",
      "category": "accessibility",
      "description": "Text has insufficient contrast (3.2:1 ratio)",
      "recommendation": "Increase contrast to at least 4.5:1 for body text",
      "wcag_reference": "WCAG 2.1 SC 1.4.3 (AA)",
      "selector": ".footer-text",
      "element": "<p class=\"footer-text\">Contact Us</p>"
    }
  ]
}

audit.failed

Sent when an audit encounters an error.

{
  "job_id": "clu1a2b3c4d5e6f7g8h9i0j1",
  "status": "failed",
  "completed_at": "2024-01-15T10:32:30Z",
  "error": "Page load timeout after 30 seconds"
}

Security Best Practices

1. Always Verify Signatures

Never process webhooks without verifying the signature:

// Always verify before parsing/processing
const isValid = await verifyWebhookSignature({ ... });
if (!isValid) {
  return res.status(401).send('Unauthorized');
}

2. Use Raw Body

Parse the body as raw text, not JSON, for signature verification:

// Express
app.post('/webhook', express.raw({ type: 'application/json' }), handler);

// Next.js
const payload = await request.text(); // Not request.json()

3. Check Timestamp

Use toleranceSeconds to reject old (potentially replayed) webhooks:

await verifyWebhookSignature({
  // ...
  toleranceSeconds: 300, // Reject webhooks > 5 minutes old
});

4. Respond Quickly

Return 200 quickly and process async to avoid timeouts:

app.post('/webhook', async (req, res) => {
  // Verify signature
  if (!await verify(req)) {
    return res.status(401).send();
  }

  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhookAsync(JSON.parse(req.body.toString()));
});

Webhook Limits by Tier

TierMax Webhooks
Free0
Pro5
Agency50
Enterprise100

Was this page helpful?

On this page