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:
scopecontainsaccessScopeId,slug, andname - Unscoped actors:
scopeisnull
Configuration
Via Dashboard
- Navigate to your App's "Webhooks" page
- Click on a topic to expand its webhook settings
- Configure the On Subscribe (hydration) and/or On Publish (trigger) webhook URLs and headers
- 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
Authorizationor API key headers
Security Note
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
scopefield 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')
})