Event webhooks
Receive signed generation, publishing and form events in your own system.
Connect a receiver
When webhook_create is listed in this deployment’s API reference, an owner or admin can register up to 20 workspace endpoints through REST or MCP using workspace:manage. No browser login is needed for these operations after initial account and key setup. Use a direct public HTTPS endpoint on port 443; private addresses, redirects and embedded URL credentials are rejected.
- Deploy the receiver below behind HTTPS. Set its expected workspace ID, a dedicated receiver database and a private random signing secret of at least 32 characters.
- Call identity_get and confirm the workspace, then submit webhook_create with the destination URL, the private secret, selected events and a new persisted idempotency key. Keep the exact request privately for recovery after an uncertain response.
- Register only events you need. An enabled endpoint receives future committed events; registration does not replay historical work or send a test event.
- Read webhook_deliveries to inspect delivery. The same commands are exposed under /api/v1/integrations/webhooks and as MCP tools.
const endpoint = await client.call('webhook_create', {
body: {
url: process.env.WAVEFORM_WEBHOOK_URL!,
secret: process.env.WAVEFORM_WEBHOOK_SECRET!,
events: ['generation.completed', 'generation.failed'],
enabled: true,
},
idempotencyKey: process.env.WAVEFORM_REQUEST_KEY!,
});
// Persist endpoint.id and revision. Do not log the request or signing secret.Choose event types
- generation.completed: a generation reached ready or needs_review. Read data.state and the current quality report; completion alone never authorizes publication.
- generation.failed and generation.cancelled: the saved generation entered the corresponding terminal state. Saved artifacts and later cost reconciliation remain available.
- page.published and page.unpublished: the single-page publication pointer changed.
- site.published and site.unpublished: a versioned site publication changed. data.version identifies publication order; delayed deliveries can arrive out of order.
- form.submitted: a real form submission entered the inbox. Synthetic test submissions are excluded. Retrieve values separately with an authorized leads:read credential.
Every envelope contains version:1, id, type, createdAt, workspaceId and a data object containing identifiers, state or version. It includes no prompt, page source, signing secret or lead field values. The same event id is sent to every matching endpoint. Events are captured transactionally with the saved change; requests and delivery attempts are separate.
Treat events as notifications to read current authorized state. Delivery order is not guaranteed. Duplicate notifications are possible after a timeout or process restart. Never execute event content or turn it into unreviewed instructions.
Verify before acknowledging
Verify the original request bytes before JSON parsing or processing. Waveform-Timestamp is a Unix timestamp in seconds. Waveform-Event-Id is the stable event id; Waveform-Delivery-Id identifies this endpoint’s delivery. Waveform-Signature contains v1= followed by the hex HMAC-SHA256 of timestamp + "." + eventId + "." + the exact request body, using the endpoint’s signing secret. Each retry receives a fresh timestamp and signature.
- Limit request bodies to 128 KiB and validate the signature with a constant-time comparison. Reject timestamps more than five minutes in the past or future.
- Check the body’s id matches Waveform-Event-Id and workspaceId matches your configured workspace.
- Store the event in a durable inbox with a unique event-id constraint in the same transaction that accepts it. Reject conflicting bodies under an existing id.
- Return a 2xx acknowledgement only after that commit. A separate worker handles the inbox. Make downstream changes idempotent as well, and mark processed_at only after successful processing.
The downloadable Node receiver implements signature verification, rotation compatibility, request limits and a PostgreSQL inbox. Install postgres in its directory, then run it with private environment variables. It binds to localhost; put your HTTPS reverse proxy in front of /waveform. It intentionally does not run arbitrary actions from received payloads.
npm install postgres
node --env-file=.env waveform-webhook-receiver.mjsWAVEFORM_WORKSPACE_ID=<workspace returned by identity_get>
WAVEFORM_WEBHOOK_SECRET=<private random secret>
DATABASE_URL=<your dedicated receiver database>
PORT=8787Inspect retries and rotate credentials
webhooks_list · MCP / client input
{}webhook_get · MCP / client input
{
"params": {
"id": "11111111-1111-4111-8111-111111111111"
}
}webhook_deliveries · MCP / client input
{
"params": {
"id": "11111111-1111-4111-8111-111111111111"
},
"query": {
"limit": 50
}
}Delivery pages contain items and nextCursor; use nextCursor as the next before value. Inspect state, attempts, cycleAttempts, lastStatus and the safe error code. A destination timeout can mean it already received the event. Waveform makes up to six automatic attempts, with delays of 30 seconds, 2 minutes, 10 minutes, 30 minutes and 2 hours. After exhaustion, webhook_retry requires the exact cumulative expectedAttempts. It starts another bounded cycle with the same event and immutable body; deduplicate by event id.
webhook_update · MCP / client input
{
"params": {
"id": "11111111-1111-4111-8111-111111111111"
},
"body": {
"expectedRevision": 1,
"enabled": false,
"events": [
"generation.completed",
"generation.failed"
]
}
}Read the current endpoint revision before updating it. On a version conflict, read it again and reconcile your intended change; do not overwrite blindly. Omitting url and secret keeps them unchanged. Reads expose only the destination origin, so the private URL path and query are not recoverable through the API.
For rotation, first make your receiver accept both the current and replacement secrets, then update secret with expectedRevision. New events use the replacement. Already queued deliveries and explicit retries retain the old secret and recipient. Keep the old secret for their lifetime, or disable the old endpoint and register a new one when a hard cutover is needed. Disabling cancels queued deliveries; a request already in flight may still complete. Re-enabling does not resurrect cancelled deliveries.
Endpoint configuration belongs to the workspace. Revoking an individual credential prevents it from managing endpoints; it does not delete an enabled workspace integration. Disable the endpoint explicitly to stop future delivery. Changes to an endpoint do not change previously saved page or site content.