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-signaturesha256=<hex> HMAC-SHA256 of the raw request body 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 HMAC-SHA256 envelope as every other Krafter webhook. The full canonical examples (Node.js, Elixir, Python) live on Webhook delivery → Verifying signatures.

Briefly:

javascript
import crypto from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  );
}

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.

Surveys webhooks do not currently send an owner-notification email when auto-disable fires — only Forms and Push do today (the helper exposes a webhook_type opt that hasn't been wired into Surveys yet). 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