Campaigns API
A campaign calls the contacts you enroll or link, from one of your phone numbers and with its agent, following its steps inside its calling hours and caps.
POST /v1/campaigns → POST /v1/campaigns/:id/contacts/import → POST /v1/campaigns/:id/startScopes campaigns:readcampaigns:write32 endpointsShow
A campaign end to end
Pick the number
GET /v1/channels?type=voice&status=connectedandGET /v1/channels/:id: a phone number with your agent deployed on it. To put the agent there, use PUT /v1/channels/:id/agent.Create the campaign
POST /v1/campaigns with
outbound_channel_id,business_hours, apurpose(the steps the dashboard builds: the agent calls, the AI reads each answered call, success by the purpose's question; addagent_idfor another agent than the number's) andapi_import_enabled: true. To write the steps yourself, use PUT /v1/campaigns/:id/workflow.Load the contacts
While it is a draft: POST /v1/campaigns/:id/contacts/import (up to 500 rows), POST /v1/campaigns/:id/enrollments for one contact, or PUT /v1/campaigns/:id/contact-lists/:list_id to link a list. They wait for the start.
Start it
POST /v1/campaigns/:id/start. It runs the dashboard's start checks; a
400lists what to fix indetails.issues.Read the results
GET /v1/campaigns/:id/enrollmentsfor each contact's status and result, GET /v1/campaigns/:id/enrollments/:enrollment_id for one contact's step, next call and latest calls, GET /v1/scheduled-calls for the calls to come, GET /v1/campaigns/:id/analytics for the totals and rates,GET /v1/calls?campaign_id=…for transcripts, or a webhook forcall.completed.Act on one contact
PATCH /v1/campaigns/:id/enrollments/:enrollment_id sets its
lead_responseor stage, POST …/requeue calls it again from the first step and POST …/remove stops calling it. POST /v1/scheduled-calls/:id/cancel cancels one coming call; a call in progress ends with POST /v1/calls/:id/terminate.
A/B tests, stages and the inbound number
An A/B test calls a campaign's new contacts with 2 or 3 agents or voices and compares the results. PUT /v1/campaigns/:id/variants saves the whole test, each variant with its share of new contacts; a contact keeps its variant for every call, retry and callback. GET /v1/campaigns/:id/variants reads it, with each variant's results when the key also holds calls:read. POST …/promote ends the test in a variant's favor, and POST …/use-live-version moves a running variant onto its agent's newest deployed version.
Stages are where each contact stands in the campaign's pipeline. GET /v1/campaigns/:id/stages gives their ids for an enrollment's stage_id; they are added, changed, put in order and deleted here, and every pipeline keeps at least one success and one failure end stage. PUT /v1/campaigns/:id/inbound-channel chooses the phone number whose callers join the campaign while it is active.
/v1/campaignsList campaigns
Lists campaigns, newest first, archived ones included unless you filter by status. completed_enrollments counts every enrollment that finished (completed, failed, exhausted, rejected or removed); goal_reached_count counts the completed ones whose lead response is interested or appointment_booked.
campaigns:readQuery parameters
limitinteger · 1–100Results per page. Default: 20.
offsetinteger · at least 0Results to skip. Default: 0.
statusdraft | active | paused | completed | archivedOnly campaigns in this status.
typeoutbound | inboundOnly campaigns of this type.
Errors
400validation_errorA query parameter is invalid (details.errors lists each one).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl "https://www.nutalk.ai/v1/campaigns?status=active&limit=10" \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": [
{
"id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"name": "Summer Sale Campaign",
"description": "Outreach for summer promotion",
"type": "outbound",
"status": "draft",
"total_enrollments": 0,
"active_enrollments": 0,
"completed_enrollments": 0,
"goal_reached_count": 0,
"business_hours": {
"enabled": true,
"timezone": "America/Los_Angeles",
"schedule": {
"monday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
},
"tuesday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
},
"wednesday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
}
},
"holidays": []
},
"rate_limits": {
"max_concurrent_calls": 5,
"max_calls_per_hour": 50,
"max_calls_per_lead_per_day": 3,
"min_minutes_between_attempts": 30
},
"created_at": "2026-10-02T02:26:06.132516+00:00",
"updated_at": "2026-10-02T02:26:06.132516+00:00"
}
],
"pagination": {
"total": 1,
"limit": 10,
"offset": 0,
"has_more": false
}
}/v1/campaignsCreate a campaign
Creates a draft campaign. With purpose or agent_id (and outbound_channel_id, which they need), its steps are the ones the dashboard builds once the calling agent is chosen: call the contact from the outbound channel, check whether the call was answered, have the AI read the call, and end it as a success or not by the purpose's question (an unanswered call ends as "Didn't answer"). Without either, the older default: call, then mark the enrollment interested when the call was answered or not_interested when it was not. Calls retry per the campaign's retry settings (retries) before the outcome is taken.
campaigns:writeTakes an Idempotency-KeyRead or replace the steps later with GET and PUT /v1/campaigns/{id}/workflow: another preset, or a whole workflow (several numbers, waits, WhatsApp templates, CRM steps, switching agents).
rate_limits.min_minutes_between_attempts is stored and returned, and it is not applied: the campaign engine never reads it. How long a campaign waits before it calls the same contact again is set by its steps and its retry settings (retries). The cap that limits calls to one contact is max_calls_per_lead_per_day.
A campaign skips contacts already enrolled in it unless deduplication says otherwise. Enrollments to a draft or paused campaign answer 409 unless api_import_enabled is true: then contacts wait (status: enrolled) and start when the campaign does. Linking a contact list (PUT /v1/campaigns/{id}/contact-lists/{list_id}) holds its members the same way.
Request body
namestring · 1–200 charactersrequiredCampaign name.
descriptionstring · up to 1,000What the campaign is for.
typeoutbound | inboundoutbound calls the contacts you enroll; inbound handles leads that arrive from integrations. Default: outbound.
outbound_channel_iduuidThe voice channel (phone number) the campaign calls from; it must be one of your channels with a deployed agent. Required before an outbound campaign can start.
business_hoursobjectWhen the campaign may call or message. Days not listed are closed. Omitted on create: Monday to Friday 09:00-18:00 (Friday until 17:00), America/Monterrey.
business_hours.timezonestringrequiredIANA timezone the hours are in, e.g. America/Mexico_City.
business_hours.scheduleobjectOpen days keyed by weekday (monday, tuesday, wednesday, thursday, friday, saturday, sunday), each {start, end}. days is accepted as an older name.
business_hours.holidaysstring[]Dates (YYYY-MM-DD, in the campaign timezone) the campaign does not call.
business_hours.enabledbooleanfalse keeps the schedule but lets the campaign call at any hour. Default: true.
rate_limitsobjectCaps. Omitted: 5 simultaneous calls, 50 calls per hour and 3 calls per contact per day (on a plan that allows fewer simultaneous or hourly calls, the plan's numbers). Sent: max_concurrent_calls is stored with the 3 calls per contact per day and the 50 calls per hour are not applied, so leave it out unless you want exactly that. Either way the campaign is also stored with min_minutes_between_attempts: 30, which is not applied (see the notes).
rate_limits.max_concurrent_callsinteger · 1–100Simultaneous calls (also capped by your plan).
purposegeneral | lead_qualification | appointment_booking | appointment_confirmation | payment_reminder | reactivation | customer_surveyWhat counts as success: the AI reads each answered call and the campaign ends it as a success when the answer to the purpose's question is yes. general (Did the person show clear interest in continuing?); lead_qualification (Did the person show clear interest in continuing?); appointment_booking (Was an appointment booked with a confirmed date and time?); appointment_confirmation (Did the person confirm the appointment?); payment_reminder (Did the person commit to paying (with a date if given)?); reactivation (Did the person show clear interest in continuing?); customer_survey (Did the person answer the survey questions?). general is what the dashboard builds.
agent_iduuidThe agent that makes the calls, named on the call step: one of yours, not archived, with a deployed version. Without it the agent deployed on outbound_channel_id talks, as in the dashboard.
step_names_languageen | esThe language the steps are named in on the dashboard. Default: en.
api_import_enabledbooleantrue: contacts you enroll or import while the campaign is a draft or paused wait (status: enrolled) and are called once it starts, so you can load them before the start; false (the default): those requests answer 409 until the campaign is active. The dashboard calls it API import.
deduplicationskip | re_enroll | update_onlyWhat enrolling someone already in the campaign does: skip (the default) leaves them as they are; re_enroll starts them over at the first step; update_only updates their details and keeps their progress.
retriesobjectCalling again a contact who didn't answer, got voicemail, was busy or whose call failed (Settings → Retries, simple mode). Sent, it replaces the retry settings: a field you leave out keeps its stored value, and retries set in the dashboard's advanced mode become simple ones. Omitted on create: 2 retries (3 calls in all), the first an hour after the call and the next twice as late, as a campaign made in the dashboard starts.
retries.enabledbooleanrequiredfalse: each contact gets one call.
retries.max_retriesinteger · 1–10Calls after the first one.
retries.delay_minutesinteger · 5–1,440Minutes before the first retry: one of 5, 15, 30, 60, 120, 240, 1440.
retries.exponential_backoffbooleantrue: each next retry waits twice as long as the one before.
callbacks_enabledbooleantrue: when someone says "call me later", the agent asks when, books it, and the campaign calls back at that time (pending retries wait for the callback). false (the default): such requests are not scheduled.
max_call_secondsinteger | null · 60–7,200The longest one call may last, in whole minutes written as seconds (60 to 7200, a multiple of 60): the carrier ends the call there. null (the default): no limit. Calls from an AiBox line or Zoom Phone don't take it.
Show 19 more fields
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).400validation_erroroutbound_channel_id is not one of your channels (with purpose or agent_id, it is required and must be one of your voice channels), agent_id is not one of your agents, is archived or has no deployed version, or business_hours is invalid (unknown timezone, a time not in HH:MM, no open day).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Citas de octubre",
"description": "Call October'\''s leads to book a first appointment",
"type": "outbound",
"outbound_channel_id": "9f19ef32-eaef-452c-bebc-14d9759cdb64",
"purpose": "appointment_booking",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"step_names_language": "es",
"business_hours": {
"timezone": "America/Monterrey",
"schedule": {
"monday": {
"start": "10:00",
"end": "18:00"
},
"tuesday": {
"start": "10:00",
"end": "18:00"
},
"wednesday": {
"start": "10:00",
"end": "18:00"
}
}
},
"api_import_enabled": true,
"retries": {
"enabled": true,
"max_retries": 2,
"delay_minutes": 120
}
}'{
"success": true,
"data": {
"id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"name": "Summer Sale Campaign",
"description": "Outreach for summer promotion",
"type": "outbound",
"status": "draft",
"total_enrollments": 0,
"active_enrollments": 0,
"completed_enrollments": 0,
"goal_reached_count": 0,
"business_hours": {
"enabled": true,
"timezone": "America/Los_Angeles",
"schedule": {
"monday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
},
"tuesday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
},
"wednesday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
}
},
"holidays": []
},
"rate_limits": {
"max_concurrent_calls": 5,
"max_calls_per_hour": 50,
"max_calls_per_lead_per_day": 3,
"min_minutes_between_attempts": 30
},
"created_at": "2026-10-02T02:26:06.132516+00:00",
"updated_at": "2026-10-02T02:26:06.132516+00:00",
"outbound_channel_id": "9f19ef32-eaef-452c-bebc-14d9759cdb64",
"api_import_enabled": false,
"deduplication": "skip",
"callbacks_enabled": false,
"max_call_seconds": null,
"retries": {
"enabled": true,
"mode": "simple",
"max_retries": 2,
"delay_minutes": 60,
"exponential_backoff": true
}
}
}/v1/campaigns/:idGet a campaign
Returns one campaign with its counters, settings (calling hours, caps, API import, duplicates rule, retries, callbacks, longest call), outbound channel, the contact lists linked to it (contact_lists, oldest link first; is_default marks the one its imports and analysis fields go to), default contact list and inbound webhook URL. retries.mode is advanced when the retries were set in the dashboard's advanced mode (time windows, stopping rules); only enabled is shown then.
campaigns:readwebhook_url receives leads from forms and ad platforms for this campaign; each request must carry the campaign's webhook secret, shown in the dashboard (campaign → Integrations). To add leads from your own code, use POST /v1/campaigns/{id}/enrollments or /contacts/import.
Path parameters
iduuidrequiredThe campaign id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"name": "Summer Sale Campaign",
"description": "Outreach for summer promotion",
"type": "outbound",
"status": "draft",
"total_enrollments": 0,
"active_enrollments": 0,
"completed_enrollments": 0,
"goal_reached_count": 0,
"business_hours": {
"enabled": true,
"timezone": "America/Los_Angeles",
"schedule": {
"monday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
},
"tuesday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
},
"wednesday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
}
},
"holidays": []
},
"rate_limits": {
"max_concurrent_calls": 5,
"max_calls_per_hour": 50,
"max_calls_per_lead_per_day": 3,
"min_minutes_between_attempts": 30
},
"created_at": "2026-10-02T02:26:06.132516+00:00",
"updated_at": "2026-10-02T02:26:06.132516+00:00",
"webhook_url": "https://bjhfvncvsmipuuijvkno.functions.supabase.co/campaign-webhook/f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"outbound_channel_id": "9f19ef32-eaef-452c-bebc-14d9759cdb64",
"default_contact_list_id": "d2406224-509f-4177-9a5a-b50bee1e7590",
"api_import_enabled": false,
"deduplication": "skip",
"callbacks_enabled": false,
"max_call_seconds": null,
"retries": {
"enabled": true,
"mode": "simple",
"max_retries": 2,
"delay_minutes": 60,
"exponential_backoff": true
},
"contact_lists": [
{
"id": "d2406224-509f-4177-9a5a-b50bee1e7590",
"name": "Leads de octubre",
"description": null,
"contact_count": 240,
"is_default": true,
"linked_at": "2026-10-23T19:12:44.120000+00:00"
}
]
}
}/v1/campaigns/:idUpdate a campaign
Changes the fields you send. status runs the lifecycle steps instead of a bare write: paused stops an active campaign like POST /pause, archived archives a campaign that is not active like DELETE, and the current status changes nothing (so you can send back what GET returned); activate with POST /start.
campaigns:writeCan wait for approvalA campaign created with outbound_channel_id keeps that channel on its call step: changing outbound_channel_id later changes the campaign default, not that step. PUT /v1/campaigns/{id}/workflow with a preset moves the calls to its channel_id.
rate_limits.min_minutes_between_attempts is stored and returned, and it is not applied: the campaign engine never reads it. How long a campaign waits before it calls the same contact again is set by its steps and its retry settings (retries). The cap that limits calls to one contact is max_calls_per_lead_per_day.
Path parameters
iduuidrequiredThe campaign id.
Request body
namestring · 1–200 charactersNew name.
descriptionstring · up to 1,000New description; an empty string clears it.
statusdraft | active | paused | completed | archivedpaused (from active) or archived (not while active). active, draft and completed answer 400.
outbound_channel_iduuidAnother of your voice channels.
business_hoursobjectNew calling hours (replaces the stored ones).
business_hours.timezonestringrequiredIANA timezone the hours are in, e.g. America/Mexico_City.
business_hours.scheduleobjectOpen days keyed by weekday (monday, tuesday, wednesday, thursday, friday, saturday, sunday), each {start, end}. days is accepted as an older name.
business_hours.holidaysstring[]Dates (YYYY-MM-DD, in the campaign timezone) the campaign does not call.
business_hours.enabledbooleanfalse keeps the schedule but lets the campaign call at any hour. Default: true.
rate_limitsobject | nullCaps. What you send replaces the campaign's speed caps (simultaneous, hourly and daily calls and messages): send every one you want to keep, since one left out is no longer applied. The cap on calls to the same contact per day is never dropped that way: it stays as stored unless you send max_calls_per_lead_per_day. null removes the speed caps and keeps that one. A stored min_minutes_between_attempts is kept too, and is not applied (see the notes). Checked against your plan.
rate_limits.max_concurrent_callsinteger · 1–100Simultaneous calls.
rate_limits.max_calls_per_hourinteger · at least 0Calls per hour.
rate_limits.max_calls_per_dayinteger · at least 0Calls per day.
rate_limits.max_calls_per_lead_per_dayinteger · at least 0Calls to the same contact per day.
rate_limits.max_messages_per_hourinteger · at least 0WhatsApp messages per hour.
rate_limits.max_messages_per_dayinteger · at least 0WhatsApp messages per day.
rate_limits.max_concurrent_messagesinteger · at least 1Simultaneous WhatsApp sends.
api_import_enabledbooleantrue: contacts you enroll or import while the campaign is a draft or paused wait (status: enrolled) and are called once it starts, so you can load them before the start; false (the default): those requests answer 409 until the campaign is active. The dashboard calls it API import.
deduplicationskip | re_enroll | update_onlyWhat enrolling someone already in the campaign does: skip (the default) leaves them as they are; re_enroll starts them over at the first step; update_only updates their details and keeps their progress.
retriesobjectCalling again a contact who didn't answer, got voicemail, was busy or whose call failed (Settings → Retries, simple mode). Sent, it replaces the retry settings: a field you leave out keeps its stored value, and retries set in the dashboard's advanced mode become simple ones. Omitted on create: 2 retries (3 calls in all), the first an hour after the call and the next twice as late, as a campaign made in the dashboard starts.
retries.enabledbooleanrequiredfalse: each contact gets one call.
retries.max_retriesinteger · 1–10Calls after the first one.
retries.delay_minutesinteger · 5–1,440Minutes before the first retry: one of 5, 15, 30, 60, 120, 240, 1440.
retries.exponential_backoffbooleantrue: each next retry waits twice as long as the one before.
callbacks_enabledbooleantrue: when someone says "call me later", the agent asks when, books it, and the campaign calls back at that time (pending retries wait for the callback). false (the default): such requests are not scheduled.
max_call_secondsinteger | null · 60–7,200The longest one call may last, in whole minutes written as seconds (60 to 7200, a multiple of 60): the carrier ends the call there. null (the default): no limit. Calls from an AiBox line or Zoom Phone don't take it.
Show 22 more fields
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).400validation_errorNo field to change was sent.400validation_errorA status the campaign cannot move to (activate with POST /start; pause only an active campaign; archive only one that is not active), a channel that is not yours, invalid business_hours, or retries turned on with no max_retries from 1 to 10.400validation_errorrate_limits exceed your plan (details.validation_errors, details.tenant_limits).404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).409validation_errorThe status changed between this request's read and its write (a pause, start or archive landed first). Read the campaign again.409validation_errorOther requests kept changing the campaign's rate_limits while this one tried to write them. An overlapping change is merged for you (the cap on calls per contact per day is taken as it is when the write happens); this answer comes only after several tries. Read the campaign again and retry.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X PATCH https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rate_limits": {
"max_concurrent_calls": 3,
"max_calls_per_hour": 40,
"max_calls_per_lead_per_day": 2
}
}'{
"success": true,
"data": {
"id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"name": "Summer Sale Campaign",
"description": "Outreach for summer promotion",
"type": "outbound",
"status": "draft",
"total_enrollments": 0,
"active_enrollments": 0,
"completed_enrollments": 0,
"goal_reached_count": 0,
"business_hours": {
"enabled": true,
"timezone": "America/Los_Angeles",
"schedule": {
"monday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
},
"tuesday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
},
"wednesday": {
"enabled": true,
"start": "10:00",
"end": "18:00"
}
},
"holidays": []
},
"rate_limits": {
"max_calls_per_lead_per_day": 2,
"min_minutes_between_attempts": 30,
"max_concurrent_calls": 3,
"max_calls_per_hour": 40
},
"created_at": "2026-10-02T02:26:06.132516+00:00",
"updated_at": "2026-10-02T02:28:30.168271+00:00"
}
}/v1/campaigns/:idArchive a campaign
Archives a campaign that is not active (pause it first). An archived campaign stays readable and appears in the list; it cannot start again.
campaigns:writeWaits for approvalPath parameters
iduuidrequiredThe campaign id.
Unless its key was set not to wait, nothing is removed yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).400validation_errorThe campaign is active: pause it first.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X DELETE https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"deleted": true
}
}/v1/campaigns/:id/workflowGet a campaign's steps
Returns the campaign (id, name, status) with its workflow: its steps (nodes, each with data.type and the step's settings) and how they connect (edges; a condition's Yes branch has sourceHandle: "true" and its No branch "false"), and the revision a save of steps built from them names in expected_revision. Webhook steps' header values and URL credentials come masked.
campaigns:readPath parameters
iduuidrequiredThe campaign id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/workflow \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"name": "Citas de octubre",
"status": "draft",
"workflow": {
"nodes": [
{
"id": "start",
"type": "trigger",
"position": {
"x": 300,
"y": 0
},
"data": {
"type": "trigger",
"trigger_type": "manual",
"label": "Start"
}
},
{
"id": "call",
"type": "action",
"position": {
"x": 300,
"y": 130
},
"data": {
"type": "action",
"action_type": "voice_call",
"label": "Booking call",
"channel_id": "9f19ef32-eaef-452c-bebc-14d9759cdb64"
}
},
{
"id": "answered",
"type": "condition",
"position": {
"x": 300,
"y": 260
},
"data": {
"type": "condition",
"condition_type": "call_outcome",
"label": "Answered?",
"outcomes": [
"answered"
]
}
},
{
"id": "analyze",
"type": "action",
"position": {
"x": 120,
"y": 390
},
"data": {
"type": "action",
"action_type": "analyze_conversation",
"label": "Read the call",
"analysis_source": "auto"
}
},
{
"id": "decision",
"type": "condition",
"position": {
"x": 120,
"y": 520
},
"data": {
"type": "condition",
"condition_type": "analysis_result",
"label": "Appointment booked?",
"analysis_node_id": "analyze",
"analysis_field": "appointment_booked",
"analysis_operator": "is_true"
}
},
{
"id": "end-yes",
"type": "terminal",
"position": {
"x": 0,
"y": 650
},
"data": {
"type": "terminal",
"terminal_type": "success",
"label": "Appointment booked",
"lead_response": "appointment_booked"
}
},
{
"id": "end-no",
"type": "terminal",
"position": {
"x": 240,
"y": 650
},
"data": {
"type": "terminal",
"terminal_type": "failure",
"label": "Not booked",
"lead_response": "not_interested"
}
},
{
"id": "end-not-reached",
"type": "terminal",
"position": {
"x": 500,
"y": 390
},
"data": {
"type": "terminal",
"terminal_type": "failure",
"label": "Didn't answer",
"lead_response": "unknown"
}
}
],
"edges": [
{
"id": "e-1",
"source": "start",
"target": "call"
},
{
"id": "e-2",
"source": "call",
"target": "answered"
},
{
"id": "e-3",
"source": "answered",
"target": "analyze",
"sourceHandle": "true"
},
{
"id": "e-4",
"source": "decision",
"target": "end-yes",
"sourceHandle": "true"
},
{
"id": "e-5",
"source": "decision",
"target": "end-no",
"sourceHandle": "false"
},
{
"id": "e-6",
"source": "analyze",
"target": "decision"
},
{
"id": "e-7",
"source": "answered",
"target": "end-not-reached",
"sourceHandle": "false"
}
]
},
"revision": 3
}
}/v1/campaigns/:id/workflowReplace a campaign's steps
Replaces the campaign's steps, with the checks of the dashboard's own save: the workflow rules, every number and agent a step names being yours, WhatsApp steps on an Official WhatsApp number. Send preset for the steps the dashboard builds for a purpose (call from channel_id, check whether the call was answered, the AI reads the call, success by the purpose's question), or workflow for any steps the dashboard's builder makes (several numbers, waits, WhatsApp templates, CRM steps, switching agents). On an active campaign the new steps apply to each contact's next step.
campaigns:writeCan wait for approvalA call step has no opening or goal of its own: each call speaks with its agent's instructions and goals. Sent ones are dropped, as in the dashboard.
Webhook steps' header values and URL credentials (a sign-in, or a query value such as token) read masked (••••••, redacted@). Sent back as read they keep their stored values, as long as the step's url calls the same address: the same url apart from its credentials. A step pointed at another address keeps none of them: send its real header values and URL credentials (a header set to null is removed).
Path parameters
iduuidrequiredThe campaign id.
Request body
presetobjectThe dashboard's steps for a purpose. Send this or workflow.
preset.purposegeneral | lead_qualification | appointment_booking | appointment_confirmation | payment_reminder | reactivation | customer_surveyWhat counts as success: the AI reads each answered call and the campaign ends it as a success when the answer to the purpose's question is yes. general (Did the person show clear interest in continuing?); lead_qualification (Did the person show clear interest in continuing?); appointment_booking (Was an appointment booked with a confirmed date and time?); appointment_confirmation (Did the person confirm the appointment?); payment_reminder (Did the person commit to paying (with a date if given)?); reactivation (Did the person show clear interest in continuing?); customer_survey (Did the person answer the survey questions?). general is what the dashboard builds.
preset.agent_iduuidThe agent that makes the calls, named on the call step: one of yours, not archived, with a deployed version. Without it the agent deployed on channel_id talks, as in the dashboard.
preset.step_names_languageen | esThe language the steps are named in on the dashboard. Default: en.
preset.channel_iduuidThe voice channel the calls go out from; the campaign's outbound_channel_id when left out. It also becomes the campaign's outbound_channel_id.
workflowobjectA whole workflow, as GET returns it. A step without position is placed under the one before. Send this or preset.
workflow.nodesobject[] · 1–200 itemsrequiredThe steps: id, type (trigger, action, condition or terminal), data and an optional position.
workflow.edgesobject[] · 0–400 itemsrequiredThe connections: id, source, target and, from a condition, sourceHandle ("true" or "false").
expected_revisioninteger · at least 1The revision GET answered: the save is refused (409) when the steps changed since. Left out, the steps are replaced whatever changed.
Show 5 more fields
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).400validation_errorThe steps break a workflow rule (details.validation_errors), name a number or agent that isn't yours, or send WhatsApp messages from a number that isn't an Official WhatsApp one; a preset's number isn't one of your voice channels, or its agent is archived or has no deployed version.404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).409workflow_changedThe steps changed since expected_revision (details.revision is the current one): read them again and rebuild.409steps_in_useContacts are still on steps this change removes (details.steps: each step's node_id, label and contacts). Keep those steps' ids, or wait until the contacts move on.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X PUT https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/workflow \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"preset": {
"purpose": "appointment_booking",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"step_names_language": "es"
},
"expected_revision": 3
}'revision, the campaign's outbound_channel_id and the rules' non-blocking warnings.{
"success": true,
"data": {
"workflow": {
"nodes": [
{
"id": "start",
"type": "trigger",
"position": {
"x": 300,
"y": 0
},
"data": {
"type": "trigger",
"trigger_type": "manual",
"label": "Start"
}
},
{
"id": "call",
"type": "action",
"position": {
"x": 300,
"y": 130
},
"data": {
"type": "action",
"action_type": "voice_call",
"label": "Booking call",
"channel_id": "9f19ef32-eaef-452c-bebc-14d9759cdb64"
}
},
{
"id": "answered",
"type": "condition",
"position": {
"x": 300,
"y": 260
},
"data": {
"type": "condition",
"condition_type": "call_outcome",
"label": "Answered?",
"outcomes": [
"answered"
]
}
},
{
"id": "analyze",
"type": "action",
"position": {
"x": 120,
"y": 390
},
"data": {
"type": "action",
"action_type": "analyze_conversation",
"label": "Read the call",
"analysis_source": "auto"
}
},
{
"id": "decision",
"type": "condition",
"position": {
"x": 120,
"y": 520
},
"data": {
"type": "condition",
"condition_type": "analysis_result",
"label": "Appointment booked?",
"analysis_node_id": "analyze",
"analysis_field": "appointment_booked",
"analysis_operator": "is_true"
}
},
{
"id": "end-yes",
"type": "terminal",
"position": {
"x": 0,
"y": 650
},
"data": {
"type": "terminal",
"terminal_type": "success",
"label": "Appointment booked",
"lead_response": "appointment_booked"
}
},
{
"id": "end-no",
"type": "terminal",
"position": {
"x": 240,
"y": 650
},
"data": {
"type": "terminal",
"terminal_type": "failure",
"label": "Not booked",
"lead_response": "not_interested"
}
},
{
"id": "end-not-reached",
"type": "terminal",
"position": {
"x": 500,
"y": 390
},
"data": {
"type": "terminal",
"terminal_type": "failure",
"label": "Didn't answer",
"lead_response": "unknown"
}
}
],
"edges": [
{
"id": "e-1",
"source": "start",
"target": "call"
},
{
"id": "e-2",
"source": "call",
"target": "answered"
},
{
"id": "e-3",
"source": "answered",
"target": "analyze",
"sourceHandle": "true"
},
{
"id": "e-4",
"source": "decision",
"target": "end-yes",
"sourceHandle": "true"
},
{
"id": "e-5",
"source": "decision",
"target": "end-no",
"sourceHandle": "false"
},
{
"id": "e-6",
"source": "analyze",
"target": "decision"
},
{
"id": "e-7",
"source": "answered",
"target": "end-not-reached",
"sourceHandle": "false"
}
]
},
"revision": 4,
"outbound_channel_id": "9f19ef32-eaef-452c-bebc-14d9759cdb64",
"warnings": []
}
}/v1/campaigns/:id/startStart a campaign
Activates a draft or paused campaign: contacts waiting in it are queued and dialing begins within the calling hours. Runs the dashboard's start checks first: an outbound campaign needs an outbound channel of your workspace, its rate_limits must fit your plan, its workflow must pass the workflow rules, and what the steps use must be ready (each call step's number has a deployed agent, WhatsApp steps use a Meta WhatsApp number, CRM steps have their connection).
campaigns:writeCan wait for approvalThe checks are the dashboard's own, so a campaign that starts there starts here. Fix what details.issues names (put the agent on the number with PUT /v1/channels/{id}/agent, deploy it, or connect the CRM in the dashboard), then start again.
Path parameters
iduuidrequiredThe campaign id.
An active campaign calls its enrolled contacts and spends credits. When an AI agent drives the API for you, have it confirm the channel, the agent, the calling hours and the contact count before it starts the campaign.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).400validation_errorThe campaign is already active, completed or archived; an outbound campaign has no outbound channel; or the channel is not one of yours.400validation_errorrate_limits exceed your plan (details.validation_errors, details.tenant_limits).400validation_errorThe campaign is not ready to start: details.issues lists each problem as {code, message} (for example number_no_agent, call_agent_unpublished, whatsapp_number_not_meta, hubspot_not_connected, or workflow_rule for a rule of the workflow itself), and details.validation_errors repeats the messages.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/start \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"name": "Summer Sale Campaign",
"status": "active",
"type": "outbound",
"updated_at": "2026-10-02T02:28:30.318485+00:00",
"enrollments_activated": 12
},
"message": "Campaign started successfully"
}/v1/campaigns/:id/pausePause a campaign
Pauses an active campaign: queued steps are cancelled, runnable contacts are set aside and calls that have not been dialed are cancelled. A call already in progress finishes. Start it again with POST /start.
campaigns:writePath parameters
iduuidrequiredThe campaign id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).400validation_errorThe campaign is not active (or already paused).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/pause \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"name": "Summer Sale Campaign",
"status": "paused",
"type": "outbound",
"updated_at": "2026-10-02T03:00:00.000000+00:00",
"cancel_event_sent": true,
"enrollments_deactivated": 9,
"voice_jobs_cancelled": 2
},
"message": "Campaign paused successfully"
}/v1/campaigns/:id/enrollmentsList a campaign's enrollments
Lists the contacts enrolled in a campaign, newest first, with their status, lead response and last call outcome. enrolled means waiting for the campaign to start. cooldown_until is set when the workspace's "don't call again for X days" rule holds the number: it is called after that moment, or, when the workspace leaves such numbers out, the enrollment is removed.
campaigns:readPath parameters
iduuidrequiredThe campaign id.
Query parameters
limitinteger · 1–100Results per page. Default: 20.
offsetinteger · at least 0Results to skip. Default: 0.
statusenrolled | queued | in_progress | retry_scheduled | callback_scheduled | paused | completed | failed | exhausted | rejected | removedOnly enrollments in this status.
lead_responseunknown | interested | not_interested | callback_requested | appointment_booked | do_not_contactOnly enrollments with this lead response.
Errors
400validation_errorA query parameter is invalid (details.errors lists each one).404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl "https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/enrollments?status=completed&limit=50" \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": [
{
"id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"contact_id": "9595b646-ab97-4485-b1b8-e8b58dfcdd19",
"phone_number": "+525512345678",
"status": "completed",
"lead_response": "interested",
"final_call_outcome": "answered",
"failure_reason": null,
"failure_detail": null,
"current_retry_attempt": 0,
"stage_id": null,
"source": "api",
"source_metadata": {
"api_key_id": "7b7a0ce4-0000-4000-8000-000000000001",
"received_at": "2026-10-02T02:28:30.401Z"
},
"lead_data": {
"name": "Ana López",
"product": "Plan Pro"
},
"enrolled_at": "2026-10-02T02:28:30.521857+00:00",
"next_action_at": null,
"cooldown_until": null,
"completed_at": "2026-10-02T02:41:12.000000+00:00",
"last_activity_at": "2026-10-02T02:41:12.000000+00:00"
}
],
"pagination": {
"total": 1,
"limit": 50,
"offset": 0,
"has_more": false
}
}/v1/campaigns/:id/enrollmentsEnroll one contact
Finds the contact by phone number (or creates it) and enrolls it. On an active campaign the first call happens within the calling hours, usually within seconds. With API import enabled on the campaign, a draft or paused campaign holds the contact (status: enrolled) until it starts.
campaigns:writeTakes an Idempotency-KeyPath parameters
iduuidrequiredThe campaign id.
Request body
phone_numberstring · 5–30 charactersrequiredNumber with the country code, e.g. +5215512345678 (E.164 recommended).
namestring · up to 200Contact name.
emailstring · up to 320Contact email.
lead_dataobjectAny fields the agent should know about this lead (product, last purchase, …).
source_metadataobjectWhere the lead came from; stored with the enrollment.
Show 1 more field
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).400validation_errorphone_number is not a valid number.404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).409validation_errorAlready enrolled (details.enrollment_id), the contact opted out (details.reason: do_not_contact), or the campaign is not active and API import is off (details.campaign_status, details.api_import_enabled).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/enrollments \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+5215512345678",
"name": "Ana López",
"lead_data": {
"product": "Plan Pro"
}
}'status is queued (will be called) or enrolled (held until the campaign starts); action is created, or updated when the campaign's deduplication policy updated an existing enrollment, whose current status is answered. When someone talked with this number recently and the workspace's "don't call again for X days" rule is on, cooldown_until says until when it waits (queued), or status is removed when the workspace leaves such numbers out.{
"success": true,
"data": {
"enrollment_id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"contact_id": "9595b646-ab97-4485-b1b8-e8b58dfcdd19",
"action": "created",
"status": "queued",
"cooldown_until": null,
"campaign_status": "active"
}
}/v1/campaigns/:id/enrollments/:enrollment_idGet an enrollment
Returns one contact's place in a campaign, as the dashboard's enrollment drawer shows it: status, lead response and stage, the step it is on (current_step), its attempts, when its next call is and why (next_call), and its 10 latest calls (recent_calls; each call's transcript is on GET /v1/calls/{id}).
campaigns:readPath parameters
iduuidrequiredThe campaign id.
enrollment_iduuidrequiredThe enrollment id (id in the campaign's enrollments list).
Errors
404not_foundNo campaign or enrollment with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/enrollments/7593112e-56ab-4a08-858f-21035eb1bacc \ -H "Authorization: Bearer $NUTALK_API_KEY"
next_call.kind is in_progress (on a call now), retry, callback or queued, with at (when; null while on a call) and reason (the retry's or the callback's); next_call is null when no call is pending.{
"success": true,
"data": {
"id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_id": "9595b646-ab97-4485-b1b8-e8b58dfcdd19",
"phone_number": "+525512345678",
"status": "retry_scheduled",
"lead_response": "unknown",
"final_call_outcome": "no_answer",
"stage_id": null,
"current_node_id": "call-1",
"total_call_attempts": 1,
"max_total_attempts": 3,
"current_retry_attempt": 1,
"max_retry_attempts": 2,
"scheduled_retry_at": "2026-10-02T16:00:00+00:00",
"retry_reason": "no_answer",
"scheduled_callback_at": null,
"callback_reason": null,
"next_action_at": "2026-10-02T16:00:00+00:00",
"cooldown_until": null,
"source": "api",
"source_metadata": {
"api_key_id": "7b7a0ce4-0000-4000-8000-000000000001",
"received_at": "2026-10-02T14:58:30.401Z"
},
"lead_data": {
"name": "Ana López",
"product": "Plan Pro"
},
"enrolled_at": "2026-10-02T14:58:30.521857+00:00",
"last_activity_at": "2026-10-02T15:01:12.000000+00:00",
"completed_at": null,
"display_name": "Ana López",
"stage_name": null,
"current_step": {
"id": "call-1",
"label": "Call lead"
},
"next_call": {
"kind": "retry",
"at": "2026-10-02T16:00:00+00:00",
"reason": "no_answer"
},
"failure_reason": null,
"failure_detail": null,
"recent_calls": [
{
"id": "1ba11348-e440-44de-a5cd-5fe62f4d9df6",
"status": "no_answer",
"started_at": "2026-10-02T15:00:02.000000+00:00",
"duration_seconds": 0,
"call_outcome": "no_answer",
"failure_reason": "no_answer",
"failure_detail": "It rang 30 s and nobody picked up."
}
]
}
}/v1/campaigns/:id/enrollments/:enrollment_idSet an enrollment's lead response or stage
Sets the contact's lead response, its stage, or both, as a person does in the enrollment drawer. It places no call and stops none: call the contact again with POST /requeue, stop calling them with POST /remove. do_not_contact opts the person out of every campaign: their other waiting enrollments close, and nothing calls or messages them until the opt-out is lifted (PATCH /v1/contacts/{id} with do_not_contact: false).
campaigns:writeCan wait for approvalPath parameters
iduuidrequiredThe campaign id.
enrollment_iduuidrequiredThe enrollment id (id in the campaign's enrollments list).
Request body
lead_responseunknown | interested | not_interested | callback_requested | appointment_booked | do_not_contactThe contact's answer.
stage_iduuid | nullOne of the campaign's stages (its pipeline in the dashboard); null takes the contact out of its stage.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).400validation_errorNeither field is sent, or stage_id is not one of this campaign's stages.404not_foundNo campaign or enrollment with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X PATCH https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/enrollments/7593112e-56ab-4a08-858f-21035eb1bacc \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"lead_response": "interested"
}'{
"success": true,
"data": {
"id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_id": "9595b646-ab97-4485-b1b8-e8b58dfcdd19",
"phone_number": "+525512345678",
"status": "retry_scheduled",
"lead_response": "interested",
"final_call_outcome": "no_answer",
"stage_id": null,
"current_node_id": "call-1",
"total_call_attempts": 1,
"max_total_attempts": 3,
"current_retry_attempt": 1,
"max_retry_attempts": 2,
"scheduled_retry_at": "2026-10-02T16:00:00+00:00",
"retry_reason": "no_answer",
"scheduled_callback_at": null,
"callback_reason": null,
"next_action_at": "2026-10-02T16:00:00+00:00",
"cooldown_until": null,
"source": "api",
"source_metadata": {
"api_key_id": "7b7a0ce4-0000-4000-8000-000000000001",
"received_at": "2026-10-02T14:58:30.401Z"
},
"lead_data": {
"name": "Ana López",
"product": "Plan Pro"
},
"enrolled_at": "2026-10-02T14:58:30.521857+00:00",
"last_activity_at": "2026-10-02T15:01:12.000000+00:00",
"completed_at": null,
"display_name": "Ana López",
"stage_name": null,
"current_step": {
"id": "call-1",
"label": "Call lead"
},
"next_call": {
"kind": "retry",
"at": "2026-10-02T16:00:00+00:00",
"reason": "no_answer"
},
"failure_reason": null,
"failure_detail": null,
"recent_calls": [
{
"id": "1ba11348-e440-44de-a5cd-5fe62f4d9df6",
"status": "no_answer",
"started_at": "2026-10-02T15:00:02.000000+00:00",
"duration_seconds": 0,
"call_outcome": "no_answer",
"failure_reason": "no_answer",
"failure_detail": "It rang 30 s and nobody picked up."
}
]
}
}/v1/campaigns/:id/enrollments/:enrollment_id/requeueCall a contact again
Starts the contact over from the campaign's first step, as Call again does in the enrollment drawer: status queued, its retries, callbacks and last outcome cleared, its calls for the day counted from zero. An active campaign calls it within the calling hours; a draft or paused one keeps it queued until it starts.
campaigns:writeCan wait for approvalIt calls a real person and spends credits: confirm with the user first.
Path parameters
iduuidrequiredThe campaign id.
enrollment_iduuidrequiredThe enrollment id (id in the campaign's enrollments list).
On an active campaign the contact is called again within the calling hours, and the call spends credits. Have an AI agent confirm the person with you first.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
404not_foundNo campaign or enrollment with this id in your workspace (ids of other workspaces answer the same).409conflictThe contact is on a call right now (details.reason CONTACT_ON_CALL): try again when the call ends.409conflictThe person asked not to be contacted (details.reason do_not_contact). Lift the opt-out first, only when they asked for it.422campaign_not_readyThe campaign can't call anyone: details.blocking_conditions lists each problem as {code, message, action} (RATE_LIMIT_ZERO: rate_limits.max_calls_per_day is 0; NO_OUTBOUND_CHANNEL: no outbound_channel_id).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/enrollments/7593112e-56ab-4a08-858f-21035eb1bacc/requeue \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_id": "9595b646-ab97-4485-b1b8-e8b58dfcdd19",
"phone_number": "+525512345678",
"status": "queued",
"lead_response": "unknown",
"final_call_outcome": null,
"stage_id": null,
"current_node_id": null,
"total_call_attempts": 1,
"max_total_attempts": 3,
"current_retry_attempt": 0,
"max_retry_attempts": 2,
"scheduled_retry_at": null,
"retry_reason": null,
"scheduled_callback_at": null,
"callback_reason": null,
"next_action_at": "2026-10-02T15:10:00.000000+00:00",
"cooldown_until": null,
"source": "api",
"source_metadata": {
"api_key_id": "7b7a0ce4-0000-4000-8000-000000000001",
"received_at": "2026-10-02T14:58:30.401Z"
},
"lead_data": {
"name": "Ana López",
"product": "Plan Pro"
},
"enrolled_at": "2026-10-02T14:58:30.521857+00:00",
"last_activity_at": "2026-10-02T15:01:12.000000+00:00",
"completed_at": null,
"display_name": "Ana López",
"stage_name": null,
"current_step": null,
"next_call": {
"kind": "queued",
"at": "2026-10-02T15:10:00.000000+00:00",
"reason": null
},
"failure_reason": null,
"failure_detail": null,
"recent_calls": [
{
"id": "1ba11348-e440-44de-a5cd-5fe62f4d9df6",
"status": "no_answer",
"started_at": "2026-10-02T15:00:02.000000+00:00",
"duration_seconds": 0,
"call_outcome": "no_answer",
"failure_reason": "no_answer",
"failure_detail": "It rang 30 s and nobody picked up."
}
]
}
}/v1/campaigns/:id/enrollments/:enrollment_id/removeTake a contact out of a campaign
Stops the campaign calling or messaging the contact, as Remove does in the enrollment drawer: status removed, its pending callbacks cancelled. Its calls and results stay, and the contact stays in your contacts. Removing a removed contact changes nothing. To stop every campaign, set lead_response to do_not_contact instead.
campaigns:writePath parameters
iduuidrequiredThe campaign id.
enrollment_iduuidrequiredThe enrollment id (id in the campaign's enrollments list).
Errors
404not_foundNo campaign or enrollment with this id in your workspace (ids of other workspaces answer the same).409conflictThe contact is on a call right now (details.reason CONTACT_ON_CALL): try again when the call ends.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/enrollments/7593112e-56ab-4a08-858f-21035eb1bacc/remove \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_id": "9595b646-ab97-4485-b1b8-e8b58dfcdd19",
"phone_number": "+525512345678",
"status": "removed",
"lead_response": "unknown",
"final_call_outcome": "no_answer",
"stage_id": null,
"current_node_id": "call-1",
"total_call_attempts": 1,
"max_total_attempts": 3,
"current_retry_attempt": 1,
"max_retry_attempts": 2,
"scheduled_retry_at": "2026-10-02T16:00:00+00:00",
"retry_reason": "no_answer",
"scheduled_callback_at": null,
"callback_reason": null,
"next_action_at": "2026-10-02T16:00:00+00:00",
"cooldown_until": null,
"source": "api",
"source_metadata": {
"api_key_id": "7b7a0ce4-0000-4000-8000-000000000001",
"received_at": "2026-10-02T14:58:30.401Z"
},
"lead_data": {
"name": "Ana López",
"product": "Plan Pro"
},
"enrolled_at": "2026-10-02T14:58:30.521857+00:00",
"last_activity_at": "2026-10-02T15:01:12.000000+00:00",
"completed_at": "2026-10-02T15:20:00.000000+00:00",
"display_name": "Ana López",
"stage_name": null,
"current_step": {
"id": "call-1",
"label": "Call lead"
},
"next_call": null,
"failure_reason": null,
"failure_detail": null,
"recent_calls": [
{
"id": "1ba11348-e440-44de-a5cd-5fe62f4d9df6",
"status": "no_answer",
"started_at": "2026-10-02T15:00:02.000000+00:00",
"duration_seconds": 0,
"call_outcome": "no_answer",
"failure_reason": "no_answer",
"failure_detail": "It rang 30 s and nobody picked up."
}
]
}
}/v1/campaigns/:id/contacts/importImport contacts in bulk
Enrolls up to 500 contacts in one request and reports what happened to each row. Each row is validated (phone, duplicates in the request, already enrolled, opted out); the run is recorded in the campaign's import history.
campaigns:writeTakes an Idempotency-KeyImported contacts are also added to the campaign's default contact list, if it has one. When another campaign also uses that list, they go to a new list for this campaign alone instead, {campaign name} · contacts, which becomes its default list: contacts added to a list are enrolled in every draft, paused or active outbound campaign that uses it.
For a large file (up to 4 GiB, millions of rows), use POST /v1/contact-imports: the file goes straight to storage and is imported in the background, with progress to follow.
Path parameters
iduuidrequiredThe campaign id.
Request body
contactsobject[] · 1–500 itemsrequiredRows. Any other key with a text or number value is kept in the enrollment's lead data (custom fields).
contacts[].phone_numberstring · up to 40requiredNumber with the country code.
contacts[].namestring · up to 200Full name (display_name also accepted).
contacts[].first_namestring · up to 100First name.
contacts[].last_namestring · up to 100Last name.
contacts[].emailstring · up to 320Email; an invalid one is dropped.
contacts[].companystring · up to 200Company (fills the contact's company when empty).
contacts[].external_idstring · up to 200Your id for the contact, stored on it as crm_ids[source].
contacts[].notesstring · up to 2,000Notes for the agent.
contacts[].lead_dataobjectExtra fields for the agent.
contacts[].fieldsobjectThe contact's field values by key ({ "interes": "Casa" }, see /contact-fields), checked against each field's type before anything is written. Only existing fields; null or an empty text leaves a value as it is.
sourcestringOrigin label (lowercase letters, digits, - and _, up to 40), e.g. salesforce. Stored as the enrollments' source and the key of external_id. Platform labels (contact_list, csv, manual, webhook, campaign, copilot, hubspot*, kommo*, facebook_ads, google_ads, meta_lead_ads) are refused. Default: api.
Show 8 more fields
Errors
400validation_errorThe body is invalid; a row that breaks the schema (no phone_number, a field too long) fails the whole request, without a row index.404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).409validation_errorThe campaign is not active and API import is off, or it is completed or archived (details.campaign_status, details.api_import_enabled).422validation_errorA row's fields names an unknown or archived field, or a value doesn't fit its field (details.reason: contact_field_values_invalid, details.issues with each row, field and code). Nothing is imported.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/contacts/import \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"source": "my_crm",
"contacts": [
{
"phone_number": "+5215512345678",
"name": "Ana López",
"company": "Acme",
"external_id": "crm-1001"
},
{
"phone_number": "+14155550123",
"name": "Bob"
},
{
"phone_number": "12",
"name": "Invalid"
}
]
}'status: enrolled (queued to call), held (waits for the start), updated (deduplication policy update_only, or re_enroll for a person being called or with a callback booked), skipped (reason: already_enrolled), rejected (reason: missing_phone, invalid_phone, duplicate_in_request, do_not_contact, campaign_not_accepting, contact_error, enrollment_error).{
"success": true,
"data": {
"batch_id": "25ea2e90-8f89-4684-afc4-e1cef81ce0bc",
"campaign_status": "active",
"summary": {
"total": 3,
"enrolled": 2,
"held": 0,
"updated": 0,
"skipped": 0,
"rejected": 1
},
"rows": [
{
"index": 0,
"phone_number": "+5215512345678",
"status": "enrolled",
"contact_id": "9595b646-ab97-4485-b1b8-e8b58dfcdd19",
"enrollment_id": "7593112e-56ab-4a08-858f-21035eb1bacc"
},
{
"index": 1,
"phone_number": "+14155550123",
"status": "enrolled",
"contact_id": "eaa5bc4d-b54a-4cb2-bcf8-c1b523f716eb",
"enrollment_id": "f47c40af-a57c-48f0-9dfb-03cb51ce8cf0"
},
{
"index": 2,
"phone_number": "12",
"status": "rejected",
"reason": "invalid_phone",
"message": "Phone number must have 8 to 15 digits including the country code"
}
]
}
}/v1/campaigns/:id/contact-lists/:list_idLink a contact list
Makes a contact list's members the campaign's contacts, as the dashboard's Contacts step does (GET /v1/campaigns/{id} lists the linked ones). In an outbound campaign that is a draft or paused they wait for the start (members: held); in an active one they are queued and called within the calling hours (members: queued). Members added to the list later join too. The first list linked becomes the campaign's default list. Linking a list that is linked already changes nothing.
campaigns:writeCan wait for approvalNeeds contact_lists:read as well as campaigns:write. Unlinking needs campaigns:write only.
The first 1,000 members are enrolled at once; the rest of a bigger list in the background, within minutes. Contacts who opted out or are already in the campaign are skipped.
Path parameters
iduuidrequiredThe campaign id.
list_iduuidrequiredThe contact list id.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
403forbiddenThe key lacks contact_lists:read (details.required_scope): linking makes the list's members the campaign's enrollments, whose phone numbers campaigns:read reads.404not_foundNo campaign or contact list with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X PUT https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/contact-lists/d2406224-509f-4177-9a5a-b50bee1e7590 \ -H "Authorization: Bearer $NUTALK_API_KEY"
members says what happens to the list's members now: held, queued, or not_enrolled (an inbound, completed or archived campaign).{
"success": true,
"data": {
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"list_id": "d2406224-509f-4177-9a5a-b50bee1e7590",
"already_linked": false,
"is_default": true,
"campaign_status": "draft",
"members": "held"
}
}/v1/campaigns/:id/contact-lists/:list_idUnlink a contact list
Stops a contact list from feeding the campaign: its members' enrollments that are still waiting (enrolled, queued, scheduled for a retry or a callback, paused) are removed; the ones already called keep their results. A big list's are removed in the background, within minutes. The campaign's default list is cleared when it was this one.
campaigns:writeWaits for approvalPath parameters
iduuidrequiredThe campaign id.
list_iduuidrequiredThe contact list id.
Unless its key was set not to wait, nothing is removed yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).404not_foundThe list is not linked to the campaign.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X DELETE https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/contact-lists/d2406224-509f-4177-9a5a-b50bee1e7590 \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"list_id": "d2406224-509f-4177-9a5a-b50bee1e7590",
"unlinked": true,
"default_contact_list_id": null
}
}/v1/scheduled-callsList scheduled calls
Lists the workspace's coming campaign calls, as the dashboard's Scheduled calls page does: contacts queued, waiting for a retry or a callback, paused with a next step, or on a call now, the soonest first. Each row is one enrollment (its id); status is the call's (queued, in_progress, retry_scheduled or callback_scheduled), scheduled_at when it is due (null while on a call), and from_channel the campaign's outbound number.
campaigns:readQuery parameters
limitinteger · 1–100Results per page. Default: 20.
offsetinteger · at least 0Results to skip. Default: 0.
statusqueued | retry | callback | in_progress | pausedretry or callback: the contacts waiting for one. queued, in_progress or paused: the enrollment's status.
campaign_iduuidOnly this campaign's calls.
Errors
400validation_errorA query parameter is invalid (details.errors lists each one).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl "https://www.nutalk.ai/v1/scheduled-calls?status=retry&limit=50" \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": [
{
"id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"enrollment_id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"campaign_name": "Summer Sale Campaign",
"campaign_status": "active",
"contact_id": "9595b646-ab97-4485-b1b8-e8b58dfcdd19",
"contact_name": "Ana López",
"phone_number": "+525512345678",
"status": "retry_scheduled",
"enrollment_status": "retry_scheduled",
"scheduled_at": "2026-10-02T16:00:00+00:00",
"reason": "no_answer",
"attempts": 1,
"max_attempts": 3,
"current_retry_attempt": 1,
"max_retry_attempts": 2,
"lead_response": "unknown",
"final_call_outcome": "no_answer",
"from_channel": {
"id": "9f19ef32-eaef-452c-bebc-14d9759cdb64",
"name": "Sales line",
"phone_number": "+525598765432"
},
"enrolled_at": "2026-10-02T14:58:30.521857+00:00",
"last_activity_at": "2026-10-02T15:01:12.000000+00:00"
}
],
"pagination": {
"total": 1,
"limit": 50,
"offset": 0,
"has_more": false
}
}/v1/scheduled-calls/:id/cancelCancel a scheduled call
Cancels a contact's coming campaign call, as Cancel does on the Scheduled calls page: the enrollment ends as failed (retry_reason manual_cancel) with no retry, callback or next step left. A call in progress is refused: end it with POST /v1/calls/{id}/terminate.
campaigns:writePath parameters
iduuidrequiredThe scheduled call's id (id in GET /v1/scheduled-calls: its enrollment's id).
Errors
404not_foundNo scheduled call with this id in your workspace (ids of other workspaces answer the same).409conflictThe contact is on a call right now (details.reason CONTACT_ON_CALL), or has no call to cancel (NO_PENDING_CALL, with details.enrollment_status): it finished, or waits for its campaign to start (take it out with POST /v1/campaigns/{id}/enrollments/{enrollment_id}/remove).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/scheduled-calls/f8d1c003-b292-448d-b8d8-1dd521b94b3b/cancel \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"enrollment_id": "7593112e-56ab-4a08-858f-21035eb1bacc",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"cancelled": true,
"enrollment_status": "failed",
"retry_reason": "manual_cancel"
}
}/v1/campaigns/:id/variantsGet a campaign's A/B test
The campaign's A/B test as its Settings show it: each variant (A, B, C) with its agent, the version its calls use (pinned at its first call; null before it and once the test is promoted, when calls follow the agent's live version), its voice and its share of the contacts that enter the campaign from now on. variants is empty when the campaign has no test. With calls:read too, each variant carries its results to date (contacts, calls, answered, interested or booked, answer and conversion rates) and unassigned_contacts counts the contacts added before the test; without it both are null, as the dashboard shows results only to those who see every call.
campaigns:readPath parameters
iduuidrequiredThe campaign id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).503service_unavailableA/B tests aren't available for a few minutes during a release.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/variants \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"variants": [
{
"id": "248bc6c9-d8e4-4e98-83d0-8bf9dbab1872",
"label": "A",
"position": 1,
"name": "Warm opener",
"agent": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sofía"
},
"agent_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"agent_live_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"voice": {
"id": "9Godp7dNohUvXk6qp0gS",
"name": "Regina - Contact Center"
},
"weight": 50,
"promoted_at": null,
"first_called_at": "2026-10-20T15:02:11.000Z",
"pinned_at": "2026-10-20T15:02:11.000Z",
"moved_at": null,
"results": {
"contacts": 412,
"attempted": 398,
"reached": 241,
"interested": 57,
"booked": 31,
"transferred": 4,
"calls": 690,
"answered_calls": 251,
"answer_rate": 0.3638,
"conversion_rate": 0.1383
}
},
{
"id": "1f677692-f174-4dab-befc-4568ebc14452",
"label": "B",
"position": 2,
"name": "Direct opener",
"agent": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sofía"
},
"agent_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"agent_live_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"voice": null,
"weight": 50,
"promoted_at": null,
"first_called_at": "2026-10-20T15:02:11.000Z",
"pinned_at": "2026-10-20T15:02:11.000Z",
"moved_at": null,
"results": {
"contacts": 405,
"attempted": 398,
"reached": 241,
"interested": 41,
"booked": 31,
"transferred": 4,
"calls": 690,
"answered_calls": 251,
"answer_rate": 0.3638,
"conversion_rate": 0.1012
}
}
],
"promoted": false,
"unassigned_contacts": 128
}
}/v1/campaigns/:id/variantsSave a campaign's A/B test
Saves the whole test, as Save does on the campaign's A/B test: an empty list ends it, or 2 to 3 variants whose shares add up to 100. Send a kept variant's id; a variant without one is new. Each variant calls with an active agent that has a deployed version, and optionally a voice from GET /v1/voices; two variants can't share the same agent and voice. A variant that has called keeps its agent and voice and can't be removed. New contacts are split by the shares from now on; contacts already in a variant keep it.
campaigns:writeCan wait for approvalWorkspace keys only · acts as the key's creatorThe change runs the dashboard's own rules as the member who created the key: that member needs a role that edits campaigns (owner, admin, builder or operator). A key whose creator has left the workspace, and a reseller key, are refused.
On an active campaign it changes, at once, which agent and voice call the contacts that enter it: confirm with the user first.
Path parameters
iduuidrequiredThe campaign id.
Request body
variantsobject[] · 0–3 itemsrequiredThe test: [] for none, else 2-3 variants.
variants[].iduuidA kept variant's id.
variants[].agent_iduuidrequiredThe agent the variant calls with.
variants[].weightinteger · 0–100requiredIts share of new contacts, in percent.
variants[].voice_idstring | nullA voice from GET /v1/voices; null or absent keeps the agent's own.
variants[].namestring | null · up to 60A name to tell it apart (A, B and C are the labels).
Show 2 more fields
On an active campaign, new contacts are called at once by the agents and voices the test names. Have an AI agent confirm the variants and their shares with you first.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).400validation_errorThe test isn't valid: details.reason says why (variant_count: 0, 2 or 3 variants; weights_total: shares add up to 100; agent_not_found or agent_unpublished: an active agent of the workspace with a deployed version; duplicate_treatment: two variants call with the same agent and voice; invalid_voice, voice_provider_mismatch: a voice from GET /v1/voices that the agent's and the numbers' voice engine can speak).409conflictA variant that has called keeps its agent and voice and can't be removed (details.reason variant_called, or variant_dialing while its first call is being placed): promote another variant instead.403forbiddenThe member who created the key can't edit campaigns or has left the workspace, or the key is a reseller key.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X PUT https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/variants \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"variants": [
{
"id": "248bc6c9-d8e4-4e98-83d0-8bf9dbab1872",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"weight": 50,
"voice_id": "9Godp7dNohUvXk6qp0gS",
"name": "Warm opener"
},
{
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"weight": 50,
"name": "Direct opener"
}
]
}'{
"success": true,
"data": {
"variants": [
{
"id": "248bc6c9-d8e4-4e98-83d0-8bf9dbab1872",
"label": "A",
"position": 1,
"name": "Warm opener",
"agent": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sofía"
},
"agent_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"agent_live_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"voice": {
"id": "9Godp7dNohUvXk6qp0gS",
"name": "Regina - Contact Center"
},
"weight": 50,
"promoted_at": null,
"first_called_at": "2026-10-20T15:02:11.000Z",
"pinned_at": "2026-10-20T15:02:11.000Z",
"moved_at": null,
"results": {
"contacts": 412,
"attempted": 398,
"reached": 241,
"interested": 57,
"booked": 31,
"transferred": 4,
"calls": 690,
"answered_calls": 251,
"answer_rate": 0.3638,
"conversion_rate": 0.1383
}
},
{
"id": "1f677692-f174-4dab-befc-4568ebc14452",
"label": "B",
"position": 2,
"name": "Direct opener",
"agent": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sofía"
},
"agent_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"agent_live_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"voice": null,
"weight": 50,
"promoted_at": null,
"first_called_at": "2026-10-20T15:02:11.000Z",
"pinned_at": "2026-10-20T15:02:11.000Z",
"moved_at": null,
"results": {
"contacts": 405,
"attempted": 398,
"reached": 241,
"interested": 41,
"booked": 31,
"transferred": 4,
"calls": 690,
"answered_calls": 251,
"answer_rate": 0.3638,
"conversion_rate": 0.1012
}
}
],
"promoted": false,
"unassigned_contacts": 128
}
}/v1/campaigns/:id/variants/:variant_id/promotePromote a variant
Ends the test in the variant's favor, as Promote does: it gets every contact that enters the campaign from now on (100%), the others none. Contacts already in a variant keep it, with their retries and callbacks, and from then on every variant's calls follow its agent's live version.
campaigns:writeCan wait for approvalWorkspace keys only · acts as the key's creatorThe change runs the dashboard's own rules as the member who created the key: that member needs a role that edits campaigns (owner, admin, builder or operator). A key whose creator has left the workspace, and a reseller key, are refused.
Path parameters
iduuidrequiredThe campaign id.
variant_iduuidrequiredThe variant id, from GET /v1/campaigns/{id}/variants.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
404not_foundNo campaign or variant with this id in your workspace (ids of other workspaces answer the same).403forbiddenThe member who created the key can't edit campaigns or has left the workspace, or the key is a reseller key.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/variants/248bc6c9-d8e4-4e98-83d0-8bf9dbab1872/promote \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"variants": [
{
"id": "248bc6c9-d8e4-4e98-83d0-8bf9dbab1872",
"label": "A",
"position": 1,
"name": "Warm opener",
"agent": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sofía"
},
"agent_version": null,
"agent_live_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"voice": {
"id": "9Godp7dNohUvXk6qp0gS",
"name": "Regina - Contact Center"
},
"weight": 100,
"promoted_at": "2026-10-26T18:00:00.000Z",
"first_called_at": "2026-10-20T15:02:11.000Z",
"pinned_at": "2026-10-20T15:02:11.000Z",
"moved_at": null,
"results": {
"contacts": 412,
"attempted": 398,
"reached": 241,
"interested": 57,
"booked": 31,
"transferred": 4,
"calls": 690,
"answered_calls": 251,
"answer_rate": 0.3638,
"conversion_rate": 0.1383
}
},
{
"id": "1f677692-f174-4dab-befc-4568ebc14452",
"label": "B",
"position": 2,
"name": "Direct opener",
"agent": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sofía"
},
"agent_version": null,
"agent_live_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"voice": null,
"weight": 0,
"promoted_at": null,
"first_called_at": "2026-10-20T15:02:11.000Z",
"pinned_at": "2026-10-20T15:02:11.000Z",
"moved_at": null,
"results": {
"contacts": 405,
"attempted": 398,
"reached": 241,
"interested": 41,
"booked": 31,
"transferred": 4,
"calls": 690,
"answered_calls": 251,
"answer_rate": 0.3638,
"conversion_rate": 0.1012
}
}
],
"promoted": true,
"unassigned_contacts": 128
}
}/v1/campaigns/:id/variants/:variant_id/use-live-versionMove a variant to its agent's live version
From now on the variant's calls use its agent's live (deployed) version instead of the version pinned when it began calling, as "Use the live version" does: deploying an agent alone leaves a running test's variant on its pinned version. The variant's results mark since when (pinned_at). moved is false, with a reason, when there was nothing to move: not_pinned (it hasn't called yet), already_live or promoted (a promoted test already follows the live version).
campaigns:writeCan wait for approvalWorkspace keys only · acts as the key's creatorThe change runs the dashboard's own rules as the member who created the key: that member needs a role that edits campaigns (owner, admin, builder or operator). A key whose creator has left the workspace, and a reseller key, are refused.
Path parameters
iduuidrequiredThe campaign id.
variant_iduuidrequiredThe variant id, from GET /v1/campaigns/{id}/variants.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
404not_foundNo campaign or variant with this id in your workspace (ids of other workspaces answer the same).403forbiddenThe member who created the key can't edit campaigns or has left the workspace, or the key is a reseller key.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/variants/248bc6c9-d8e4-4e98-83d0-8bf9dbab1872/use-live-version \ -H "Authorization: Bearer $NUTALK_API_KEY"
moved and reason{
"success": true,
"data": {
"variants": [
{
"id": "248bc6c9-d8e4-4e98-83d0-8bf9dbab1872",
"label": "A",
"position": 1,
"name": "Warm opener",
"agent": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sofía"
},
"agent_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"agent_live_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"voice": {
"id": "9Godp7dNohUvXk6qp0gS",
"name": "Regina - Contact Center"
},
"weight": 50,
"promoted_at": null,
"first_called_at": "2026-10-20T15:02:11.000Z",
"pinned_at": "2026-10-20T15:02:11.000Z",
"moved_at": null,
"results": {
"contacts": 412,
"attempted": 398,
"reached": 241,
"interested": 57,
"booked": 31,
"transferred": 4,
"calls": 690,
"answered_calls": 251,
"answer_rate": 0.3638,
"conversion_rate": 0.1383
}
},
{
"id": "1f677692-f174-4dab-befc-4568ebc14452",
"label": "B",
"position": 2,
"name": "Direct opener",
"agent": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sofía"
},
"agent_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"agent_live_version": {
"id": "2ea87934-6e03-4db7-80f2-ac753b724c82",
"label": "v1.4"
},
"voice": null,
"weight": 50,
"promoted_at": null,
"first_called_at": "2026-10-20T15:02:11.000Z",
"pinned_at": "2026-10-20T15:02:11.000Z",
"moved_at": null,
"results": {
"contacts": 405,
"attempted": 398,
"reached": 241,
"interested": 41,
"booked": 31,
"transferred": 4,
"calls": 690,
"answered_calls": 251,
"answer_rate": 0.3638,
"conversion_rate": 0.1012
}
}
],
"promoted": false,
"unassigned_contacts": 128,
"moved": true,
"reason": null
}
}/v1/campaigns/:id/stagesList a campaign's stages
The campaign's pipeline stages in their order, as its Pipeline settings list them. An enrollment's stage_id says which one a contact is in; PATCH /v1/campaigns/{id}/enrollments/{enrollment_id} moves it. Every pipeline has at least one success and one failure end stage.
campaigns:readPath parameters
iduuidrequiredThe campaign id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/stages \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": [
{
"id": "8ccfec1e-3750-4ce2-97f9-b81c6a4c1c04",
"name": "New",
"position": 0,
"color": "#64748b",
"is_terminal": false,
"terminal_type": null,
"created_at": "2026-10-01T16:20:00.000Z"
},
{
"id": "07421f9c-cb6c-4b43-9d83-905f64782959",
"name": "Interested",
"position": 3,
"color": "#22c55e",
"is_terminal": true,
"terminal_type": "success",
"created_at": "2026-10-01T16:20:00.000Z"
}
]
}/v1/campaigns/:id/stagesAdd a stage
Adds a stage after the campaign's last one, as Add stage does in its Pipeline settings. PUT /v1/campaigns/{id}/stages/order moves it.
campaigns:writeWorkspace keys only · acts as the key's creatorPath parameters
iduuidrequiredThe campaign id.
Request body
namestring · 1–80 charactersrequiredThe stage's name.
colorstringA hex color, such as #22c55e (default #64748b).
is_terminalbooleanAn end stage: a contact's way through the pipeline ends there, well or badly.
terminal_typesuccess | failure | nullFor an end stage: success or failure.
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X POST https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/stages \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Interested",
"color": "#22c55e",
"is_terminal": true,
"terminal_type": "success"
}'{
"success": true,
"data": {
"id": "07421f9c-cb6c-4b43-9d83-905f64782959",
"name": "Interested",
"position": 3,
"color": "#22c55e",
"is_terminal": true,
"terminal_type": "success",
"created_at": "2026-10-01T16:20:00.000Z"
}
}/v1/campaigns/:id/stages/:stage_idChange a stage
Renames a stage, changes its color, or makes it an end stage (is_terminal with terminal_type) or a step on the way (is_terminal: false). Only the fields sent change.
campaigns:writeCan wait for approvalWorkspace keys only · acts as the key's creatorPath parameters
iduuidrequiredThe campaign id.
stage_iduuidrequiredThe stage id, from GET /v1/campaigns/{id}/stages.
Request body
namestring · 1–80 charactersThe stage's name.
colorstringA hex color, such as #22c55e (default #64748b).
is_terminalbooleanAn end stage: a contact's way through the pipeline ends there, well or badly.
terminal_typesuccess | failure | nullFor an end stage: success or failure.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).404not_foundNo campaign or stage with this id in your workspace (ids of other workspaces answer the same).409conflictIt would leave the pipeline without a success or a failure end stage (details.reason LAST_END_STAGE): every pipeline keeps at least one of each.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X PATCH https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/stages/07421f9c-cb6c-4b43-9d83-905f64782959 \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Booked"
}'{
"success": true,
"data": {
"id": "07421f9c-cb6c-4b43-9d83-905f64782959",
"name": "Booked",
"position": 3,
"color": "#22c55e",
"is_terminal": true,
"terminal_type": "success",
"created_at": "2026-10-01T16:20:00.000Z"
}
}/v1/campaigns/:id/stages/:stage_idDelete a stage
Deletes a stage no contact is in and no workflow step moves contacts into. Only a key created by an owner or admin may, as only they delete stages in the dashboard.
campaigns:writeWaits for approvalWorkspace keys only · acts as the key's creatorPath parameters
iduuidrequiredThe campaign id.
stage_iduuidrequiredThe stage id, from GET /v1/campaigns/{id}/stages.
Unless its key was set not to wait, nothing is removed yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
404not_foundNo campaign or stage with this id in your workspace (ids of other workspaces answer the same).409conflictIt would leave the pipeline without a success or a failure end stage (details.reason LAST_END_STAGE): every pipeline keeps at least one of each.409conflictContacts are in the stage (details.reason STAGE_HAS_CONTACTS, details.contacts): move them first. Or a workflow step moves contacts into it (STAGE_IN_WORKFLOW, details.steps): change that step first.403forbiddenThe key wasn't created by an owner or admin, its creator has left the workspace, or it is a reseller key.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X DELETE https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/stages/07421f9c-cb6c-4b43-9d83-905f64782959 \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"id": "07421f9c-cb6c-4b43-9d83-905f64782959",
"deleted": true
}
}/v1/campaigns/:id/stages/orderReorder a campaign's stages
Puts the stages in a new order, as dragging them does in the Pipeline settings: stage_ids lists every stage of the campaign exactly once, first to last. Nothing is written when it doesn't.
campaigns:writeCan wait for approvalWorkspace keys only · acts as the key's creatorPath parameters
iduuidrequiredThe campaign id.
Request body
stage_idsuuid[] · 1–200 itemsrequiredEvery stage of the campaign, in the new order.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
400validation_errorThe body is invalid, or stage_ids doesn't list every stage of the campaign exactly once.404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X PUT https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/stages/order \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"stage_ids": [
"8ccfec1e-3750-4ce2-97f9-b81c6a4c1c04",
"07421f9c-cb6c-4b43-9d83-905f64782959"
]
}'{
"success": true,
"data": [
{
"id": "8ccfec1e-3750-4ce2-97f9-b81c6a4c1c04",
"name": "New",
"position": 0,
"color": "#64748b",
"is_terminal": false,
"terminal_type": null,
"created_at": "2026-10-01T16:20:00.000Z"
},
{
"id": "07421f9c-cb6c-4b43-9d83-905f64782959",
"name": "Interested",
"position": 3,
"color": "#22c55e",
"is_terminal": true,
"terminal_type": "success",
"created_at": "2026-10-01T16:20:00.000Z"
}
]
}/v1/campaigns/:id/inbound-channelGet a campaign's inbound number
The phone channel whose callers join the campaign as contacts (the inbound number in the campaign's settings), or channel: null.
campaigns:readPath parameters
iduuidrequiredThe campaign id.
Errors
404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/inbound-channel \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"channel": {
"id": "9f19ef32-eaef-452c-bebc-14d9759cdb64",
"name": "Sales line",
"phone_number": "+15125550100"
}
}
}/v1/campaigns/:id/inbound-channelSet a campaign's inbound number
Sets the phone channel whose callers join the campaign as contacts, as "Add people who call this number" does in its settings: while the campaign is active, a caller to it is enrolled and goes through its steps. channel_id: null unlinks it, and that is never held for approval (it stops callers joining). A channel another campaign already takes callers from is refused; unlink it there first. A campaign takes callers from one number: the one set here replaces the one it had.
campaigns:writeCan wait for approvalWorkspace keys only · acts as the key's creatorPath parameters
iduuidrequiredThe campaign id.
Request body
channel_iduuid | nullrequiredA phone channel of the workspace (GET /v1/channels), or null to unlink.
Nothing changes yet: the call answers 202 with data.change_request, and runs once an owner or admin approves it in Settings → API keys. Follow it with GET /v1/change-requests/:id.
Errors
400validation_errorThe body is not JSON, or a field is missing or invalid (details.errors lists each field).404not_foundNo campaign or phone channel with this id in your workspace (ids of other workspaces answer the same).409conflictAnother campaign already takes this number's callers (details.reason CHANNEL_LINKED_TO_ANOTHER_CAMPAIGN, details.campaign_id), or the campaign's inbound number changed while this request ran (details.reason INBOUND_CHANNEL_CHANGED): nothing was written, so read it again and retry.403forbiddenThe key lacks channels:write, or the member who created it can't change channels (owner, admin or builder) or has left the workspace, or it is a reseller key.Plus the errors every request can return (401, 403, 429, 500): Errors.
curl -X PUT https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/inbound-channel \
-H "Authorization: Bearer $NUTALK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "9f19ef32-eaef-452c-bebc-14d9759cdb64"
}'{
"success": true,
"data": {
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"channel": {
"id": "9f19ef32-eaef-452c-bebc-14d9759cdb64",
"name": "Sales line",
"phone_number": "+15125550100"
}
}
}/v1/campaigns/:id/analyticsGet a campaign's analytics
The campaign's results as its Analytics page shows them: its contacts by answer (lead_responses), its calls by outcome, the answer rate (answered calls per call) and the conversion rate (interested or booked contacts per contact), the WhatsApp messages its steps sent, and the contacts added per day in the campaign's timezone. range limits the calls, the messages and the daily counts to the last 7, 30 or 90 days (the daily counts cover the last 30 days with all); the contact counts are the campaign's totals.
campaigns:readThe call figures (call_outcomes, total_calls, answered_calls, voicemail_calls, answer_rate) count every call of the campaign, so they are answered only to a key that may read every call (calls:read), as the dashboard shows them only to members who see every call. For any other key they are null; the contact, message and conversion figures are always answered.
Path parameters
iduuidrequiredThe campaign id.
Query parameters
range7d | 30d | 90d | allThe period: 7d, 30d, 90d or all. Default: all.
Errors
400validation_errorA query parameter is invalid (details.errors lists each one).404not_foundNo campaign with this id in your workspace (ids of other workspaces answer the same).Plus the errors every request can return (401, 403, 429, 500): Errors.
curl "https://www.nutalk.ai/v1/campaigns/f8d1c003-b292-448d-b8d8-1dd521b94b3b/analytics?range=30d" \ -H "Authorization: Bearer $NUTALK_API_KEY"
{
"success": true,
"data": {
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"range": "30d",
"total_enrollments": 945,
"active_enrollments": 212,
"completed_enrollments": 690,
"failed_enrollments": 43,
"lead_responses": {
"unknown": 501,
"interested": 98,
"not_interested": 214,
"callback_requested": 37,
"appointment_booked": 72,
"do_not_contact": 23
},
"call_outcomes": {
"answered": 492,
"no_answer": 610,
"voicemail": 288,
"busy": 41,
"failed": 19
},
"total_calls": 1450,
"answered_calls": 492,
"voicemail_calls": 288,
"total_messages": 310,
"answer_rate": 0.3393,
"conversion_rate": 0.1799,
"enrollments_by_day": [
{
"date": "2026-10-24",
"count": 38
},
{
"date": "2026-10-25",
"count": 12
}
]
}
}Campaign statuses
| Status | Meaning | What you can do |
|---|---|---|
draft | Being set up; contacts can wait in it when API import is on. | start, update, archive |
active | Calling enrolled contacts within its calling hours. | pause, update |
paused | Not calling: queued steps were cancelled and runnable contacts set aside until it starts again. | start, update, archive |
completed | Neither the API nor the dashboard sets it: a campaign does not complete on its own when its contacts finish. | archive |
archived | Kept for history; it cannot start again. | read |
Enrollment statuses
| Status | Meaning |
|---|---|
enrolled | Waiting for the campaign to start (held). |
queued | Ready for its next step. |
in_progress | A step is running (for example a call). |
retry_scheduled | Waiting for a retry (no answer, voicemail…). |
callback_scheduled | The contact asked to be called back at a time. |
paused | Parked: the workspace ran out of credits, or its steps kept failing. Credit-parked contacts resume when the campaign is started again after a top-up. |
completed | Finished the workflow; see lead_response and final_call_outcome. |
failed | A step kept failing (for example no agent deployed on the channel), or its coming call was cancelled (retry_reason manual_cancel). |
exhausted | Ran out of attempts. |
rejected | Refused (for example the contact opted out). |
removed | Taken out of the campaign. |
lead_response is one of unknown, interested, not_interested, callback_requested, appointment_booked or do_not_contact. With a purpose, the AI's answer to its question picks the end (for example appointment_booked or not_interested) and an unanswered call ends unknown. With the older default (no purpose or agent) an answered call ends interested and an unanswered one not_interested.