BETA
Skip to content

Webhooks ​

Receive real-time HTTP notifications when responses are started, completed, or abandoned, when a survey is closed, or when an AI summary is generated.

Base URL: https://app.krafter.dev/api/v1

All webhook endpoints are nested under a survey: /surveys/:survey_id/webhooks. Signing, retry policy, headers, and auto-disable rules are shared across the platform — see Webhook delivery for the canonical contract.

Events ​

EventWhen it firesdata payload
response.startedA respondent calls POST /api/v1/surveys/:slug/responses (or opens the embed widget for the first time){ "response_id": "..." }
response.completedResponse is finalised via POST /complete — also bumps the survey's response_count{ "response_id": "..." }
response.abandonedA mark_abandoned cron run flips a stale in_progress response to abandoned{ "response_id": "..." }
survey.closedSurvey transitions to closed (via API or dashboard){ "survey_id": "..." }
summary.generatedAn AI summary finishes generating — see Analytics{ "summary_id": "..." }

A webhook with events: [] receives nothing. Pass the events you want explicitly. To receive every event, list all five.

Use the show endpoint to fetch full data

Webhook payloads carry ids only, not full objects. Your handler should call GET /api/v1/surveys/:survey_id/responses/:response_id (or /summary) to fetch the body it needs. This keeps webhook bodies small and immune to unrelated schema changes.

The Webhook object ​

FieldTypeDescription
idstringUUID
survey_idstringOwning survey
urlstringDestination URL
eventsstring[]Subscribed event names
activebooleanWhether the webhook is currently delivering. Auto-disabled after 10 consecutive failures
secretstringSigning secret. Only returned on creation (or on PATCH if you supply a new one). Never returned on list/show
failure_countintegerConsecutive failures since the last successful delivery. Reset to 0 on any 2xx
last_triggered_atstring | nullISO 8601 timestamp of the last delivery attempt (success or failure)
created_atstringISO 8601 timestamp

List Webhooks ​

GET /surveys/:survey_id/webhooks

Required scope: surveys:read

bash
curl https://app.krafter.dev/api/v1/surveys/SURVEY_ID/webhooks \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": [
    {
      "id": "f1a2b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
      "survey_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
      "url": "https://api.yourapp.com/webhooks/surveys",
      "events": ["response.completed", "response.abandoned"],
      "active": true,
      "failure_count": 0,
      "last_triggered_at": "2026-05-09T10:30:00Z",
      "created_at": "2026-05-01T08:00:00Z"
    }
  ]
}

The secret is never included in list / show responses.


Create Webhook ​

POST /surveys/:survey_id/webhooks

Required scope: surveys:write

Request Body ​

FieldTypeRequiredDescription
urlstringYesDestination URL. Must be http:// or https:// (HTTP allowed today, HTTPS strongly recommended)
eventsstring[]NoSubset of the five event names. Defaults to [] (no deliveries)
activebooleanNoDefaults to true

A 64-character hex signing secret is generated automatically and returned in the response. Store it — it is the only time it is shown.

bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/webhooks \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourapp.com/webhooks/surveys",
    "events": ["response.completed", "response.abandoned"]
  }'
json
// 201 Created
{
  "data": {
    "id": "f1a2b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
    "survey_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
    "url": "https://api.yourapp.com/webhooks/surveys",
    "events": ["response.completed", "response.abandoned"],
    "active": true,
    "secret": "a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
    "failure_count": 0,
    "last_triggered_at": null,
    "created_at": "2026-05-10T10:00:00Z"
  }
}

WARNING

The secret is only returned at creation time. Store it securely — you will need it to verify webhook signatures, and there is no rotation endpoint. If lost, delete and recreate the webhook.


Update Webhook ​

PATCH /surveys/:survey_id/webhooks/:webhook_id

Required scope: surveys:write

Accepts url, events, active. Send only fields you want to change. Re-enabling a webhook (active: true) does not reset failure_count — only a successful delivery does.

bash
curl -X PATCH https://app.krafter.dev/api/v1/surveys/SURVEY_ID/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{"events": ["response.completed", "summary.generated"]}'

Delete Webhook ​

DELETE /surveys/:survey_id/webhooks/:webhook_id

Required scope: surveys:write

204 No Content

Test Webhook ​

POST /surveys/:survey_id/webhooks/:webhook_id/test

Required scope: surveys:write

Enqueues a test delivery using the same delivery worker as production events. Useful to confirm your endpoint signature verification works.

bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/webhooks/WEBHOOK_ID/test \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{ "ok": true, "message": "Test webhook dispatched" }

The test request body is:

json
{
  "event": "test",
  "survey_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
  "data": {
    "test": true,
    "triggered_at": "2026-05-10T10:00:00Z"
  }
}

The x-krafter-event header is test and the request is signed exactly like a production delivery.


Payload format ​

Every delivery POST has the same envelope:

json
{
  "event": "response.completed",
  "survey_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
  "data": {
    "response_id": "f1a2b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c"
  }
}
FieldTypeDescription
eventstringOne of the event names listed above (or "test")
survey_idstringUUID of the survey
dataobjectPer-event payload — see the Events table

Headers ​

HeaderDescription
content-typeapplication/json
x-krafter-eventThe event name (e.g. response.completed)
x-krafter-webhook-idUUID v4 — regenerated on every attempt, so retries are individually identifiable
x-krafter-signaturet=<unix>,v1=<hex> — HMAC-SHA256 of <t>.<body>; see Verifying signatures, keyed by your webhook secret
x-krafter-testPresent only when triggered via Test Webhook

See Webhook delivery → Headers.


Signature verification ​

Surveys webhooks use the same signing envelope as every other Krafter delivery: x-krafter-signature: t=<unix>,v1=<hex>, where the HMAC-SHA256 covers <t>.<raw body> keyed by your webhook secret. Because t is inside the signed payload, a captured request stops verifying once it goes stale.

Split the header, reject anything whose t is more than 300 seconds old, then compare HMAC-SHA256(secret, "<t>." + rawBody) to v1 in constant time. Node.js, Elixir and Python implementations are on Verifying signatures.

Always use the raw bytes of the request body — re-serialising parsed JSON will not match.


Retries and auto-disable ​

Failed deliveries (any non-2xx, network error, or timeout) are re-enqueued by Oban with a linear backoff of attempt * 15 seconds — 15s, 30s, 45s, 60s, 75s — for up to 5 attempts. After 10 consecutive failures across deliveries, the helper flips active to false and stops attempting new deliveries until you re-enable the webhook.

When auto-disable fires, the webhook's owners are emailed, the same as for every other service that passes webhook_type to the shared delivery helper. You can also monitor failure_count via List Webhooks or the dashboard.

See Webhook delivery → Retry policy and Auto-disable.


Idempotency ​

Use data.response_id (or data.summary_id / data.survey_id) as your dedup key. The same logical event delivered after a retry carries a new x-krafter-webhook-id — that header identifies a delivery attempt, not the underlying event.

For response.completed, a complete API call against an already-completed response is a no-op and does not re-fire the webhook.


Best practices ​

  • Always verify the signature before processing events
  • Respond 2xx within a few seconds — process work asynchronously if you need more time. Slow responses count as failures once the upstream HTTP timeout hits
  • Subscribe narrowly — empty events: [] saves your endpoint from chatter you don't want
  • Plan for response.completed to fire before summary.generated — summaries run on a background job after enough responses have come in. Don't assume both arrive together
  • Use HTTPS — the schema accepts http:// today but production webhooks should always use HTTPS for confidentiality and integrity

Built by Krafter Studio