Skip to main content
Configure a webhook URL + signing secret on your API key and Knouds will POST the result to you when an async execution completes — no polling needed. This is the event-driven alternative to async polling.

Architecture

  1. You configure a webhook URL + signing secret on a specific API key (browser, session-only endpoint at /api/api-keys/:id/webhook).
  2. Your key carries the webhook:receive capability (granted at key creation).
  3. You submit an async run with ?async=true. Response includes webhookDelivery.configured: true.
  4. When the execution finishes, a background worker on Knouds POSTs the result payload to your URL with an HMAC signature header.
  5. 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.
In the dashboard, click your key → Webhook → enter:
  • 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.
Save. The next async run on that key will deliver to your URL.

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:
Possible event values:
  • execution.completed — success; result populated
  • execution.failed — provider failure; error: { code, message } populated
  • execution.cancelled — user cancelled; result may also be populated if provider finished before cancel landed
  • webhook.test — synthetic event from clicking “Test” in the dashboard; doesn’t write a usage_log row

HMAC verification (REQUIRED)

Every delivery includes an X-Knouds-Signature header in Stripe-style format:
Where v1 = HMAC-SHA256(signingSecret, "<unix>.<rawBody>") and t is the unix timestamp (seconds). Your receiver MUST:
  1. Parse t and v1 from the header
  2. Reject if Math.abs(now - t) > 300 (5-minute replay window)
  3. Recompute expected = HMAC-SHA256(secret, "${t}.${rawBody}") and compare with v1 using constant-time comparison
  4. 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: A delivery succeeds on any 2xx response. Any other status (3xx, 4xx, 5xx, network error) counts as a failure and triggers the next retry. After the 6th failure, 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.
Idempotency is on you. Knouds may deliver the same executionId up to 6 times. Your receiver MUST dedupe — typically by storing seen executionIds in Redis with a 24h TTL, or as a unique constraint in your jobs table.

Test the configured webhook

In the dashboard, click Test on a configured webhook. This synthesizes an event: 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 unless NODE_ENV !== 'production' AND the URL is non-loopback (i.e. dev only).
  • signingSecret is a privacy boundary — never returned in any list endpoint; only the per-key GET endpoint returns it (browser session, owner-only).
  • webhookSecret excluded from usage_log SELECTs — even admin queries don’t surface it accidentally.