Skip to main content

Webhooks

Looking for the product view?
The Help Center explains this surface as users see it: API Routes & Webhooks.

Apps can expose public webhook endpoints — server-side handlers that receive requests without requiring Informer authentication. Webhooks are ideal for receiving callbacks from external services (Slack, Stripe, GitHub, etc.).

How Webhooks Work​

Webhooks use the same file-convention routing as server routes, but files go in a webhooks/ directory instead of server/:

DirectoryAuth RequiredURL Pattern
server/Yes (session/token)/api/apps/{id}/view/api/_server/...
webhooks/No (public)/api/apps/{id}/_hook/...

During deploy, both directories are scanned and bundled. Webhook routes are registered with a public: true flag.

Defining Webhooks​

Create handler files in webhooks/ using the same conventions as server routes:

// webhooks/stripe.js
export const config = { timeout: 60000 };

export async function POST({ query, fetch, request }) {
const event = request.body;

// Verify signature using rawBody
// (rawBody is available on webhook requests for HMAC verification)

await query(
'INSERT INTO webhook_events (type, payload) VALUES ($1, $2)',
[event.type, JSON.stringify(event)]
);

return { status: 200, body: { received: true } };
}
// webhooks/slack/commands.js
export const config = { timeout: 25000 };

export async function POST({ query, fetch, respond, request }) {
const { text, response_url } = request.body;

// Ack Slack immediately (3-second deadline)
await respond({ response_type: 'ephemeral', text: 'Processing...' });

// Do the real work in background
const result = await fetch('datasets/admin:sales/_search', {
method: 'POST',
body: { query: { match_all: {} }, size: 10 }
});

// Post back to Slack
await fetch('integrations/slack/request', {
method: 'POST',
body: {
url: response_url,
method: 'POST',
data: { text: `Found ${result.body.hits.total} records` }
}
});
}

File Routing​

FileRouteURL
webhooks/stripe.jsPOST /stripe/api/apps/{id}/_hook/stripe
webhooks/slack/commands.jsPOST /slack/commands/api/apps/{id}/_hook/slack/commands
webhooks/github/[action].jsPOST /github/:action/api/apps/{id}/_hook/github/push

Webhook Handler Context​

Webhook handlers receive the same single context bag as server routes — query, fetch, context (typed dependencies), respond, emit, broadcast, notify, email, crypto, markdown, extractText, log, and env, plus the base64 globals (base64Encode/base64Decode/base64UrlEncode/base64UrlDecode, btoa/atob). See Server Routes for the full reference on each callback, and The crypto Object for the crypto surface.

notify() and email() are available on webhooks — handlers run as the app owner, so message attribution is well-defined. This is at parity with server routes.

The differences are all in how the request is identified — webhooks are public, so there's no authenticated caller:

PropertyWebhook value
request.rawBodyRaw request body string — present on webhooks for HMAC signature verification
request.userThe app owner's identity (handlers run as the owner), not the caller
request.rolesAlways [] (a public caller has no app roles)
request.appSame as server routes: identity plus baseUrl, origin, and apiUrl (App Object)
request.body / headers / params / querySame as server routes

Authentication​

Webhook requests run with the app owner's credentials. This means:

  • fetch() calls use the owner's API permissions
  • query() accesses the owner's workspace schema
  • The owner credentials are cached for 5 minutes

Since webhooks are public, verify the caller's identity using signature verification. Use crypto.verifyHmac(...) — it computes the HMAC and compares in constant time, avoiding the timing leak of a plain ===/!== comparison:

export async function POST({ request, crypto, env }) {
// GitHub sends `x-hub-signature-256: sha256=<hex>`
const signature = (request.headers['x-hub-signature-256'] || '').replace(/^sha256=/, '');

const ok = await crypto.verifyHmac('sha256', env.GITHUB_WEBHOOK_SECRET, request.rawBody, signature);
if (!ok) {
return { status: 401, body: { error: 'Invalid signature' } };
}

// Process verified webhook...
}

Webhook API Endpoints​

GET /api/apps/{id}/webhooks​

