Webhooks

Webhooks allow your external systems to react to NoLag events. Configure per-topic webhooks to pre-populate state when actors subscribe, and trigger external workflows when messages are published. Webhook payloads include the actor's access scope for automatic multi-tenant isolation.

Key Concept

Webhooks are configured per topic within your app's topic settings. Different topics can trigger different endpoints, giving you fine-grained control over integrations.

Webhook Types

Hydration Webhook

Called when an actor subscribes to a topic. Use this to pre-populate the actor's state with current data (e.g., recent messages, current game state, latest prices).

Request Format

{
  "actorId": "act_xxx",
  "roomName": "general-chat",
  "topicName": "messages",
  "scope": {
    "accessScopeId": "01939f83-8b57-...",
    "slug": "client-acme",
    "name": "Acme Corp"
  }
}

scope is null for unscoped actors, or an object with the actor's access scope details for multi-tenant routing.

Response

Return any JSON data. This will be forwarded to the subscribing actor as a hydration message.

{
  "recentMessages": [
    { "from": "alice", "text": "Hello!", "timestamp": 1234567890 },
    { "from": "bob", "text": "Hi there!", "timestamp": 1234567891 }
  ],
  "participantCount": 5
}

Trigger Webhook

Called when an actor publishes data to a topic. Use this to trigger external workflows, store messages, send notifications, or integrate with third-party services.

Request Format

{
  "roomName": "general-chat",
  "topicName": "messages",
  "actorId": "act_xxx",
  "data": {
    "text": "Hello everyone!",
    "timestamp": 1234567890
  },
  "scope": {
    "accessScopeId": "01939f83-8b57-...",
    "slug": "client-acme",
    "name": "Acme Corp"
  }
}

Response

Return any 2xx status code to acknowledge receipt. The response body is ignored.

Scope in Payloads

All webhook payloads include a scope field containing the actor's access scope information. This allows your backend to route requests to the correct tenant without additional lookups.

  • Scoped actors: scope contains accessScopeId, slug, and name
  • Unscoped actors: scope is null

Configuration

Via Dashboard

  1. Navigate to your App's "Webhooks" page
  2. Click on a topic to expand its webhook settings
  3. Configure the On Subscribe (hydration) and/or On Publish (trigger) webhook URLs and headers
  4. Save changes for that topic

Via REST API

// Webhooks are configured per topic through the NoLag Dashboard
// or via the REST API using topicConfigs:
//
// PATCH /v1/organizations/{orgId}/projects/{projId}/apps/{appId}
// {
//   "topicConfigs": {
//     "messages": {
//       "webhooks": {
//         "onSubscribe": {
//           "url": "https://api.example.com/hydrate",
//           "headers": { "Authorization": "Bearer xxx" }
//         },
//         "onPublish": {
//           "url": "https://api.example.com/trigger",
//           "headers": { "Authorization": "Bearer xxx" }
//         }
//       }
//     }
//   }
// }

Authentication

Webhooks support two authentication methods:

  • Query Parameters - Add auth tokens directly in the URL: https://api.example.com/webhook?api_key=xxx
  • Request Headers - Add custom headers like Authorization or API key headers

Security Note

Always use HTTPS for webhook URLs. Webhook headers are stored encrypted at rest.

Dead Letter Queue (DLQ)

When a webhook call fails after all retry attempts, the failed request is stored in the Dead Letter Queue. You can view and retry these failed requests from the dashboard.

Retry Behavior

  • Webhooks are retried up to 3 times with exponential backoff
  • Server errors (5xx) and network errors trigger retries
  • Client errors (4xx) do not trigger retries
  • Timeout is 30 seconds per request

DLQ Entry Contents

Each DLQ entry contains:

  • Original webhook URL
  • Request headers and body
  • Response status code (if received)
  • Error message
  • Timestamp and retry count

Viewing the DLQ

Failed webhook requests can be viewed and retried from the NoLag Dashboard. Navigate to your App settings and look for the Dead Letter Queue section.

Best Practices

  • Respond quickly - Webhook handlers should respond within a few seconds. Use async processing for heavy work.
  • Idempotency - Design your handlers to be idempotent since retries may cause duplicate calls.
  • Use scope for tenant isolation - Use the scope field in webhook payloads to route data to the correct tenant.
  • Logging - Log incoming webhook requests for debugging and auditing.
  • Monitoring - Monitor your DLQ and set up alerts for failed webhooks.

Example: Chat Application

Here's a complete webhook handler example for a chat application:

import express from 'express'

const app = express()
app.use(express.json())

// Hydration: Return recent messages when user joins
app.post('/nolag/hydration', async (req, res) => {
  const { actorId, roomName, topicName, scope } = req.body

  // scope is null for unscoped actors, or:
  // { accessScopeId: "...", slug: "client-acme", name: "Acme Corp" }
  const tenantFilter = scope ? { tenantId: scope.accessScopeId } : {}

  const messages = await db.messages.findMany({
    where: { room: roomName, ...tenantFilter },
    orderBy: { createdAt: 'desc' },
    take: 50
  })

  res.json({ messages: messages.reverse() })
})

// Trigger: Store message and send notifications
app.post('/nolag/trigger', async (req, res) => {
  const { roomName, topicName, actorId, data, scope } = req.body

  await db.messages.create({
    data: {
      room: roomName,
      actorId,
      content: data.text,
      tenantId: scope?.accessScopeId
    }
  })

  await pushService.notifyRoom(roomName, data)
  res.status(200).send('OK')
})