{"site":{"name":"Koji","description":"AI-native customer research platform that helps teams conduct, analyze, and synthesize customer interviews at scale.","url":"https://www.koji.so","contentTypes":["blog","documentation"],"lastUpdated":"2026-11-14T17:55:32.410Z"},"content":[{"type":"documentation","id":"9b42f662-976d-40ec-b4b4-496f40cb2a29","slug":"webhook-setup","title":"Webhook Setup","url":"https://www.koji.so/docs/webhook-setup","summary":"Koji sends signed outbound webhooks. In any study open Integrate, then Webhooks, add a public https endpoint, choose one study or all studies, store the whsec_ signing secret and send a test event. The interview.completed event fires once an interview is finished and scored and carries the participant external_id, source and a trust block (quality score 1 to 5, meets_quality_bar, fraud_risk). Deliveries are signed with Standard Webhooks, retried seven times over about 35 hours, logged with resend for 30 days, and endpoints are turned off after 3 days of failures. Owners and admins manage webhooks. Included on Insights, Interviews and Enterprise plans, not Free.","content":"# Webhook Setup\n\nKoji sends your server a signed webhook each time a participant finishes an interview. Open any study, go to **Integrate**, then **Webhooks**, and add the HTTPS address of your receiver. From then on Koji POSTs an `interview.completed` event as soon as an interview is finished and scored, with your own ID for the participant and a quality score for their answers. Webhooks are included on the Insights, Interviews and Enterprise plans.\n\nUse them to send a reward the moment someone finishes, update a contact in your CRM, post to a team channel through an automation tool, or start the next step of a research pipeline without polling.\n\n---\n\n## What a webhook gives you\n\n| What | Detail |\n|---|---|\n| Event | `interview.completed`, once per interview per webhook |\n| When it fires | After a participant finishes and Koji has scored the interview, including interviews a researcher closes with \"Mark as done\" |\n| Not sent for | Partial or abandoned interviews, previews and deleted interviews |\n| Who it covers | One study, or every study in the workspace including new ones |\n| Security | Signed with the Standard Webhooks scheme, HTTPS only |\n| Reliability | Retried for about 35 hours, with a delivery log you can resend from |\n| Plans | Insights, Interviews and Enterprise (not Free) |\n\n---\n\n## Set up a webhook\n\nOnly workspace owners and admins can add or change webhooks. Other editors can see that a webhook exists, but not its full address, because automation URLs often carry their own secret token.\n\n1. **Open the study and go to Integrate, then Webhooks.** If your plan doesn’t include webhooks, you’ll see the layout with a link to the plans.\n2. **Click Add webhook.** Paste the endpoint URL. It must be a public `https://` address; Koji refuses `localhost`, private networks and URLs with a username or password in them. Give it a name if you like (\"Rewards service\").\n3. **Pick the event.** `interview.completed` is the event you subscribe to.\n4. **Choose which studies.** \"Only this study\" is the recommended default. \"All studies, including new ones\" covers the whole workspace; use `data.study_id` in the payload to tell studies apart.\n5. **Copy the signing secret.** Open the webhook, reveal the signing secret (it starts with `whsec_`) and store it in your server’s environment, for example as `KOJI_WEBHOOK_SECRET`. Never commit it or log it.\n6. **Send a test event.** Koji posts a `webhook.test` message with the real shape and obviously fake values. The delivery log shows the status code your server answered with and the start of its response body.\n\n![Process showing six steps to set up a Koji webhook, from opening Integrate to sending a test event and watching the delivery log.](/visuals/koji-webhook-setup-steps/koji-webhook-setup-steps.png \"Six steps from an empty Webhooks tab to a receiver you can trust\")\n\n**Let a coding assistant build the receiver.** The Webhooks tab has a \"Give this to your AI\" brief: one Markdown document with the payload, the headers, the signature check, retry rules and a test checklist. Copy it into Claude, Cursor or Copilot and ask for a receiver in your stack. The brief never contains your secret; it tells the assistant to read it from the environment.\n\n---\n\n## The payload\n\nEvery delivery is a JSON POST. Here is an `interview.completed` event:\n\n```json\n{\n  \"id\": \"msg_2f6c0d1e8a4b4c2d9e3f0a1b2c3d4e5f\",\n  \"type\": \"interview.completed\",\n  \"created_at\": \"2026-11-11T10:04:12.000Z\",\n  \"workspace_id\": \"7a1d3c5e-9b2f-4d6a-8c0e-1f3b5d7a9c2e\",\n  \"data\": {\n    \"interview_id\": \"9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c\",\n    \"study_id\": \"3b8e1f2a-6c4d-4a9e-b7f1-0d2c5e8a1b3f\",\n    \"study_name\": \"Onboarding feedback\",\n    \"completed_at\": \"2026-11-11T10:02:55Z\",\n    \"duration_seconds\": 512,\n    \"mode\": \"text\",\n    \"participant\": {\n      \"id\": \"e2b4d6f8-1a3c-4e5b-9d7f-0c2a4e6b8d1f\",\n      \"respondent_id\": \"c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f\",\n      \"source\": \"csv_import\",\n      \"external_id\": \"user_12345\",\n      \"email\": \"jamie@example.com\",\n      \"panel\": null\n    },\n    \"trust\": {\n      \"quality_score\": 4,\n      \"quality_level\": \"Good\",\n      \"meets_quality_bar\": true,\n      \"fraud_risk\": \"low\"\n    },\n    \"links\": {\n      \"api\": \"https://www.koji.so/api/v1/interviews/9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c\",\n      \"app\": \"https://www.koji.so/t/acme/p/3b8e1f2a-6c4d-4a9e-b7f1-0d2c5e8a1b3f/results/9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c\"\n    }\n  }\n}\n```\n\nThe fields you will use most:\n\n- **`data.participant.external_id`**: your own user ID, when you gave it to Koji (an ID column in an import, or `respondent.external_id` when you start interviews through the API). It is `null` for people who came through the public link and were never imported. Don’t try to match those by name.\n- **`data.participant.source`**: how they arrived: `organic`, `custom_link`, `csv_import`, `api`, `call_link`, `phone_import` or `panel`. More values may be added.\n- **`data.participant.panel`**: set only for participants recruited through Koji’s research panel. The panel pays their incentive, so don’t reward them a second time.\n- **`data.trust.quality_score`** and **`quality_level`**: 1 to 5, shown as Shallow (1 to 2), Fair (3), Good (4) or Excellent (5). See [Understanding Quality Scores](/docs/understanding-quality-scores).\n- **`data.trust.meets_quality_bar`**: true at a score of 3 or more, the same line Koji uses for its own charging and incentives.\n- **`data.trust.fraud_risk`**: `high`, `medium`, `low` or `unknown`, in beta. It comes from how typed answers were entered, so it is always `unknown` for voice and phone interviews. Treat `unknown` as no evidence either way.\n- **`data.links.api`**: the full interview, with transcript and analysis, from the REST API. It needs an API key for that study.\n\n---\n\n## Verify every request\n\nKoji signs each delivery with the [Standard Webhooks](https://www.standardwebhooks.com/) scheme, an open specification for signing webhooks. Each request carries three headers:\n\n| Header | Meaning |\n|---|---|\n| `webhook-id` | Unique per event and the same across retries and resends. Use it to ignore repeats. |\n| `webhook-timestamp` | Unix seconds when this attempt was sent. |\n| `webhook-signature` | One or more `v1,<signature>` values, separated by spaces. There are two during a secret rotation. |\n\nTo check a request: take the raw body exactly as received (before any JSON parsing), build the string `{webhook-id}.{webhook-timestamp}.{raw body}`, sign it with HMAC-SHA256 using the base64-decoded part of the secret after `whsec_`, and compare it in constant time with each `v1` signature. Refuse timestamps more than five minutes from now, and answer 401 when the check fails.\n\nThe simplest route is the official library:\n\n```javascript\n// npm install standardwebhooks\nimport { Webhook } from 'standardwebhooks'\n\nconst wh = new Webhook(process.env.KOJI_WEBHOOK_SECRET)\n// Throws if the signature or timestamp is wrong.\nconst event = wh.verify(rawBody, headers)\n```\n\nThe Verify signatures panel in the Webhooks tab has dependency-free versions in Node and Python if you would rather not add a package. With Express, read the body with `express.raw({ type: 'application/json' })`, not `express.json`, or the bytes you verify won’t be the bytes Koji signed.\n\n---\n\n## Delivery rules\n\n- **Answer fast.** Any 2xx within 10 seconds is a success. Anything else, a timeout or a connection error counts as a failure. Queue the real work and reply straight away.\n- **Retries.** Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours: seven attempts over about 35 hours. After that the delivery is marked failed, and you can resend it by hand for 30 days.\n- **At least once, in no fixed order.** Store each processed `webhook-id` and skip repeats. For rewards, also make `data.interview_id` unique, so one interview can never pay twice even if someone resends it.\n- **No redirects.** Koji doesn’t follow them, so register the final URL.\n- **Automatic switch-off.** If an endpoint keeps failing for 3 days (at least 5 failed attempts since its last success), Koji turns it off and emails the workspace owners and admins. Fix the receiver, then turn the webhook back on from its menu.\n- **Secret rotation.** Rotate the signing secret from the webhook’s menu. The old secret keeps signing alongside the new one for 24 hours, so you can deploy without dropping events.\n\nThe delivery log on each webhook filters by Delivered, Retrying and Failed, shows every attempt with its status code, and has a Resend button that sends the same message again with the same ID.\n\n---\n\n## Testing locally\n\nKoji only sends to public HTTPS addresses. To test a receiver on your laptop, expose it with a tunnel such as `cloudflared tunnel` or `ngrok http`, add the tunnel URL as a webhook, and click **Send test event**. Test events have the type `webhook.test`: log them and answer 2xx, and make sure they can never trigger a reward.\n\nWorth covering in your tests: a valid signature passes, a changed body fails, an old timestamp fails, a repeated `webhook-id` is ignored, a test event never pays, and panel or anonymous participants never pay.\n\n---\n\n## Automation tools and fallbacks\n\n**Zapier, Make and n8n.** Add the tool’s catch-hook or webhook-trigger URL as the endpoint. These tools don’t verify Standard Webhooks signatures on their own, so treat the hook URL as a secret and filter on `type` = `interview.completed` in the first step. See [Zapier Research Automation](/docs/zapier-research-automation).\n\n**Polling is still there.** `GET /api/v1/interviews/{id}` returns status, transcript and analysis with an API key (API keys come with the Interviews and Enterprise plans). Use it to fetch the transcript after a webhook arrives, or as a nightly safety net that catches anything your receiver missed. Mind the limits in [Rate Limits and CORS](/docs/rate-limits-and-cors).\n\n**Embed events for the page.** If you run interviews in the embed widget, the `koji:interview_completed` browser event fires the moment the participant finishes, which is useful for showing a thank-you state in your app. It only fires in an open tab, so keep the webhook as your source of truth. See [Embed Widget Reference](/docs/embed-widget-reference).\n\n---\n\n## A common use: rewarding participants\n\nProduct teams most often wire webhooks to a rewards service. A sensible starting policy:\n\n1. Skip `webhook.test` events.\n2. Skip panel participants and anyone without an `external_id`.\n3. Skip interviews where `meets_quality_bar` is false or `fraud_risk` is `high`.\n4. Pay a larger reward for a quality score of 4 or more, a standard one otherwise.\n5. Use `data.interview_id` as the idempotency key.\n\nAdjust the thresholds to your own incentive rules, and tell participants up front what they need to do to earn the reward.\n\n---\n\n## FAQ\n\n### Where do I add a webhook URL?\n\nIn any study, open Integrate, then Webhooks, and click Add webhook. Only workspace owners and admins can add or change one.\n\n### Which plans include webhooks?\n\nInsights, Interviews and Enterprise. The Free plan doesn’t include them, and moving a workspace to Free turns them off.\n\n### Does a webhook cover every study?\n\nYou choose. \"Only this study\" covers one study, and \"All studies, including new ones\" covers the whole workspace. Use `data.study_id` to tell studies apart.\n\n### Is the transcript in the webhook?\n\nNo. The webhook carries IDs, timing, the participant’s identifiers and the trust signals. Call `data.links.api` with an API key for that study to fetch the full transcript and analysis.\n\n### What happens if my server is down?\n\nKoji retries seven times over about 35 hours. Failed deliveries stay in the log for 30 days and can be resent. If an endpoint fails for 3 days straight, Koji turns it off and emails the workspace owners and admins.\n\n### Are partial interviews sent?\n\nNo. `interview.completed` fires only for finished interviews, including ones a researcher closes with \"Mark as done\". Partial and abandoned interviews, previews and deleted interviews are not sent.\n\n---\n\n## Related Resources\n\n- [Research Automation with Webhooks](/docs/research-automation-webhooks)\n- [Completing Interviews via API](/docs/completing-interviews-via-api)\n- [API Authentication](/docs/api-authentication)\n- [Understanding Quality Scores](/docs/understanding-quality-scores)\n- [CRM Research Integration Guide](/docs/crm-research-integration-guide)\n- [Structured Questions Guide](/docs/structured-questions-guide)","category":"API Reference","lastModified":"2026-11-13T05:10:56.035927+00:00","metaTitle":"Webhook Setup: interview.completed events | Koji","metaDescription":"Add a webhook in any Koji study and receive a signed interview.completed event with your participant ID and quality score. Setup, payload, signatures, retries.","keywords":["koji webhooks","webhook setup","interview completed webhook","standard webhooks signature","research automation webhooks","user research api webhooks"],"aiSummary":"Koji sends signed outbound webhooks. In any study open Integrate, then Webhooks, add a public https endpoint, choose one study or all studies, store the whsec_ signing secret and send a test event. The interview.completed event fires once an interview is finished and scored and carries the participant external_id, source and a trust block (quality score 1 to 5, meets_quality_bar, fraud_risk). Deliveries are signed with Standard Webhooks, retried seven times over about 35 hours, logged with resend for 30 days, and endpoints are turned off after 3 days of failures. Owners and admins manage webhooks. Included on Insights, Interviews and Enterprise plans, not Free.","aiPrerequisites":["api-authentication"],"aiLearningOutcomes":["Add a webhook endpoint to a Koji study and send a test event","Read the interview.completed payload, including external_id and trust signals","Verify Standard Webhooks signatures on your server","Handle retries, duplicates, secret rotation and auto-disable","Use the webhook to reward participants safely"],"aiDifficulty":"intermediate","aiEstimatedTime":"10 min read"}],"pagination":{"total":1,"returned":1,"offset":0}}