Architecture
- You configure a webhook URL + signing secret on a specific API key (browser, session-only endpoint at
/api/api-keys/:id/webhook). - Your key carries the
webhook:receivecapability (granted at key creation). - You submit an async run with
?async=true. Response includeswebhookDelivery.configured: true. - When the execution finishes, a background worker on Knouds POSTs the result payload to your URL with an HMAC signature header.
- Your receiver verifies the signature, deduplicates on
executionId, and processes the result.
Configuring a webhook on your key
The per-key webhook config endpoints (
/api/api-keys/:id/webhook) are session-only by design — defense-in-depth on key management. You configure webhooks via the Developer Dashboard, not via API call.- URL — your receiver endpoint. Must be HTTPS in production. Knouds rejects private/internal IPs (SSRF defense).
- Signing secret — 16–256 chars. If omitted on first save, the server generates
whsec_<base64url>for you.
Granting webhook:receive to a key
When you create the key, pick the Webhook receiver preset (capability webhook:receive) — or in Custom mode, tick the webhook:receive chip.
A key without webhook:receive cannot have a webhook configured. The dashboard’s Webhook tab is hidden on those keys.
Delivery payload
When an execution finishes, Knouds POSTs to your URL:event values:
execution.completed— success;resultpopulatedexecution.failed— provider failure;error: { code, message }populatedexecution.cancelled— user cancelled;resultmay also be populated if provider finished before cancel landedwebhook.test— synthetic event from clicking “Test” in the dashboard; doesn’t write ausage_logrow
HMAC verification (REQUIRED)
Every delivery includes anX-Knouds-Signature header in Stripe-style format:
v1 = HMAC-SHA256(signingSecret, "<unix>.<rawBody>") and t is the unix timestamp (seconds). Your receiver MUST:
- Parse
tandv1from the header - Reject if
Math.abs(now - t) > 300(5-minute replay window) - Recompute
expected = HMAC-SHA256(secret, "${t}.${rawBody}")and compare withv1using constant-time comparison - Reject if mismatch — never trust an unverified payload
Node.js / Express receiver
Python / Flask receiver
Retry schedule
Knouds delivers up to 6 attempts total (1 initial + 5 retries) on this exponential backoff:| Attempt | Delay from first attempt |
|---|---|
| 1 (initial) | 0 |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 (final) | 12 hours |
webhookNextAttemptAt is set to NULL — no more retries. The execution is marked delivery exhausted in your dashboard. Your customer must manually re-fire by re-submitting the request, OR (Phase 13.5+ candidate) hitting a future replay endpoint.
Test the configured webhook
In the dashboard, click Test on a configured webhook. This synthesizes anevent: webhook.test payload (no usage_log row written) and runs the full delivery pipeline (HMAC sign + SSRF gate + 10s abort timeout). Response shows whether your receiver returned 2xx.
Use this to verify your signature verification works before pointing real production traffic at the URL.
Security defenses
- SSRF gate — Knouds refuses to deliver to private IPs / loopback / link-local addresses. The check runs at delivery time, so DNS rebinding (public at config, private at delivery) fails closed.
- HTTPS required in production —
http://URLs are rejected unlessNODE_ENV !== 'production'AND the URL is non-loopback (i.e. dev only). signingSecretis a privacy boundary — never returned in any list endpoint; only the per-key GET endpoint returns it (browser session, owner-only).webhookSecretexcluded fromusage_logSELECTs — even admin queries don’t surface it accidentally.