Webhooks

Real-time

Receive real-time notifications when events occur in your application. Webhooks push data to your server instantly, eliminating the need for polling.

Real-time Delivery

Events delivered instantly as they happen

Secure Signatures

HMAC-SHA256 signatures for verification

Automatic Retries

Failed deliveries retry with exponential backoff

Delivery Logs

Full visibility into delivery status

What are Webhooks?

Webhooks are HTTP callbacks that notify your server when specific events occur. Instead of continuously polling our API, you register a URL and we send you data in real-time.

Event Occurs

User signs up, payment succeeds, etc.

Your Server

Receives POST with event data

When to use Webhooks

Use webhooks when you need to react to events immediately, sync data to external systems, or trigger workflows based on user actions.

Using the Hook

The useWebhooks hook provides a simple interface for managing webhooks in your React application.

Client-side (React)
'use client'

import { useWebhooks } from '@sylphx/platform-sdk/react'

export function WebhookSettings() {
  const {
    webhooks,
    isLoading,
    create,
    update,
    remove,
    test,
  } = useWebhooks()

  const handleCreate = async () => {
    await create({
      url: 'https://api.example.com/webhooks',
      events: ['user.created', 'user.updated'],
      secret: 'whsec_...',
    })
  }

  const handleTest = async (webhookId: string) => {
    const result = await test(webhookId)
    if (result.success) {
      console.log('Webhook delivered successfully')
    }
  }

  if (isLoading) return <div>Loading...</div>

  return (
    <div className="space-y-4">
      <h2>Your Webhooks</h2>
      {webhooks.map((webhook) => (
        <div key={webhook.id} className="p-4 border rounded-lg">
          <p className="font-mono text-sm">{webhook.url}</p>
          <p className="text-sm text-muted-foreground">
            Events: {webhook.events.join(', ')}
          </p>
          <div className="mt-2 flex gap-2">
            <button onClick={() => handleTest(webhook.id)}>
              Test
            </button>
            <button onClick={() => remove(webhook.id)}>
              Delete
            </button>
          </div>
        </div>
      ))}
      <button onClick={handleCreate}>
        Add Webhook
      </button>
    </div>
  )
}

Available Events

Subscribe to specific events or use wildcards to receive all events of a type.

EventDescription
user.createdA new user signed up
user.updatedUser profile was updated
user.deletedUser account was deleted
subscription.createdNew subscription started
subscription.updatedSubscription plan changed
subscription.cancelledSubscription was cancelled
payment.succeededPayment was successful
payment.failedPayment attempt failed
invoice.createdNew invoice generated
invoice.paidInvoice was paid
*All events (wildcard)
Subscribe to multiple events
// Subscribe to specific events
await platform.webhooks.create({
  url: 'https://api.example.com/webhooks',
  events: [
    'user.created',
    'user.updated',
    'payment.succeeded',
    'payment.failed',
  ],
})

// Subscribe to all user events
await platform.webhooks.create({
  url: 'https://api.example.com/webhooks',
  events: ['user.*'],
})

// Subscribe to all events
await platform.webhooks.create({
  url: 'https://api.example.com/webhooks',
  events: ['*'],
})

Payload Structure

Every webhook delivery includes a consistent payload structure with event metadata and the actual data.

Example Payload
{
  "id": "evt_1234567890",
  "type": "user.created",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "version": "2024-01-01",
  "data": {
    "id": "usr_abc123",
    "email": "user@example.com",
    "name": "John Doe",
    "createdAt": "2024-01-15T10:30:00.000Z"
  },
  "metadata": {
    "webhookId": "whk_xyz789",
    "attempt": 1,
    "environment": "production"
  }
}
PropertyTypeDescription
idrequiredstringUnique event identifier
typerequiredstringEvent type (e.g., user.created)
timestamprequiredstringISO 8601 timestamp of when the event occurred
versionrequiredstringAPI version for the payload format
datarequiredobjectEvent-specific data payload
metadataobjectDelivery metadata including webhook ID and attempt number

Security

Every webhook request includes a signature header that you should verify to ensure the request is authentic.

HMAC-SHA256
Timestamp Validation

