Skip to content

Campaigns

A campaign calls a list of contacts with one published agent, inside a calling window you choose. You create a campaign as a draft, add contacts, then start it.

Status Meaning
draft Being set up. Only a draft can be edited or receive contacts.
scheduled Waiting for its start time.
running Dialing contacts inside the calling window.
paused Paused. Calls in progress finish.
completed Every contact has reached a final result.
stopped Stopped for good.

Contacts are dialed only inside the calling window (window_start to window_end, on the days in window_days, in the campaign’s time_zone). A contact that is not answered is retried up to max_attempts times, retry_minutes apart.

{
"id": "camp_a91c33",
"name": "November follow-ups",
"agent_id": "agent_8f3a1c",
"agent_version_id": null,
"status": "draft",
"outcomes": [
{ "key": "interested", "label": "Interested", "description": "Wants to hear more, asked for details, or agreed to a next step." },
{ "key": "not_interested", "label": "Not interested", "description": "Declined, politely or otherwise." },
{ "key": "callback", "label": "Call back later", "description": "Asked to be called at another time." },
{ "key": "wrong_number", "label": "Wrong number", "description": "Not the person we meant to reach." },
{ "key": "do_not_call", "label": "Do not call", "description": "The person asked not to be called again." },
{ "key": "unclear", "label": "Unclear", "description": "The conversation did not reach a clear result." }
],
"window_start": "10:00",
"window_end": "19:00",
"window_days": "123456",
"time_zone": "Asia/Kolkata",
"max_attempts": 2,
"retry_minutes": 90,
"starts_at": null,
"started_at": null,
"finished_at": null,
"created_at": "2026-11-04T09:00:00",
"updated_at": "2026-11-04T09:00:00",
"counts": {},
"contacts_total": 0
}
Field Description
outcomes The buckets Sahay sorts each answered call into. do_not_call and unclear are always present. When a contact’s outcome is do_not_call, the number is added to your organization’s blocklist.
window_days Calling days as digits, where Monday is 1 and Sunday is 7. 123456 is Monday to Saturday.
counts Number of contacts in each contact status.
contacts_total Number of contacts.
outcome_counts Contacts per outcome. Returned by GET /campaigns/{campaign_id} once the campaign has left draft.
talk_seconds Total talk time. Returned by GET /campaigns/{campaign_id}.

GET /campaigns

Lists campaigns, newest first.

Parameter In Description
status query One or more of draft, scheduled, running, paused, completed, stopped, comma separated.
limit query Page size, 1 to 100. Default 50.
cursor query The next_cursor of the previous page.
Terminal window
curl "$BASE_URL/campaigns?status=running,paused" \
-H "Authorization: Bearer $SAHAY_API_KEY"

Response 200:

{
"data": [
{
"id": "camp_a91c33",
"name": "November follow-ups",
"agent_id": "agent_8f3a1c",
"agent_version_id": null,
"status": "running",
"window_start": "10:00",
"window_end": "19:00",
"window_days": "123456",
"time_zone": "Asia/Kolkata",
"max_attempts": 2,
"retry_minutes": 90,
"counts": { "pending": 40, "answered": 8, "no_answer": 2 },
"contacts_total": 50
}
],
"next_cursor": null
}

Each item has the full campaign object. It is shortened here.

Errors: 400 invalid_cursor, 422 invalid_request (for example limit above 100).

POST /campaigns

Creates a draft campaign for a published agent. It answers 201 with the campaign.

Field Type Required Description
name string yes 1 to 120 characters.
agent_id string yes An active agent with a published version.
outcomes array no Your own outcome buckets, replacing the defaults. Each is {"key", "label", "description"}. key is lowercase letters, digits and underscores, starting with a letter, 2 to 32 characters. label is 1 to 40 characters, description up to 200. Keys must be unique, at most 8, and cannot be do_not_call or unclear, which Sahay adds. Default is interested, not_interested, callback and wrong_number.
window_start string no HH:MM, 24-hour. Default 10:00.
window_end string no HH:MM, 24-hour. Must be after window_start. Default 19:00.
window_days string no Digits 1 to 7, Monday is 1. Default 123456.
max_attempts integer no Tries per contact, 1 to 5. Default 2.
retry_minutes integer no Minutes before a retry, at least 5. Default 90.
Terminal window
curl -X POST "$BASE_URL/campaigns" \
-H "Authorization: Bearer $SAHAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "November follow-ups", "agent_id": "agent_8f3a1c", "window_start": "10:00", "window_end": "19:00", "window_days": "123456"}'

Response 201: the campaign object shown above, in the draft status.

Errors: 409 agent_not_published (pick an active agent with a published version), 422 invalid_request (for example window_start not before window_end, or too many outcomes).

GET /campaigns/{campaign_id}

Returns one campaign. counts and contacts_total show its progress.

Terminal window
curl "$BASE_URL/campaigns/camp_a91c33" \
-H "Authorization: Bearer $SAHAY_API_KEY"

Response 200 for a running campaign (outcomes shortened):

{
"id": "camp_a91c33",
"name": "November follow-ups",
"agent_id": "agent_8f3a1c",
"agent_version_id": null,
"status": "running",
"window_start": "10:00",
"window_end": "19:00",
"window_days": "123456",
"time_zone": "Asia/Kolkata",
"max_attempts": 2,
"retry_minutes": 90,
"starts_at": null,
"started_at": "2026-11-04T10:00:00",
"finished_at": null,
"created_at": "2026-11-04T09:00:00",
"updated_at": "2026-11-04T10:00:00",
"counts": { "pending": 40, "answered": 8, "no_answer": 2 },
"contacts_total": 50,
"outcome_counts": { "interested": 3, "not_interested": 5 },
"talk_seconds": 640
}

