Skip to content

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 429 with a Retry-After: 60 header.
  • Content type: application/json for all request bodies.
  • IDs are UUIDs.

Endpoints

POST/v1/projects

Create a project

A project is the product being researched. Studies hang off it, and contacts belong to it. Body: { name, website_url? }.

GET/v1/projects

List projects

GET/v1/projects/{id}

Get a project

POST/v1/projects/{id}/contacts

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

GET/v1/projects/{id}/contacts

List project contacts

GET/v1/studies

List studies

POST/v1/studies

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

GET/v1/studies/{id}

Get a study

PATCH/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.

POST/v1/studies/{id}/enrolments

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

GET/v1/studies/{id}/enrolments

List enrolments

GET/v1/studies/{id}/contacts

List participants

POST/v1/studies/{id}/contacts

Moved — 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.

GET/v1/studies/{id}/quota

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

POST/v1/studies/{id}/launch

Launch outreach

Starts contacting every eligible contact within their local calling hours. Returns a launch summary.

POST/v1/studies/{id}/pause

Pause outreach

GET/v1/contacts/{id}/result

Get a contact's interview result

Returns the transcript, summary, extracted attributes, quality signals, and outreach history.

GET/v1/webhooks

List webhooks

POST/v1/webhooks

Create a webhook

The signing secret is returned once, on creation. See Webhooks for events and signature verification.

DELETE/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.

FieldTypeDescription
objective *stringWhat you want to learn.
audience_description *stringWho is being interviewed.
key_topics *string[]3–6 topics the conversation must cover.
success_criteria *stringWhat a successful interview produces.
identity_label *stringWho Nosie says it's calling on behalf of.
tonestringConversation tone. Default: "warm, concise, respectful".
allowed_modes *("call"|"sms"|"email")[]Channels Nosie may use.
timebox_secondsintegerInterview length cap. Default: 180.
output_schema *objectMap of attribute key → { type: string|number|boolean, description }. Extracted answers are returned under these exact keys.
scheduleobjectOptional scheduling constraints.

Contact schema

FieldTypeDescription
name *stringContact's name.
phonestringE.164 format, e.g. +64211234567. Required for call/sms modes.
emailstringRequired for email mode.
allowed_modes *("call"|"sms"|"email")[]Channels this contact may be reached on.
timezonestringIANA timezone, e.g. Pacific/Auckland. Used for local calling hours.
country"NZ" | "AU" | "US"Contact's country.
custom_fieldsobjectString key-value pairs carried through to results.

Errors

Errors are JSON: { "error": "<message>" }.

FieldTypeDescription
401unauthorizedMissing, invalid, or revoked API key.
404not foundResource doesn't exist or belongs to another account.
422validationBody failed validation — including any contact-create without a consent attestation.
429rate limitedMore 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