Always Verify Signatures

Never process webhook payloads without verifying the signature. This protects against replay attacks and forged requests.

Signature Verification

The signature is sent in the X-Webhook-Signature header. Verify it using your webhook secret.

verify-webhook.ts
import crypto from 'crypto'

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string,
  tolerance = 300 // 5 minutes
): boolean {
  const [timestamp, hash] = signature.split(',')
  const ts = parseInt(timestamp.replace('t=', ''), 10)
  const sig = hash.replace('v1=', '')

  // Check timestamp is within tolerance
  const now = Math.floor(Date.now() / 1000)
  if (Math.abs(now - ts) > tolerance) {
    throw new Error('Webhook timestamp outside tolerance')
  }

  // Compute expected signature
  const signedPayload = `${ts}.${payload}`
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex')

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(sig),
    Buffer.from(expected)
  )
}

// Usage in API route
export async function POST(req: Request) {
  const payload = await req.text()
  const signature = req.headers.get('X-Webhook-Signature')!
  const secret = process.env.WEBHOOK_SECRET!

  if (!verifyWebhookSignature(payload, signature, secret)) {
    return new Response('Invalid signature', { status: 401 })
  }

  const event = JSON.parse(payload)
  // Process the event...

  return new Response('OK', { status: 200 })
}

Request Headers

PropertyTypeDescription
X-Webhook-SignaturerequiredstringHMAC-SHA256 signature with timestamp
X-Webhook-IDstringUnique delivery ID for idempotency
X-Webhook-TimestampstringUnix timestamp of the request
Content-Typestring= application/jsonAlways application/json
User-AgentstringSylphx-Webhook/1.0

Retry Policy

If your endpoint returns a non-2xx status code or times out, we automatically retry with exponential backoff.

1

Attempt

Immediate

2

Attempt

1 minute

3

Attempt

5 minutes

4

Attempt

30 minutes

5

Attempt

2 hours

Response Requirements

Return a 2xx status code within 30 seconds to acknowledge receipt. The response body is ignored.

Handling Failures

// Respond quickly, process async
export async function POST(req: Request) {
  const event = await verifyAndParseWebhook(req)

  // Queue for background processing
  await queue.add('process-webhook', {
    eventId: event.id,
    type: event.type,
    data: event.data,
  })

  // Return immediately
  return new Response('OK', { status: 200 })
}

// Process in background job
export async function processWebhook(job) {
  const { eventId, type, data } = job.data

  // Check for duplicate delivery
  const processed = await db.processedEvents.findUnique({
    where: { eventId },
  })

  if (processed) {
    console.log('Duplicate event, skipping:', eventId)
    return
  }

  // Process the event
  await handleEvent(type, data)

  // Mark as processed
  await db.processedEvents.create({
    data: { eventId, processedAt: new Date() },
  })
}

Server-side API

Manage webhooks programmatically from your server.

Server-side
import { platform } from '@/lib/platform'

// Create a webhook
const webhook = await platform.webhooks.create({
  url: 'https://api.example.com/webhooks',
  events: ['user.created', 'payment.succeeded'],
  description: 'Production webhook for user sync',
  metadata: {
    team: 'backend',
  },
})

// List all webhooks
const webhooks = await platform.webhooks.list()

// Update a webhook
await platform.webhooks.update(webhook.id, {
  events: ['user.*', 'payment.*'],
  enabled: true,
})

// Delete a webhook
await platform.webhooks.delete(webhook.id)

// Test a webhook
const testResult = await platform.webhooks.test(webhook.id)
console.log('Test delivery:', testResult.success ? 'Success' : 'Failed')

// Get delivery logs
const logs = await platform.webhooks.getDeliveries(webhook.id, {
  limit: 50,
  status: 'failed',
})

Best Practices

Verify Signatures

Always verify the webhook signature before processing. Never trust unverified payloads.

Respond Quickly

Return a 2xx response immediately, then process the event asynchronously in a background job.

Handle Duplicates

Use the event ID for idempotency. The same event may be delivered multiple times.

Monitor Deliveries

Regularly check delivery logs and set up alerts for failed webhooks.