Back to docs
API Reference

Webhook Setup

Set up Koji webhooks: get a signed interview.completed message on your server each time a participant finishes, with your own participant ID and a quality score.

Koji 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.

Use 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.


What a webhook gives you

WhatDetail
Eventinterview.completed, once per interview per webhook
When it firesAfter a participant finishes and Koji has scored the interview, including interviews a researcher closes with "Mark as done"
Not sent forPartial or abandoned interviews, previews and deleted interviews
Who it coversOne study, or every study in the workspace including new ones
SecuritySigned with the Standard Webhooks scheme, HTTPS only
ReliabilityRetried for about 35 hours, with a delivery log you can resend from
PlansInsights, Interviews and Enterprise (not Free)

Set up a webhook

Only 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.

  1. 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.
  2. 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").
  3. Pick the event. interview.completed is the event you subscribe to.
  4. 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.
  5. 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.
  6. 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.
Process showing six steps to set up a Koji webhook, from opening Integrate to sending a test event and watching the delivery log.
Six steps from an empty Webhooks tab to a receiver you can trust · Download or share

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.


The payload

Every delivery is a JSON POST. Here is an interview.completed event:

{
  "id": "msg_2f6c0d1e8a4b4c2d9e3f0a1b2c3d4e5f",
  "type": "interview.completed",
  "created_at": "2026-11-11T10:04:12.000Z",
  "workspace_id": "7a1d3c5e-9b2f-4d6a-8c0e-1f3b5d7a9c2e",
  "data": {
    "interview_id": "9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c",
    "study_id": "3b8e1f2a-6c4d-4a9e-b7f1-0d2c5e8a1b3f",
    "study_name": "Onboarding feedback",
    "completed_at": "2026-11-11T10:02:55Z",
    "duration_seconds": 512,
    "mode": "text",
    "participant": {
      "id": "e2b4d6f8-1a3c-4e5b-9d7f-0c2a4e6b8d1f",
      "respondent_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
      "source": "csv_import",
      "external_id": "user_12345",
      "email": "[email protected]",
      "panel": null
    },
    "trust": {
      "quality_score": 4,
      "quality_level": "Good",
      "meets_quality_bar": true,
      "fraud_risk": "low"
    },
    "links": {
      "api": "https://www.koji.so/api/v1/interviews/9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c",
      "app": "https://www.koji.so/t/acme/p/3b8e1f2a-6c4d-4a9e-b7f1-0d2c5e8a1b3f/results/9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c"
    }
  }
}

The fields you will use most:

  • 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.
  • data.participant.source: how they arrived: organic, custom_link, csv_import, api, call_link, phone_import or panel. More values may be added.
  • 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.
  • 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.
  • data.trust.meets_quality_bar: true at a score of 3 or more, the same line Koji uses for its own charging and incentives.
  • 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.
  • data.links.api: the full interview, with transcript and analysis, from the REST API. It needs an API key for that study.

Verify every request

Koji signs each delivery with the Standard Webhooks scheme, an open specification for signing webhooks. Each request carries three headers:

HeaderMeaning
webhook-idUnique per event and the same across retries and resends. Use it to ignore repeats.
webhook-timestampUnix seconds when this attempt was sent.
webhook-signatureOne or more v1,<signature> values, separated by spaces. There are two during a secret rotation.

To 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.

The simplest route is the official library:

// npm install standardwebhooks
import { Webhook } from 'standardwebhooks'

const wh = new Webhook(process.env.KOJI_WEBHOOK_SECRET)
// Throws if the signature or timestamp is wrong.
const event = wh.verify(rawBody, headers)

The 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.


Delivery rules

  • 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.
  • 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.
  • 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.
  • No redirects. Koji doesn’t follow them, so register the final URL.
  • 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.
  • 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.

The 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.


Testing locally

Koji 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.

Worth 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.


Automation tools and fallbacks

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.

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.

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.


A common use: rewarding participants

Product teams most often wire webhooks to a rewards service. A sensible starting policy:

  1. Skip webhook.test events.
  2. Skip panel participants and anyone without an external_id.
  3. Skip interviews where meets_quality_bar is false or fraud_risk is high.
  4. Pay a larger reward for a quality score of 4 or more, a standard one otherwise.
  5. Use data.interview_id as the idempotency key.

Adjust the thresholds to your own incentive rules, and tell participants up front what they need to do to earn the reward.


FAQ

Where do I add a webhook URL?

In any study, open Integrate, then Webhooks, and click Add webhook. Only workspace owners and admins can add or change one.

Which plans include webhooks?

Insights, Interviews and Enterprise. The Free plan doesn’t include them, and moving a workspace to Free turns them off.

Does a webhook cover every study?

You 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.

Is the transcript in the webhook?

No. 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.

What happens if my server is down?

Koji 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.

Are partial interviews sent?

No. 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.


Related Resources