Skip to content

Docs

Webhooks

Webhooks push interview results to your endpoint the moment they happen — no polling. Every delivery is a JSON POST, signed with HMAC-SHA256 so you can verify it came from Nosie. Create webhooks in the dashboard under Developers or via POST /v1/webhooks; the signing secret is returned once, on creation.

Last updated: 6 July 2026

What does a delivery look like?

Every event shares one envelope: event, created_at, and an event-specific data object.

POST to your endpoint · contact.completed
{
  "event": "contact.completed",
  "created_at": "2026-07-06T02:14:07.000Z",
  "data": {
    "contact_id": "9be0c1f2-…",
    "project_id": "4d7a55e8-…",
    "interview_id": "c3a91b04-…",
    "summary": "- Switched from a whiteboard after double-bookings…",
    "attributes": {
      "previous_tool": "whiteboard + group chat",
      "switch_trigger": "missed jobs",
      "team_size": 6
    },
    "quality": { "engagement_level": "high", "coverage": 0.85 }
  }
}

Headers on every delivery: Content-Type: application/json and X-Nosie-Signature.

Which events can I subscribe to?

Choose events when creating the webhook; subscribing to none means all events.

FieldTypeDescription
contact.invitedeventAn invitation went out. data: contact_id, project_id, mode.
contact.call_startedeventA voice call began. data: contact_id, project_id, attempt_no.
contact.completedeventInterview finished. data: contact_id, project_id, interview_id, summary, attributes (your output_schema keys), quality.
contact.failedeventAll attempts exhausted. data: contact_id, project_id, completion_status.
contact.opted_outeventThe contact opted out — authoritative and permanent. data: contact_id, channel.
contact.disqualifiedeventThe contact was screened out before an interview ran — terminal, and never billed. data: contact_id, project_id, reason (answer | stratum_full), answers.
project.completedeventEvery contact in a project has reached a terminal state, or the project's quota filled.
usage.recordedeventA billable completed interview was recorded. data: interview_id, project_id. Interviews beyond a project's quota are absorbed by Nosie and do not emit this event.
incentive.earnedeventA contact completed an interview on a project that declares an incentive. Nosie never issues the reward — you do, in your own system. data: contact_id, project_id, interview_id, description.

How do I verify a signature?

The X-Nosie-Signature header has the form t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of the string `${t}.${rawBody}` keyed with your webhook secret. Compute it over the raw request body — before any JSON parsing.

verify.ts (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyNosieSignature(
  header: string, // X-Nosie-Signature: "t=1751760000,v1=6f2a…"
  rawBody: string, // the exact request body string
  secret: string // whsec from webhook creation (shown once)
): boolean {
  const { t, v1 } = Object.fromEntries(
    header.split(",").map((part) => part.split("=", 2))
  );
  if (!t || !v1) return false;

  // Reject stale timestamps to prevent replay (5 minutes).
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Retries & idempotency

  • Respond with a 2xx within 10 seconds; anything else counts as a failed attempt. Do slow work after acknowledging.
  • Failed deliveries are retried with backoff, up to 5 attempts.
  • Delivery is at-least-once — key your processing on interview_id / contact_id so duplicates are harmless.
Endpoint requirements: HTTPS URL, reachable from the public internet. Test locally with a tunnel (e.g. an ngrok or cloudflared URL) pointed at your dev server. The full API surface is described in the API reference.

Machine-readable versions: OpenAPI spec · llms.txt · llms-full.txt. Questions? hello@nosie.app