List registered webhook routes and their invocation statistics.

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps)

Query Parameters: method and path (together) filter the recent and daily sections to one route.

List webhook routes and statsGET /api/apps/00000000-0000-4000-8000-000000000001/webhooks
recent holds the last 50 invocations; daily aggregates the last 30 days. method/path query params filter both.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/webhooks"
}
},
"routes": [
{
"method": "POST",
"path": "/order-created",
"config": {
"public": true
}
}
],
"recent": [
{
"id": "00000000-0000-4000-8000-000000000002",
"method": "POST",
"path": "/order-created",
"startedAt": "2026-01-15T16:20:00.000Z",
"durationMs": 42,
"statusCode": 200,
"memoryPeakBytes": "8388608",
"error": null
},
{
"id": "00000000-0000-4000-8000-000000000003",
"method": "POST",
"path": "/order-created",
"startedAt": "2026-01-15T16:20:00.000Z",
"durationMs": 42,
"statusCode": 200,
"memoryPeakBytes": "8388608",
"error": null
}
],
"daily": [
{
"day": "2026-01-15T16:20:00.000Z",
"count": 2,
"avgDurationMs": 42,
"errorCount": 0
}
]
}
Captured from the API examples test suite; ids and timestamps are normalized.

Response Sections:

FieldDescription
routesRegistered public routes: { method, path, config }
recentLast 50 invocations: id, method, path, startedAt, durationMs, statusCode, memoryPeakBytes, and the captured error (if any)
dailyPer-day aggregates for the last 30 days: count, avgDurationMs, errorCount

Webhook Invocation Endpoints​

These are the public endpoints that external services call:

  • GET /api/apps/{id}/_hook/{path*} — Public GET webhook
  • POST /api/apps/{id}/_hook/{path*} — Public POST webhook
  • PUT /api/apps/{id}/_hook/{path*} — Public PUT webhook

No Informer session is required; the ?token= JWT authenticates the call and the {path*} segment is matched against registered webhook routes.

Invoke a webhook (public)POST /api/apps/00000000-0000-4000-8000-000000000001/_hook/order-created?token=eyJhbGciOiJIUzI1NiJ9.example-token-1.signature
No Informer session: the ?token= JWT authenticates the call and the handler runs as the app owner. GET, POST, and PUT are supported.
Request body
{
"orderId": 1043
}
Response · 200
{
"received": true,
"orderId": 1043
}
Captured from the API examples test suite; ids and timestamps are normalized.

Webhook Tokens​

Webhook invocation URLs are authenticated with a ?token=<jwt> query parameter. A webhook token is a JWT signed with the app's webhook secret, so its validity is scoped to a single app. Tokens are revocable: deleting a token immediately rejects any future request that presents it with 401, without rotating the app's secret or invalidating other tokens.

These management endpoints require app:write (Designer for Magic Reports, Admin for full Apps).

POST /api/apps/{id}/webhook-tokens​

Mint a new revocable token. Optional body: { "notes": "<label>" }.

Mint a webhook tokenPOST /api/apps/00000000-0000-4000-8000-000000000001/webhook-tokens
The token is a JWT signed with the app webhook secret; embed it as ?token= on the _hook URL. Revoking the row immediately invalidates it.
Request body
{
"notes": "Order Desk CI hook"
}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tokens/00000000-0000-4000-8000-000000000002"
},
"inf:token-type": {
"href": "https://informer.example.com/api/token-types/webhook"
}
},
"resource": "Order Desk",
"permissions": {
"edit": true,
"delete": true
},
"id": "00000000-0000-4000-8000-000000000002",
"type": "webhook",
"readOnly": false,
"notes": "Order Desk CI hook",
"restrict": null,
"host": null,
"cidr": null,
"data": null,
"requests": 0,
"blocked": 0,
"lastAccessedAt": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"username": "admin",
"reportId": null,
"queryId": null,
"visualId": null,
"datasourceId": null,
"datasetId": null,
"appId": "00000000-0000-4000-8000-000000000001",
"assistantId": null,
"oauthClientId": null,
"user": {
"permissions": {
"changeDomain": true,
"changeProfile": true,
"changePassword": false,
"delete": true,
"edit": true,
"reassign": true,
"superuser": true,
"enable": true,
"changeTheme": true,
"lock": true,
"setGlobalPermissions": true,
"manageLogin": false
},
"displayName": "acme Administrator",
"email": null
},
"assistant": null,
"oauthClient": null,
"typeName": "App Webhook Token",
"key": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature",
"_embedded": {
"inf:app": {
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_print"
},
{
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_deploy"
},
"inf:app-mcp": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/mcp"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/draft/_diff"
},
"edit": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000003",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000003/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "admin:order-desk",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"unmanage": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "app",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000003",
"folderId": null,
"deletedAt": null,
"editingId": null,
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/apps/{id}/webhook-tokens​

