# Nosie — full integration documentation > Nosie (nosie.app) runs brief, evidence-based micro-interviews with customers — by phone call, SMS/chat, or email — and returns structured results: a transcript, a summary, attributes mapped to a user-defined schema, and quality signals. A product of Ortomate Ltd., Wellington, New Zealand. Last updated: 30 August 2026. HTML versions: https://nosie.app/docs · https://nosie.app/docs/api · https://nosie.app/docs/webhooks. OpenAPI 3.1 spec: https://nosie.app/api/v1/openapi.json ## The two entity levels Nosie separates the thing being researched from the research itself, and getting this right is most of understanding the API: - A **project** is the product or domain being researched. Contacts belong to the project, and so does an opt-out. - A **study** is one interview effort against that project — one objective, one brief, one set of questions. A project can have many studies over time. - A **wave** is one round of a recurring study. The same people, interviewed again — a weekly diary study, a 30/60/90-day onboarding sequence, a quarterly check-in. - An **enrolment** is one person's participation in one study, in one wave. Per-study state (status, stratum, screener answers, per-round context) lives here. - An **interview** is a single completed conversation. The practical consequence: you add a contact **to a project**, then **enrol** them into a study. A person who opts out is opted out of the whole project, permanently, across every study — that is enforced in the database, not in application code. ## Integrating Nosie There are two ways to use Nosie: manually from the dashboard (no code), or programmatically through the REST API and webhooks. Both share the same projects, studies, contacts, and results. ### Using the dashboard (no code) 1. Create a project: tell Nosie what product you are researching. Nosie researches the domain so it does not arrive knowing nothing. 2. Create a study: answer a short chat about what you want to learn. Nosie drafts a structured interview brief (objective, audience, key topics, attributes to extract) which you review and approve before anything goes live. 3. Add contacts to the project: one at a time or by CSV import. CSV headers: name, phone, email, modes ("call|sms|email"), timezone, country — any other column becomes a custom field. Every add requires a consent attestation. 4. Launch the study: Nosie reaches each enrolled contact in their allowed modes within local calling hours, asks follow-ups, and respects opt-outs immediately and permanently. 5. Read results: each completed interview appears under the study's Results tab with summary, attributes, and quality signals — exportable as CSV or JSON. ### The consent rule (cannot be disabled) Every contact-create — dashboard, CSV, or API — must carry a consent attestation: - attested: literal true - basis: how consent was collected, minimum 10 characters (stored in the consent audit log) - method: one of signup_form | verbal | written | existing_relationship | other API requests without one are rejected with HTTP 422. Opt-outs are honoured immediately and can never be reversed, by anyone. An opt-out is project-wide: it covers every study of that project, including studies created later. ## REST API Base URL: https://nosie.app/api/v1 Auth: `Authorization: Bearer ` — keys are created in the dashboard (Developers page), shown once, prefixed nk_live_ Rate limit: 60 requests/minute per key (429 + Retry-After: 60 when exceeded) Content type: application/json. IDs are UUIDs. Errors are JSON: { "error": "" } with status 401 (bad/missing key), 404 (not found), 409 (conflict with the record's state: archived project or study, launch refused, archive refused — body carries a `message`), 410 (endpoint moved), 422 (validation / missing consent), 429 (rate limit). ### Endpoints - GET /v1/projects — list projects - POST /v1/projects — create a project and start researching it - GET /v1/projects/{id} — get a project - POST /v1/projects/{id}/archive — archive the project and every study left in it; 409 while any study is active or launching (pause first); contacts are untouched; not reversible - POST /v1/projects/{id}/contacts — add contacts to the project; body { contacts: Contact[], consent: Consent }; 422 without consent; 409 project_archived if the project is archived - GET /v1/projects/{id}/contacts — list the project's contacts - GET /v1/studies — list studies - POST /v1/studies — create a study; body { project_id, name, brief }; 409 project_archived if the project is archived - GET /v1/studies/{id} — get a study - PATCH /v1/studies/{id} — update name, external_ref, brief, context_slots, close_criteria, quota or retention_days; a change to brief, name or retention_days re-provisions the voice agent (response carries agent_sync; 502 agent_sync_failed if the provider refused) - POST /v1/studies/{id}/enrolments — enrol existing project contacts into this study; accepts { contact_ids } or { enrolments: [{ contact_id, wave, context }] }; 409 study_archived if the study is archived - GET /v1/studies/{id}/enrolments — list this study's enrolments - GET /v1/studies/{id}/contacts — list this study's participants - POST /v1/studies/{id}/contacts — MOVED. Returns 410; create contacts on the project, then enrol them - POST /v1/studies/{id}/launch — launch outreach for this study - POST /v1/studies/{id}/pause — pause outreach for this study - POST /v1/studies/{id}/archive — archive the study (from draft, paused or completed; 409 while outreach is running); cancels scheduled outreach; hidden from the dashboard; not reversible - GET /v1/studies/{id}/quota — quota and strata progress - GET /v1/contacts/{id}/result — transcript, summary, attributes, quality, and the full interview history for this person - POST /v1/contacts/{id}/opt-out — record an opt-out the person gave you directly; body { channel: "email"|"phone"|"in_person"|"other", note? }; project-wide and permanent; returns { opted_out: true, already_opted_out: false }, or already_opted_out: true if they already were and nothing changed - GET /v1/webhooks — list webhooks - POST /v1/webhooks — create a webhook (signing secret returned once) - PATCH /v1/webhooks/{id} — pause or resume deliveries; body { active: boolean }; returns the webhook - DELETE /v1/webhooks/{id} — delete a webhook ### Brief schema (required on study create) - objective (string, required): what you want to learn - audience_description (string, required): who is being interviewed - key_topics (string[], required): 3–6 topics the conversation must cover - success_criteria (string, required): what a successful interview produces - identity_label (string, required): who Nosie says it's calling on behalf of - tone (string, optional): default "warm, concise, respectful" - allowed_modes (("call"|"sms"|"email")[], required) - timebox_seconds (integer, optional): default 180 - output_schema (object, required): map of attribute key → { type: "string"|"number"|"boolean", description }. Extracted answers come back under these exact keys; unknowns are omitted, never guessed. - probes (string[], optional): up to 8 must-hit questions the interview cannot end without asking - boundaries (string, optional): up to 500 characters of what the interview must not do - first_message (string, optional): overrides the generated opening. May carry {{slots}}, which are rendered per enrolment when the call is placed and must appear in the study's context_slots - voice_id (string, optional): ElevenLabs voice id (20 characters of A-Za-z0-9) for this study's agent. Absent uses the platform default voice - consent_method (string, optional): one of signup_form, verbal, written, existing_relationship, other. Only changes how the generated opening describes the consent it relies on — "you opted in" is true of a signup form and wrong for anything else. Never affects whether a contact may be interviewed; each contact's own consent attestation governs that - schedule (object, optional): { type: "asap" } (default) calls inside Nosie's 9am–8pm window in each contact's local time. Add allowed_local_hours: { from, to } (hours 0–23, contact-local, clamped to 8am–9pm) to set the study's own hours. type: "window" is accepted with the same field for compatibility; a study's end date is close_criteria.ends_at, not the schedule ### Contact schema - name (string, required) - phone (string, optional): E.164 format, e.g. +64211234567 — required for call/sms modes - email (string, optional): required for email mode - allowed_modes (("call"|"sms"|"email")[], required) - timezone (string, optional): IANA, e.g. Pacific/Auckland — used for local calling hours - country ("NZ"|"AU"|"US", optional) - custom_fields (object of string values, optional): carried through to results ### Example: quickstart in five calls 1. Create a project — the product you are researching: ``` curl -X POST https://nosie.app/api/v1/projects \ -H "Authorization: Bearer nk_live_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Scheduling", "website_url": "https://acme.example" }' ``` 2. Create a study against it: ``` curl -X POST https://nosie.app/api/v1/studies \ -H "Authorization: Bearer nk_live_..." \ -H "Content-Type: application/json" \ -d '{ "project_id": "PROJECT_ID", "name": "New-customer onboarding research", "brief": { "objective": "Understand why new customers signed up and where they stall in week one", "audience_description": "Admins who created an account in the last 14 days", "key_topics": ["previous tool", "reason for switching", "first-week friction"], "success_criteria": "Three specific friction points per customer", "identity_label": "the Acme Scheduling team", "allowed_modes": ["call", "email"], "timebox_seconds": 180, "output_schema": { "previous_tool": { "type": "string", "description": "What they used before" }, "switch_trigger": { "type": "string", "description": "The moment that made them switch" }, "team_size": { "type": "number", "description": "People who will use the product" } } } }' ``` 3. Add contacts to the PROJECT (consent attestation mandatory): ``` curl -X POST https://nosie.app/api/v1/projects/PROJECT_ID/contacts \ -H "Authorization: Bearer nk_live_..." \ -H "Content-Type: application/json" \ -d '{ "contacts": [ { "name": "Rewi Morgan", "phone": "+64211234567", "email": "rewi@example.co.nz", "allowed_modes": ["call", "email"], "timezone": "Pacific/Auckland", "country": "NZ" } ], "consent": { "attested": true, "basis": "Ticked the research callback opt-in on our signup form", "method": "signup_form" } }' ``` 4. Enrol those contacts into the study: ``` curl -X POST https://nosie.app/api/v1/studies/STUDY_ID/enrolments \ -H "Authorization: Bearer nk_live_..." \ -H "Content-Type: application/json" \ -d '{ "contact_ids": ["CONTACT_ID"] }' ``` An opted-out contact is refused individually and reported in `rejected`; the rest of the batch still enrols. 5. Launch: `curl -X POST https://nosie.app/api/v1/studies/STUDY_ID/launch -H "Authorization: Bearer nk_live_..."` Then receive results on a webhook (below) or poll GET /v1/contacts/{id}/result. ## Recurring studies (waves) A study can interview the same person more than once. Each round is a **wave**. Enrol per wave instead of by bare id: ``` curl -X POST https://nosie.app/api/v1/studies/STUDY_ID/enrolments \ -H "Authorization: Bearer nk_live_..." \ -H "Content-Type: application/json" \ -d '{ "enrolments": [ { "contact_id": "CONTACT_ID", "wave": "2026-08", "context": { "agenda": "the CRM data import they raised last month" } } ] }' ``` Exactly one of `contact_ids` or `enrolments` must be present. A body with both is rejected rather than guessed at. The `contact_ids` shape is unchanged and enrols into the default wave. - **`wave`** is an opaque round label — `"2026-08"`, `"q3-2026"`, `"day-30"`. Nosie never parses it and never infers order from it; rounds are ordered by when their first enrolment was created, so your naming scheme does not have to mean anything to us. The same contact may be enrolled once per wave. - **`context`** is per-person, per-wave values substituted into the interview, so two people in the same wave can be asked about different things. Reference them in the brief as `{{snake_case}}`. - Every context key must be declared in the study's `context_slots`, and every declared slot must be supplied, or the enrolment is refused with 422. This is deliberate: an undeclared slot is never substituted, and the interviewer would read it aloud verbatim to the participant. Limits are 4,000 characters per value and 8,000 per enrolment. Create the study with `recurring: true` and its `context_slots`: ``` { "project_id": "...", "name": "...", "recurring": true, "context_slots": ["agenda"], "close_criteria": { "ends_at": "2027-01-01T00:00:00Z" }, "brief": { "objective": "Follow up on {{agenda}}", ... } } ``` A recurring study is quiet between waves by design, so it is exempt from the exhaustion close and its quota counts per wave rather than across the study. That also means it needs another way to finish — supply `close_criteria.ends_at`, `close_criteria.spend_cap_cents` or `quota`, or the request is refused with 422. **Consent is unchanged.** A wave is a new round of contact, not new consent. An opted-out contact is refused from every wave, including waves created after they opted out. **Retention.** Interview content is purged at the end of the study's own retention period, set by `retention_days` on POST /v1/studies. It defaults to 90 days and can be anywhere from 30 to 1825 (five years), so rounds months or years apart can still be compared. The period can be reduced at any time but not raised once the study has interviews, because participants took part under the period disclosed at the time. Voice-provider retention is separate and stays at 90 days. ## Webhooks Webhooks push results to your HTTPS endpoint as JSON POSTs, signed with HMAC-SHA256. Create them in the dashboard or via POST /v1/webhooks; the signing secret is returned once. Subscribing to no events means all events. A webhook can be scoped to particular studies. ### Delivery envelope Every delivery: { "event": "", "created_at": "", "data": { ...event-specific } } Headers: Content-Type: application/json and X-Nosie-Signature. ### Events - contact.invited — invitation sent. data: contact_id, study_id, mode - contact.call_started — voice call began. data: contact_id, study_id, attempt_no - contact.completed — interview finished. data: contact_id, study_id, enrolment_id, wave, interview_id, summary, attributes (your output_schema keys), quality - contact.failed — attempts exhausted. data: contact_id, study_id, completion_status - contact.disqualified — screened out. data: contact_id, study_id, reason - contact.opted_out — contact opted out (permanent, project-wide). data: contact_id, channel - study.completed — every enrolment in a study reached a terminal state - usage.recorded — billable completed interview recorded. data: interview_id, study_id - incentive.earned — participant earned a declared incentive. data: contact_id, study_id Payloads also carry `external_ref` when the project has one, so you can key results to your own records without a lookup. Enrolment-scoped events additionally carry `enrolment_id` and `wave`, so you know which round a result belongs to without a second call; `wave` is `""` for a study that does not use waves. ### Signature verification X-Nosie-Signature has the form `t=,v1=`, where v1 = HMAC-SHA256(secret, `${t}.${rawBody}`) as hex. Compute over the raw request body before JSON parsing. Reject stale timestamps (e.g. older than 5 minutes) to prevent replay, and compare with a timing-safe equality check. ```js import { createHmac, timingSafeEqual } from "node:crypto"; function verifyNosieSignature(header, rawBody, secret) { const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=", 2))); if (!t || !v1) return false; 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 2xx within 10 seconds; anything else counts as a failed attempt. Do slow work after acknowledging. - Failed deliveries retry with backoff, up to 5 attempts. - Delivery is at-least-once — key processing on interview_id / contact_id so duplicates are harmless. ## Results Every completed interview produces: a speaker-labelled transcript plus short summary; attributes mapped to the exact output_schema keys (unknowns omitted, never guessed); and quality signals (engagement, coverage, duration, outcome). Delivery: webhooks (push), REST (pull), or CSV/JSON export from the dashboard. GET /v1/contacts/{id}/result returns `interviews` as the full history for that person, newest first, each with its own study_id and wave — one person can be interviewed by several studies of the same project, and several times within one recurring study. Order rounds by the interviews' own created_at rather than by parsing the wave label. ## Pricing NZ$20/month including 10 completed interviews, then NZ$2 per additional completed interview, billed monthly in arrears via Stripe. Voicemails, no-answers, failed attempts, and declines are never billed. Monthly spend cap available. Details: https://nosie.app/pricing ## Notes for AI agents - The user must create the API key themselves in the dashboard (Developers page) — keys are shown once and cannot be retrieved later. - Never fabricate a consent basis: a real human must be able to stand behind it. - PATCH /v1/studies/{id} takes a WHOLE brief, not a partial one. GET the study, edit the brief object, send it back. A merged partial brief could never clear an optional field. Fields it will not change: project_id, recurring, status (use launch/pause), research_mode. - PATCH rules are evaluated against the study as it would be after the change, not against the body. Removing a context_slots entry the stored brief still uses is refused (422 context_slots_mismatch) exactly like adding a slot nothing supplies. - Changing a brief, the study name or retention_days re-provisions the ElevenLabs agent. The response carries agent_sync. A 502 agent_sync_failed with study_updated: true means the study saved but calls still use the previous voice and opening — retry, PATCH replaces rather than accumulates. - Contacts belong to a project, not to a study. If you are reaching for POST /v1/studies/{id}/contacts, you want POST /v1/projects/{id}/contacts followed by POST /v1/studies/{id}/enrolments. - Contact: hello@nosie.app · Ortomate Ltd., 12 Jessie Street, Wellington, New Zealand.