Errors: 404 campaign_not_found.

PATCH /campaigns/{campaign_id}

Updates a draft campaign. Every field is optional, and a field you leave out is unchanged. The fields are the same as for Create a campaign, and you can also change agent_id and name.

Terminal window
curl -X PATCH "$BASE_URL/campaigns/camp_a91c33" \
-H "Authorization: Bearer $SAHAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "November follow-ups, batch 2"}'

Response 200: the updated campaign object.

Errors: 404 campaign_not_found, 409 not_draft (only a draft can be changed), 409 bad_window, 409 agent_not_published, 422 invalid_request.

POST /campaigns/{campaign_id}/contacts

Adds contacts to a draft campaign, up to 500 per request.

Field Type Required Description
contacts array yes 1 to 500 contacts.
contacts[].phone string yes E.164, for example +919876543210.
contacts[].name string no Up to 200 characters.
contacts[].variables object no Values the agent can use in the call. Each value is a string, number, boolean or null.
Terminal window
curl -X POST "$BASE_URL/campaigns/camp_a91c33/contacts" \
-H "Authorization: Bearer $SAHAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contacts": [{"phone": "+919876543210", "name": "Asha", "variables": {"plan": "gold"}}, {"phone": "+919812345678", "name": "Ravi"}]}'

Response 200:

{
"accepted": 2,
"rejected": 0,
"leads_created": 2,
"errors": [],
"max_per_request": 500
}

A bad row is rejected on its own and listed in errors. The other rows are still added. Each entry in errors looks like this:

{
"index": 3,
"phone": "12345",
"code": "invalid_phone",
"message": "phone must be E.164, e.g. +919876543210"
}

index is the position of the row in your contacts array. The codes are:

Code Meaning
invalid_phone The number is not a dialable E.164 number.
duplicate_in_request The same number appears earlier in this request.
already_in_campaign The number is already a contact of this campaign.
blocklisted The number is on your organization’s blocklist. If it belongs to an existing lead it is added as a skipped contact and never dialed, but it is still reported here and counted in rejected.

leads_created is the number of new Sahay leads created for contacts that were not already leads.

Errors: 404 campaign_not_found, 409 not_draft, 422 invalid_request (more than 500 contacts, none, an unknown field, or a phone longer than 32 characters).

GET /campaigns/{campaign_id}/contacts

Lists a campaign’s contacts and their call results, 100 per page.

Parameter In Description
page query Page number, starting at 0. Default 0.
status query Only contacts with this status: pending, dialling, answered, no_answer, busy, failed, skipped_blocklist or skipped.
Terminal window
curl "$BASE_URL/campaigns/camp_a91c33/contacts?status=answered&page=0" \
-H "Authorization: Bearer $SAHAY_API_KEY"

Response 200:

{
"data": [
{
"id": "cc_01",
"phone": "+919876543210",
"name": "Asha",
"lead_id": "lead_77c1e9",
"status": "answered",
"attempts": 1,
"outcome": "interested",
"summary": "Wants a quote for 20 units.",
"callback_at": null,
"duration_seconds": 74,
"variables": { "plan": "gold" },
"created_at": "2026-11-04T09:05:00",
"updated_at": "2026-11-04T10:12:00"
}
],
"total": 1,
"page": 0,
"page_size": 100
}

outcome and summary are null until the answered call has been classified. callback_at is set when the outcome is a request to call back at a given time. Note that the contact status is spelled dialling, while a call status is spelled dialing.

Errors: 404 campaign_not_found.

POST /campaigns/{campaign_id}/start

Starts dialing a draft campaign. It needs at least one contact. There is no request body.

Terminal window
curl -X POST "$BASE_URL/campaigns/camp_a91c33/start" \
-H "Authorization: Bearer $SAHAY_API_KEY"

Response 200: the campaign object with status set to running and started_at filled in.

Errors: 404 campaign_not_found, 409 not_draft, 409 no_contacts, 409 no_caller_id, 409 agent_number_paused, 409 agent_not_published, 409 tick_not_scheduled.

POST /campaigns/{campaign_id}/pause

Pauses a running or scheduled campaign. Calls in progress finish. There is no request body.

Terminal window
curl -X POST "$BASE_URL/campaigns/camp_a91c33/pause" \
-H "Authorization: Bearer $SAHAY_API_KEY"

Response 200: the campaign object with status set to paused.

Errors: 404 campaign_not_found, 409 not_pausable.

POST /campaigns/{campaign_id}/resume

Resumes a paused campaign. There is no request body.

Terminal window
curl -X POST "$BASE_URL/campaigns/camp_a91c33/resume" \
-H "Authorization: Bearer $SAHAY_API_KEY"

Response 200: the campaign object with status set to running.

Errors: 404 campaign_not_found, 409 not_resumable.

POST /campaigns/{campaign_id}/stop

Stops a running, scheduled or paused campaign for good. It cannot be restarted. There is no request body.

Terminal window
curl -X POST "$BASE_URL/campaigns/camp_a91c33/stop" \
-H "Authorization: Bearer $SAHAY_API_KEY"

Response 200: the campaign object with status set to stopped.

Errors: 404 campaign_not_found, 409 not_stoppable.

To follow a campaign without polling, subscribe to the campaign.contact.completed and campaign.completed events. See Webhooks.