List the app's revocable webhook tokens (rows are embedded under _embedded["inf:token"], newest first). Each entry includes the signed key, re-derived from the current webhook secret on every read.

List an App's webhook tokensGET /api/apps/00000000-0000-4000-8000-000000000001/webhook-tokens
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/webhook-tokens"
}
},
"start": 0,
"count": 1,
"total": 1,
"_embedded": {
"inf:token": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/webhook-tokens/00000000-0000-4000-8000-000000000002"
},
"inf:token-type": {
"href": "https://informer.example.com/api/token-types/webhook"
}
},
"permissions": {
"edit": true,
"delete": true
},
"id": "00000000-0000-4000-8000-000000000002",
"type": "webhook",
"readOnly": false,
"notes": "Order Desk CI hook",
"restrict": null,
"host": null,
"cidr": null,
"data": null,
"requests": 0,
"blocked": 0,
"lastAccessedAt": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"username": "admin",
"reportId": null,
"queryId": null,
"visualId": null,
"datasourceId": null,
"datasetId": null,
"appId": "00000000-0000-4000-8000-000000000001",
"assistantId": null,
"oauthClientId": null,
"typeName": "App Webhook Token",
"key": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature"
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/apps/{id}/webhook-tokens/{tokenId}​

Revoke a token by deleting it. Subsequent webhook requests presenting that token are rejected with 401. Returns 404 if the token does not exist for this app.

Revoke a webhook tokenDELETE /api/apps/00000000-0000-4000-8000-000000000001/webhook-tokens/00000000-0000-4000-8000-000000000002
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/webhook-tokens/00000000-0000-4000-8000-000000000002"
}
},
"id": "00000000-0000-4000-8000-000000000002"
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/apps/{id}/_rotate-webhook-secret​

Rotate the app's webhook secret. Every previously-distributed token signature stops verifying at once; existing token rows survive and re-derive against the new secret on the next list.

Rotate the webhook secretPOST /api/apps/00000000-0000-4000-8000-000000000001/_rotate-webhook-secret
Invalidates every previously-distributed token signature at once. Token rows survive and re-derive against the new secret on the next list.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_rotate-webhook-secret"
}
},
"id": "00000000-0000-4000-8000-000000000001"
}
Captured from the API examples test suite; ids and timestamps are normalized.

Emitting Events from Webhooks​

Webhooks can trigger agents by emitting events:

// webhooks/github.js
export async function POST({ emit, request }) {
const event = request.headers['x-github-event'];
const payload = request.body;

// Emit an event that triggers an agent
await emit('github_push', {
repo: payload.repository.full_name,
branch: payload.ref,
commits: payload.commits.length
});

return { status: 200, body: { received: true } };
}

Invocation Tracking​

Each webhook invocation is recorded with:

  • HTTP method and path
  • Response status code
  • Execution duration (ms)
  • Peak memory usage
  • Error details (if any)

Daily aggregates are available via GET /api/apps/{id}/webhooks for the last 30 days.

Project Structure​

my-app/
server/
orders/index.js ← Auth-required routes
webhooks/
stripe.js ← Public webhook (POST /stripe)
slack/
commands.js ← Public webhook (POST /slack/commands)
events.js ← Public webhook (POST /slack/events)
migrations/
001-create-tables.sql
informer.yaml
index.html
package.json