Docs
API reference
The Nosie API is a small REST surface — seven resources over plain JSON — for creating interview studies, adding consented contacts, launching outreach, and reading results. Base URL: https://nosie.app/api/v1. The machine-readable OpenAPI 3.1 spec lives at /api/v1/openapi.json.
Last updated: 25 August 2026
Authentication & limits
Authenticate every request with an API key in the Authorization header. Keys are created in the dashboard under Developers, shown once, and prefixed nk_live_.
Authorization: Bearer nk_live_...- Rate limit: 60 requests/minute per key. Exceeding it returns
429with aRetry-After: 60header. - Content type:
application/jsonfor all request bodies. - IDs are UUIDs.
Endpoints
/v1/projectsCreate a project
A project is the product being researched. Studies hang off it, and contacts belong to it. Body: { name, website_url? }.
/v1/projectsList projects
/v1/projects/{id}Get a project
/v1/projects/{id}/contactsAdd contacts (consent required)
Body: { contacts: Contact[], consent: Consent }. Every request must carry a consent attestation; requests without one are rejected with 422.
A contact is a person on this product, not a participant in one study. Enrol them into a study with POST /v1/studies/{id}/enrolments. An opt-out is project-wide and permanent: it covers every study of that project, including ones created later.
/v1/projects/{id}/contactsList project contacts
/v1/studiesList studies
/v1/studiesCreate a study
A study is one interview effort against a project — one objective, one brief. Body: { project_id, name, brief } — see the Brief schema below. Returns 201 with the created study, or 422 if validation fails.
Add recurring: true and context_slots to run the study in repeated waves — the same people, interviewed again each round. Because a recurring study is quiet between waves by design, it is exempt from the exhaustion close and its quota counts per wave rather than across the whole study. That also means it needs some other way to finish, so it requires at least one of close_criteria.ends_at, close_criteria.spend_cap_cents or quota.
Briefs also accept two optional specificity levers: probes (up to 8 must-hit questions the interview cannot end without asking) and boundaries (up to 500 characters describing what the interviewer must not do).
Three optional fields decide how the study sounds. voice_id is an ElevenLabs voice id, for a study whose interviewer is a named character rather than a researcher; absent, every study shares the same default voice. first_message replaces the generated opening outright, and may carry {{slots}} declared in context_slots. consent_method 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. It never affects whether a contact may be interviewed; each contact's own consent attestation governs that.
/v1/studies/{id}Get a study
/v1/studies/{id}Update a study
Only the fields you send change: name, external_ref, brief, context_slots, close_criteria, quota and retention_days. brief is a whole Brief rather than a patch of one — GET the study, edit what you want, send it back. Merging a partial brief would mean you could set an optional field but never clear it again.
You can’t change project_id, recurring, status or research_mode. The project owns the contacts; recurring decides whether a quota counts per wave and whether a study can close on exhaustion, so changing it would reinterpret enrolments that already exist; status has its own launch and pause endpoints; and research mode is frozen once an interview has completed. Create a new study instead.
The rules are checked against the study as it would be after your change, not against the body you sent — so removing a context_slots entry the stored brief still uses is refused, the same as adding a slot nothing supplies.
Changing the brief re-provisions the study’s voice agent, and the response carries agent_sync. If the provider rejects the update you get a 502 with study_updated: true: the study is saved but calls still use the previous voice and opening. Retry — PATCH replaces rather than accumulates, so repeating it is safe.
/v1/studies/{id}/enrolmentsEnrol contacts into this study
Body: { contact_ids: string[] }. The contacts must already exist on this study's project. A contact who has opted out is refused individually and reported in rejected — that refusal is enforced by the database and cannot be overridden, so the rest of the batch still enrols. It applies to every wave, including ones created later: a wave is a new round of contact, not new consent.
To run waves, send { enrolments: [{ contact_id, wave, context }] } instead. Exactly one of the two shapes must be present; a body carrying 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", "day-30". Nosie never parses it and never infers order from it; rounds are ordered by when their first enrolment was created, so your own naming scheme does not have to mean anything to us. The same person can be enrolled once per wave.
context carries per-person, per-wave values substituted into the interview, so two people in the same wave can be asked about different things. Every key must be declared in the study's context_slots and every declared slot must be supplied, or the enrolment is refused with 422 — an undeclared slot is never substituted, and the interviewer would read it aloud verbatim. Limits: 4,000 characters per value, 8,000 per enrolment.
If the study is active, outreach for these enrolments starts immediately — you do not need to launch again. The response includes scheduled and held; “held” means a contact's quota group is already full, so they stay pending and start automatically if you raise that target. This is what makes a continuously-refreshing cohort work without a weekly re-launch.
/v1/studies/{id}/enrolmentsList enrolments
/v1/studies/{id}/contactsList participants
/v1/studies/{id}/contactsMoved — returns 410
Contacts belong to a project, not a study. Create them with POST /v1/projects/{id}/contacts, then enrol them with POST /v1/studies/{id}/enrolments.
/v1/studies/{id}/quotaLive quota state
Per-stratum progress: completed vs target, which groups are full, how many contacts are held, and how many were screened out. Returns 404 when the study has no quota configured. Interviews beyond a target still complete and still count, but are absorbed by Nosie rather than billed to you.
/v1/studies/{id}/launchLaunch outreach
Starts contacting every eligible contact within their local calling hours. Returns a launch summary.
/v1/studies/{id}/pausePause outreach
/v1/contacts/{id}/resultGet a contact's interview result
Returns the transcript, summary, extracted attributes, quality signals, and outreach history.
/v1/webhooksList webhooks
/v1/webhooksCreate a webhook
The signing secret is returned once, on creation. See Webhooks for events and signature verification.
/v1/webhooks/{id}Delete a webhook
Brief schema
The brief is the interview's contract: what to learn, from whom, and exactly which attributes to extract.
| Field | Type | Description |
|---|---|---|
| objective * | string | What you want to learn. |
| audience_description * | string | Who is being interviewed. |
| key_topics * | string[] | 3–6 topics the conversation must cover. |
| success_criteria * | string | What a successful interview produces. |
| identity_label * | string | Who Nosie says it's calling on behalf of. |
| tone | string | Conversation tone. Default: "warm, concise, respectful". |
| allowed_modes * | ("call"|"sms"|"email")[] | Channels Nosie may use. |
| timebox_seconds | integer | Interview length cap. Default: 180. |
| output_schema * | object | Map of attribute key → { type: string|number|boolean, description }. Extracted answers are returned under these exact keys. |
| schedule | object | Optional scheduling constraints. |
Contact schema
| Field | Type | Description |
|---|---|---|
| name * | string | Contact's name. |
| phone | string | E.164 format, e.g. +64211234567. Required for call/sms modes. |
| string | Required for email mode. | |
| allowed_modes * | ("call"|"sms"|"email")[] | Channels this contact may be reached on. |
| timezone | string | IANA timezone, e.g. Pacific/Auckland. Used for local calling hours. |
| country | "NZ" | "AU" | "US" | Contact's country. |
| custom_fields | object | String key-value pairs carried through to results. |
Consent schema
422. Opt-outs are permanent and cannot be reversed via the API.| Field | Type | Description |
|---|---|---|
| attested * | true | Literal true — you attest these contacts agreed to be contacted. |
| basis * | string | How consent was collected, minimum 10 characters. Stored in the consent audit log. |
| method * | enum | One of: signup_form, verbal, written, existing_relationship, other. |
Errors
Errors are JSON: { "error": "<message>" }.
| Field | Type | Description |
|---|---|---|
| 401 | unauthorized | Missing, invalid, or revoked API key. |
| 404 | not found | Resource doesn't exist or belongs to another account. |
| 422 | validation | Body failed validation — including any contact-create without a consent attestation. |
| 429 | rate limited | More than 60 requests/minute. Retry after the Retry-After header (60s). |
Machine-readable versions: OpenAPI spec · llms.txt · llms-full.txt. Questions? hello@nosie.app