Campaigns
Outbound calling campaigns, their contacts, per-contact results, follow-ups and extracted data.
Create a campaign
Section titled “Create a campaign”POST /campaigns
Creates a DRAFT outbound calling campaign in your organisation. The body’s organisationId must be the key’s own organisation and the agent must belong to it. Upload contacts next, then start it.
Scope: campaigns:write
Body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
organisationId |
string | yes | Your organisation ID. Must equal the key’s own organisation. |
voiceAgentId |
string | yes | Agent that makes the calls. Must be a live agent in your organisation. |
callStartTime |
string | yes | Daily window start, HH:MM in the campaign timezone. Must be before callEndTime. |
callEndTime |
string | yes | Daily window end, HH:MM. |
description |
string | no | |
targetCountryCode |
string | no | Dial code used to normalise uploaded numbers. Defaults to +1 when omitted, so send +91 for Indian numbers. |
dialingNumber |
string | no | Caller number. For an agent that calls through a carrier account (Plivo, Twilio, Exotel, FreJun, VoiceLink) it must be the agent’s own calling number or an active, voice-capable number on that same carrier account in your organisation (400 otherwise); the campaign then calls from it. For an agent without a carrier it must match an active outbound SIP configuration in your organisation (404 otherwise). Omit it to call from the agent’s own number. |
retryCount |
integer | no | |
cooldownSeconds |
integer | no | Wait between retries. |
maxCallDuration |
integer | no | Seconds. |
inactivityDuration |
integer | no | Seconds of silence before the call ends. |
startDate |
string | no | ISO 8601 date or YYYY-MM-DD. Optional. |
endDate |
string | no | ISO 8601 date or YYYY-MM-DD. Must not be before today (in the campaign timezone) or before startDate. |
timezone |
string | no | IANA timezone such as Asia/Kolkata. |
dataExtractionSchema |
object | no | Post-campaign data extraction configuration. |
followUpRules |
object | no | Follow-up rules. Omit to leave unchanged, send null to remove them. Missing keys take defaults (enabled true, results [no_answer, not_reached, voicemail], maxRounds 1, delayDays 1, windowDays 7, autoStart true). Booleans also accept “true”/“false”/“1”/“0”; anything else is a 400. |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns" \ -H "Authorization: Bearer $FOAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Diwali offer reminders","organisationId":"cmf1a2b3c0000orgx5678ijkl","voiceAgentId":"cmf2b3c4d0002agnt9012mnop","callStartTime":"10:00","callEndTime":"19:00","timezone":"Asia/Kolkata","targetCountryCode":"+91","retryCount":2,"cooldownSeconds":1800,"startDate":"2026-10-01","endDate":"2026-10-07","dialingNumber":"+918071234567"}'Response 201
{ "success": true, "data": { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "description": "Remind past buyers about the festive offer", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "createdById": "cmf0z9y8x0003user3456qrst", "createdBy": { "id": "cmf0z9y8x0003user3456qrst", "name": "Priya Sharma", "email": "priya@example.com" }, "voiceAgentId": "cmf2b3c4d0002agnt9012mnop", "dialingNumber": "+918071234567", "voiceAgent": { "id": "cmf2b3c4d0002agnt9012mnop", "name": "Order confirmation", "customVariables": { "orderId": "", "city": "" }, "callingNumber": "+918071234567", "telephonyProvider": "plivo" }, "targetCountryCode": "+91", "callStartTime": "10:00", "callEndTime": "19:00", "startDate": "2026-10-01T00:00:00.000Z", "endDate": "2026-10-07T23:59:59.999Z", "timezone": "Asia/Kolkata", "retryCount": 2, "cooldownSeconds": 1800, "maxCallDuration": 300, "inactivityDuration": 30, "status": "DRAFT", "totalContacts": 0, "completedCalls": 0, "failedCalls": 0, "inProgressCalls": 0, "uploadedFileName": null, "uploadedFileUrl": null, "dataExtractionSchema": null, "followUpRound": 0, "followUpRules": null, "createdAt": "2026-09-25T09:30:00.000Z", "updatedAt": "2026-09-25T09:30:00.000Z" }, "message": "Campaign created successfully"}Errors
| Code | When |
|---|---|
| 400 | Validation failed: missing fields, bad HH:MM, start not before end, bad timezone, dates in the past, invalid followUpRules, or a dialingNumber that is not an active number on the agent’s carrier account |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks campaigns:write, or organisationId is not the key’s organisation |
| 404 | Voice agent not in this organisation, dialingNumber has no active outbound SIP configuration (agents without a carrier), or a follow-up agent/number is not in this organisation |
| 429 | Rate limited |
List campaigns
Section titled “List campaigns”GET /campaigns/organisations/{organisationId}
Lists your organisation’s campaigns, newest first. For running or paused campaigns the counters are recomputed live from the contact list.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organisationId |
path | string | yes | Your organisation ID. Must be the key’s own organisation. |
page |
query | integer | no | Page number, starting at 1. |
limit |
query | integer | no | Items per page (1 to 100). |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/organisations/{organisationId}" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "campaigns": [ { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "description": "Remind past buyers about the festive offer", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "createdById": "cmf0z9y8x0003user3456qrst", "createdBy": { "id": "cmf0z9y8x0003user3456qrst", "name": "Priya Sharma", "email": "priya@example.com" }, "voiceAgentId": "cmf2b3c4d0002agnt9012mnop", "dialingNumber": "+918071234567", "voiceAgent": { "id": "cmf2b3c4d0002agnt9012mnop", "name": "Order confirmation", "customVariables": { "orderId": "", "city": "" }, "callingNumber": "+918071234567", "telephonyProvider": "plivo" }, "targetCountryCode": "+91", "callStartTime": "10:00", "callEndTime": "19:00", "startDate": "2026-10-01T00:00:00.000Z", "endDate": "2026-10-07T23:59:59.999Z", "timezone": "Asia/Kolkata", "retryCount": 2, "cooldownSeconds": 1800, "maxCallDuration": 300, "inactivityDuration": 30, "status": "IN_PROGRESS", "totalContacts": 250, "completedCalls": 120, "failedCalls": 18, "inProgressCalls": 3, "uploadedFileName": "diwali_buyers.csv", "uploadedFileUrl": null, "dataExtractionSchema": null, "followUpRound": 0, "followUpRules": null, "createdAt": "2026-09-25T09:30:00.000Z", "updatedAt": "2026-09-25T09:30:00.000Z", "startedAt": "2026-10-01T04:30:00.000Z" } ], "total": 1, "totalPages": 1, "currentPage": 1 }, "message": "Campaigns retrieved successfully"}Errors
| Code | When |
|---|---|
| 400 | Invalid page or limit |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks campaigns:read, or organisationId is not the key’s organisation |
| 429 | Rate limited |
Get a campaign
Section titled “Get a campaign”GET /campaigns/{id}
Returns one campaign. For running or paused campaigns the contact counters are recomputed live.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{id}" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "description": "Remind past buyers about the festive offer", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "createdById": "cmf0z9y8x0003user3456qrst", "createdBy": { "id": "cmf0z9y8x0003user3456qrst", "name": "Priya Sharma", "email": "priya@example.com" }, "voiceAgentId": "cmf2b3c4d0002agnt9012mnop", "dialingNumber": "+918071234567", "voiceAgent": { "id": "cmf2b3c4d0002agnt9012mnop", "name": "Order confirmation", "customVariables": { "orderId": "", "city": "" }, "callingNumber": "+918071234567", "telephonyProvider": "plivo" }, "targetCountryCode": "+91", "callStartTime": "10:00", "callEndTime": "19:00", "startDate": "2026-10-01T00:00:00.000Z", "endDate": "2026-10-07T23:59:59.999Z", "timezone": "Asia/Kolkata", "retryCount": 2, "cooldownSeconds": 1800, "maxCallDuration": 300, "inactivityDuration": 30, "status": "PAUSED", "totalContacts": 250, "completedCalls": 140, "failedCalls": 20, "inProgressCalls": 0, "uploadedFileName": "diwali_buyers.csv", "uploadedFileUrl": null, "dataExtractionSchema": null, "followUpRound": 0, "followUpRules": null, "createdAt": "2026-09-25T09:30:00.000Z", "updatedAt": "2026-09-25T09:30:00.000Z", "uploadedAt": "2026-09-25T09:40:00.000Z", "startedAt": "2026-10-01T04:30:00.000Z", "pausedAt": "2026-10-02T08:00:00.000Z" }, "message": "Campaign retrieved successfully"}Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited |
Update a campaign
Section titled “Update a campaign”PUT /campaigns/{id}
Partially updates a campaign’s settings; send only the fields to change. Status cannot be changed here. The country code is locked once contacts exist.
Scope: campaigns:write
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | |
voiceAgentId |
string | no | Agent that makes the calls. Must be a live agent in your organisation. Changing it re-checks the campaign’s saved caller number against the new agent (400 or 404 when it no longer fits). When the campaign has no saved number and the new agent calls through a carrier account, the new agent’s own calling number is saved. |
callStartTime |
string | no | Daily window start, HH:MM in the campaign timezone. Must be before callEndTime. |
callEndTime |
string | no | Daily window end, HH:MM. |
description |
string | no | |
targetCountryCode |
string | no | Can only change while the campaign has no contacts (409 otherwise). “+91”, “91” and “ +91 “ count as the same country. |
dialingNumber |
string | no | Caller number. For an agent that calls through a carrier account it must be the agent’s own calling number or an active, voice-capable number on that carrier account in your organisation (400 otherwise). For an agent without a carrier it must match an active outbound SIP configuration in your organisation (404 otherwise). Empty string or null clears the saved number. |
retryCount |
integer | no | |
cooldownSeconds |
integer | no | Wait between retries. |
maxCallDuration |
integer | no | Seconds. |
inactivityDuration |
integer | no | Seconds of silence before the call ends. |
startDate |
string | no | ISO 8601 or YYYY-MM-DD; null clears it. |
endDate |
string | no | ISO 8601 or YYYY-MM-DD; null clears it. Must not be in the past. |
timezone |
string | no | IANA timezone. |
dataExtractionSchema |
object | no | Post-campaign data extraction configuration. |
followUpRules |
object | no | Follow-up rules. Omit to leave unchanged, send null to remove them. Keys you leave out keep the campaign’s current rules (or the defaults when it has none: enabled true, results [no_answer, not_reached, voicemail], maxRounds 1, delayDays 1, windowDays 7, autoStart true). Booleans also accept “true”/“false”/“1”/“0”; anything else is a 400. |
status |
string: DRAFT, SCHEDULED, IN_PROGRESS, PAUSED, COMPLETED, CANCELLED | no | Not settable here: any value is refused with 400. Use start, pause or complete. |
Example request
curl -X PUT "https://api.foan.ai/api/v1/campaigns/{id}" \ -H "Authorization: Bearer $FOAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"callStartTime":"11:00","callEndTime":"18:30","retryCount":1,"followUpRules":{"enabled":true,"results":["no_answer","voicemail"],"maxRounds":2}}'Response 200
{ "success": true, "data": { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "description": "Remind past buyers about the festive offer", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "createdById": "cmf0z9y8x0003user3456qrst", "createdBy": { "id": "cmf0z9y8x0003user3456qrst", "name": "Priya Sharma", "email": "priya@example.com" }, "voiceAgentId": "cmf2b3c4d0002agnt9012mnop", "dialingNumber": "+918071234567", "voiceAgent": { "id": "cmf2b3c4d0002agnt9012mnop", "name": "Order confirmation", "callingNumber": "+918071234567", "telephonyProvider": "plivo" }, "targetCountryCode": "+91", "callStartTime": "11:00", "callEndTime": "18:30", "startDate": "2026-10-01T00:00:00.000Z", "endDate": "2026-10-07T23:59:59.999Z", "timezone": "Asia/Kolkata", "retryCount": 1, "cooldownSeconds": 1800, "maxCallDuration": 300, "inactivityDuration": 30, "status": "DRAFT", "totalContacts": 0, "completedCalls": 0, "failedCalls": 0, "inProgressCalls": 0, "uploadedFileName": null, "uploadedFileUrl": null, "dataExtractionSchema": null, "followUpRound": 0, "followUpRules": { "enabled": true, "results": [ "no_answer", "voicemail" ], "maxRounds": 2, "delayDays": 1, "windowDays": 7, "voiceAgentId": null, "sipConfigurationId": null, "autoStart": true }, "createdAt": "2026-09-25T09:30:00.000Z", "updatedAt": "2026-09-25T11:00:00.000Z" }, "message": "Campaign updated successfully"}Errors
| Code | When |
|---|---|
| 400 | Validation failed, status sent, invalid dates or followUpRules, or a dialingNumber (sent, or kept when the agent changes) that is not an active number on the agent’s carrier account |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:write scope, or the campaign belongs to another organisation |
| 404 | Campaign not found, agent not in this organisation, dialingNumber not an active outbound number, or follow-up agent/number not in this organisation |
| 409 | Country change on a campaign that already has contacts, or the campaign is busy with a contact upload |
| 429 | Rate limited |
Delete a campaign
Section titled “Delete a campaign”DELETE /campaigns/{id}
Permanently deletes a campaign and all its contacts and extracted data. A campaign that is IN_PROGRESS cannot be deleted; pause or complete it first. Call records of calls already made stay in call history.
Scope: campaigns:write
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X DELETE "https://api.foan.ai/api/v1/campaigns/{id}" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": null, "message": "Campaign deleted successfully"}Errors
| Code | When |
|---|---|
| 400 | Campaign is in progress |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:write scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 409 | The campaign is busy with a contact upload or copy; retry shortly |
| 429 | Rate limited |
Duplicate a campaign
Section titled “Duplicate a campaign”POST /campaigns/{id}/duplicate
Creates a new DRAFT campaign with the same agent, caller number, schedule, retry settings and extraction schema. Contacts are copied only when includeContacts is true, as a fresh PENDING list with no call history (duplicate numbers collapsed). Follow-up rules are not copied.
Scope: campaigns:write
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | Name of the copy. Defaults to “<source name> (copy)”. |
includeContacts |
boolean | no | Copy the contact list. Also accepts “true”/“false”/“1”/“0”. |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns/{id}/duplicate" \ -H "Authorization: Bearer $FOAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Diwali offer reminders (copy)","includeContacts":true}'Response 201
{ "success": true, "data": { "id": "cmf3k2j9x0009abcd1234wxyz", "name": "Diwali offer reminders (copy)", "description": "Remind past buyers about the festive offer", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "createdById": "cmf0z9y8x0003user3456qrst", "createdBy": { "id": "cmf0z9y8x0003user3456qrst", "name": "Priya Sharma", "email": "priya@example.com" }, "voiceAgentId": "cmf2b3c4d0002agnt9012mnop", "dialingNumber": "+918071234567", "voiceAgent": { "id": "cmf2b3c4d0002agnt9012mnop", "name": "Order confirmation", "customVariables": { "orderId": "", "city": "" }, "callingNumber": "+918071234567", "telephonyProvider": "plivo" }, "targetCountryCode": "+91", "callStartTime": "10:00", "callEndTime": "19:00", "startDate": null, "endDate": null, "timezone": "Asia/Kolkata", "retryCount": 2, "cooldownSeconds": 1800, "maxCallDuration": 300, "inactivityDuration": 30, "status": "DRAFT", "totalContacts": 250, "completedCalls": 0, "failedCalls": 0, "inProgressCalls": 0, "uploadedFileName": "diwali_buyers.csv", "uploadedFileUrl": null, "dataExtractionSchema": null, "followUpRound": 0, "followUpRules": null, "createdAt": "2026-09-25T09:30:00.000Z", "updatedAt": "2026-09-25T09:30:00.000Z", "sipConfigurationId": null, "sipConfiguration": null, "parentCampaignId": null, "followUpSettledAt": null, "followUpDecidedAt": null, "uploadedAt": "2026-09-25T10:00:00.000Z", "startedAt": null, "pausedAt": null, "completedAt": null, "copiedContacts": 250, "scheduleCleared": true }, "message": "Campaign duplicated with 250 contacts. Its dates had already passed, so the copy has no date range yet - set new dates before starting it."}Errors
| Code | When |
|---|---|
| 400 | Validation failed (the error message lists the problems) |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:write scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 409 | The source campaign is busy with another contact upload; retry shortly |
| 429 | Rate limited |
Preview a follow-up round
Section titled “Preview a follow-up round”GET /campaigns/{id}/follow-up
Shows what a follow-up round of this campaign would call: a count per call-result bucket, how many contacts the stored (or default) selection would dial, and the rounds already created. Read only, nothing is created.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{id}/follow-up" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "campaignId": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "status": "COMPLETED", "timezone": "Asia/Kolkata", "followUpRound": 0, "rules": null, "roundsLeft": 0, "buckets": [ { "id": "no_answer", "label": "Did not answer", "count": 42, "canFollowUp": true, "selected": true }, { "id": "not_reached", "label": "Never reached", "count": 8, "canFollowUp": true, "selected": true }, { "id": "voicemail", "label": "Went to voicemail", "count": 11, "canFollowUp": true, "selected": true }, { "id": "hung_up", "label": "Hung up", "count": 6, "canFollowUp": true, "selected": false }, { "id": "callback", "label": "Asked us to call back", "count": 9, "canFollowUp": true, "selected": false }, { "id": "unclear", "label": "Unclear", "count": 5, "canFollowUp": true, "selected": false }, { "id": "interested", "label": "Interested", "count": 64, "canFollowUp": true, "selected": false }, { "id": "not_interested", "label": "Not interested", "count": 90, "canFollowUp": false, "selected": false }, { "id": "do_not_call", "label": "Asked not to be called", "count": 3, "canFollowUp": false, "selected": false }, { "id": "wrong_person", "label": "Wrong person", "count": 12, "canFollowUp": false, "selected": false } ], "eligibleContacts": 61, "excludedFamilyOptOuts": 0, "excludedUnknownOutcomes": 2, "rounds": [], "rootCampaignId": "cmf3k2j9x0001abcd1234efgh", "rootCampaignName": "Diwali offer reminders", "family": [ { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "parentCampaignId": null, "followUpRound": 0, "status": "COMPLETED", "totalContacts": 250, "completedCalls": 180, "failedCalls": 62, "startDate": "2026-10-01T00:00:00.000Z", "endDate": "2026-10-07T23:59:59.999Z", "createdAt": "2026-09-25T09:30:00.000Z", "isOriginal": true } ] }, "message": "Follow-up preview retrieved"}Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 409 | The follow-up chain is inconsistent (cycle, missing member, more than 5 rounds or more than 200 campaigns) |
| 429 | Rate limited |
Create a follow-up round
Section titled “Create a follow-up round”POST /campaigns/{id}/follow-up
Creates the next follow-up round of a COMPLETED campaign: a new campaign with the same settings (and the same caller number, unless the rules pick a different agent) holding only contacts whose last call result is in the selected buckets. People who said not interested, do not call or wrong person in any round are never included, and neither is anyone who was dialled but has no recorded result. A campaign family (the original and all its rounds) can have at most 5 follow-up rounds in total, whichever member you start from. With autoStart on (the default) the round is started straight away, which schedules real calls from the round’s start date.
Scope: campaigns:write
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | Name for the round. Defaults to “<source name> (follow-up N)”. |
results |
array of string | no | Buckets to call again. not_interested, do_not_call and wrong_person are refused with 400. not_reached selects contacts nobody ever dialled. |
autoStart |
boolean | no | Start the round immediately (true by default when neither this request nor the campaign’s stored rules say otherwise). |
maxRounds |
integer | no | |
delayDays |
integer | no | |
windowDays |
integer | no | |
voiceAgentId |
string | no | Agent for the round; must be in your organisation. |
sipConfigurationId |
string | no | Outbound number for the round; must be an active outbound SIP configuration in your organisation. |
rules |
object | no | Alternatively send the same rule keys nested here instead of flat. |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns/{id}/follow-up" \ -H "Authorization: Bearer $FOAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"results":["no_answer","not_reached","callback"],"delayDays":1,"windowDays":3,"autoStart":false}'Response 200
{ "success": true, "data": { "campaign": null, "contacts": 0, "round": 1 }, "message": "Nobody from this campaign needs calling again, so no follow-up was created"}Errors
| Code | When |
|---|---|
| 400 | Invalid rules (unknown or never-follow-up result, out-of-range numbers, bad boolean, empty selection), or the campaign family already has 5 follow-up rounds |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:write scope, or the campaign belongs to another organisation |
| 404 | Campaign not found, or the follow-up agent or number is not in this organisation |
| 409 | Campaign is not COMPLETED, another round of it is still draft/scheduled/running/paused, the campaign is busy, or the follow-up chain is inconsistent |
| 429 | Rate limited |
List calls across a follow-up chain
Section titled “List calls across a follow-up chain”GET /campaigns/{campaignId}/follow-ups/history
Lists the calls made by this campaign and every round in its follow-up chain (original plus rounds), newest first. Pass roundCampaignId to see one round only.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
roundCampaignId |
query | string | no | Limit to one campaign in the chain. Must be a member of this campaign’s follow-up family (400 otherwise). |
page |
query | integer | no | Page number, starting at 1. |
limit |
query | integer | no | Items per page (1 to 100). |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{campaignId}/follow-ups/history" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": [ { "id": "66f1c2e8a4b5c6d7e8f90123", "roomName": "call-7f3a9c2e", "agentId": "cmf2b3c4d0002agnt9012mnop", "agentName": "Order confirmation", "orgId": "cmf1a2b3c0000orgx5678ijkl", "participantName": "Rahul Verma", "phoneNumber": "+919876543210", "callStatus": "Completed", "startTime": "2026-10-09T05:12:00.000Z", "createdAt": "2026-10-09T05:12:00.000Z", "endedAt": "2026-10-09T05:13:34.000Z", "callDuration": 94000, "callOutcome": "completed", "callOutcomeDetail": null, "recordingUrl": "/api/v1/voice-sessions/66f1c2e8a4b5c6d7e8f90123/recording", "customVariables": { "participantName": "Rahul Verma" }, "callDirection": "OUTBOUND", "callType": "websocket", "scheduledTime": null, "scheduledStatus": null, "campaignId": "cmf3k2j9x0009abcd1234wxyz", "campaign": { "id": "cmf3k2j9x0009abcd1234wxyz", "name": "Diwali offer reminders (follow-up 1)", "status": "IN_PROGRESS", "followUpRound": 1 }, "campaignName": "Diwali offer reminders (follow-up 1)", "followUpRound": 1, "overview": { "score": 78, "classification": "Relevant", "sentiment": "positive", "summary": [ "Customer asked for the offer link on WhatsApp." ] }, "actions": { "tracking": "not_recorded", "items": [] }, "totalMessages": 14, "userMessages": 7, "assistantMessages": 7, "totalTokens": 0, "firstMessageAt": "2026-10-09T05:12:03.000Z", "lastMessageAt": "2026-10-09T05:13:31.000Z" } ], "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1, "hasMore": false }}Errors
| Code | When |
|---|---|
| 400 | Invalid page/limit, empty roundCampaignId, or roundCampaignId not in this follow-up family |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 409 | The follow-up chain is inconsistent |
| 429 | Rate limited |
Download a contact template
Section titled “Download a contact template”GET /campaigns/template/download/{campaignId}
Returns a CSV template for this campaign’s contact upload: phoneNumber, participantName, then one column per custom variable of the campaign’s agent, with three sample rows.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/template/download/{campaignId}" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited |
| 500 | Template could not be built |
Start a campaign
Section titled “Start a campaign”POST /campaigns/{id}/start
Starts a DRAFT or PAUSED campaign that has contacts. Inside today’s call window it becomes IN_PROGRESS and dialling begins within about a minute; otherwise it becomes SCHEDULED and starts when the window opens. This places real calls.
Scope: campaigns:control
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns/{id}/start" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "description": "Remind past buyers about the festive offer", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "createdById": "cmf0z9y8x0003user3456qrst", "createdBy": { "id": "cmf0z9y8x0003user3456qrst", "name": "Priya Sharma", "email": "priya@example.com" }, "voiceAgentId": "cmf2b3c4d0002agnt9012mnop", "dialingNumber": "+918071234567", "voiceAgent": { "id": "cmf2b3c4d0002agnt9012mnop", "name": "Order confirmation", "callingNumber": "+918071234567", "telephonyProvider": "plivo" }, "targetCountryCode": "+91", "callStartTime": "10:00", "callEndTime": "19:00", "startDate": "2026-10-01T00:00:00.000Z", "endDate": "2026-10-07T23:59:59.999Z", "timezone": "Asia/Kolkata", "retryCount": 2, "cooldownSeconds": 1800, "maxCallDuration": 300, "inactivityDuration": 30, "status": "IN_PROGRESS", "totalContacts": 250, "completedCalls": 0, "failedCalls": 0, "inProgressCalls": 0, "uploadedFileName": null, "uploadedFileUrl": null, "dataExtractionSchema": null, "followUpRound": 0, "followUpRules": null, "createdAt": "2026-09-25T09:30:00.000Z", "updatedAt": "2026-09-25T09:30:00.000Z", "startedAt": "2026-10-01T04:30:00.000Z" }, "message": "Campaign started successfully"}Errors
| Code | When |
|---|---|
| 400 | Status is not DRAFT or PAUSED, the campaign has no contacts, or its date range has ended |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks campaigns:control, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited: 20 control actions per minute per API key (default) |
Pause a campaign
Section titled “Pause a campaign”POST /campaigns/{id}/pause
Pauses an IN_PROGRESS campaign so no new calls are placed. Calls already in progress finish normally. Resume with start.
Scope: campaigns:control
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns/{id}/pause" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "description": "Remind past buyers about the festive offer", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "createdById": "cmf0z9y8x0003user3456qrst", "createdBy": { "id": "cmf0z9y8x0003user3456qrst", "name": "Priya Sharma", "email": "priya@example.com" }, "voiceAgentId": "cmf2b3c4d0002agnt9012mnop", "dialingNumber": "+918071234567", "voiceAgent": { "id": "cmf2b3c4d0002agnt9012mnop", "name": "Order confirmation", "callingNumber": "+918071234567", "telephonyProvider": "plivo" }, "targetCountryCode": "+91", "callStartTime": "10:00", "callEndTime": "19:00", "startDate": "2026-10-01T00:00:00.000Z", "endDate": "2026-10-07T23:59:59.999Z", "timezone": "Asia/Kolkata", "retryCount": 2, "cooldownSeconds": 1800, "maxCallDuration": 300, "inactivityDuration": 30, "status": "PAUSED", "totalContacts": 250, "completedCalls": 0, "failedCalls": 0, "inProgressCalls": 0, "uploadedFileName": null, "uploadedFileUrl": null, "dataExtractionSchema": null, "followUpRound": 0, "followUpRules": null, "createdAt": "2026-09-25T09:30:00.000Z", "updatedAt": "2026-09-25T09:30:00.000Z", "startedAt": "2026-10-01T04:30:00.000Z", "pausedAt": "2026-10-01T06:00:00.000Z" }, "message": "Campaign paused successfully"}Errors
| Code | When |
|---|---|
| 400 | Only in-progress campaigns can be paused (a SCHEDULED campaign cannot) |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks campaigns:control, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited: 20 control actions per minute per API key (default) |
Complete a campaign
Section titled “Complete a campaign”POST /campaigns/{id}/complete
Ends a campaign now. Contacts still waiting are marked SKIPPED, contacts mid-call are marked FAILED (not settled), counters are finalised and the status becomes COMPLETED. It is never dialled again. Works from any status.
Scope: campaigns:control
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns/{id}/complete" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "description": "Remind past buyers about the festive offer", "organisationId": "cmf1a2b3c0000orgx5678ijkl", "createdById": "cmf0z9y8x0003user3456qrst", "createdBy": { "id": "cmf0z9y8x0003user3456qrst", "name": "Priya Sharma", "email": "priya@example.com" }, "voiceAgentId": "cmf2b3c4d0002agnt9012mnop", "dialingNumber": "+918071234567", "voiceAgent": { "id": "cmf2b3c4d0002agnt9012mnop", "name": "Order confirmation", "callingNumber": "+918071234567", "telephonyProvider": "plivo" }, "targetCountryCode": "+91", "callStartTime": "10:00", "callEndTime": "19:00", "startDate": "2026-10-01T00:00:00.000Z", "endDate": "2026-10-07T23:59:59.999Z", "timezone": "Asia/Kolkata", "retryCount": 2, "cooldownSeconds": 1800, "maxCallDuration": 300, "inactivityDuration": 30, "status": "COMPLETED", "totalContacts": 250, "completedCalls": 180, "failedCalls": 62, "inProgressCalls": 0, "uploadedFileName": null, "uploadedFileUrl": null, "dataExtractionSchema": null, "followUpRound": 0, "followUpRules": null, "createdAt": "2026-09-25T09:30:00.000Z", "updatedAt": "2026-09-25T09:30:00.000Z", "startedAt": "2026-10-01T04:30:00.000Z", "completedAt": "2026-10-03T13:30:00.000Z" }, "message": "Campaign completed successfully"}Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks campaigns:control, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 409 | The campaign is busy with a contact upload; retry shortly |
| 429 | Rate limited: 20 control actions per minute per API key (default) |
| 500 | Unexpected failure |
Upload contacts
Section titled “Upload contacts”POST /campaigns/{campaignId}/contacts/upload
Uploads a contact file. By default it appends: numbers already on the campaign are skipped and existing call history is kept. mode=replace deletes every existing contact and their results first, and is only allowed on a DRAFT campaign. Numbers are normalised with the campaign’s country code.
Scope: campaigns:write
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
mode |
query | string: append, replace | no | append (default) keeps existing contacts; replace wipes them first (DRAFT only). |
Body (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | yes | One .csv or .xlsx file, at most 8 MB, sent in the form field “file”. Only the first sheet of a workbook is read and its first row must be the headers. Old Excel 97-2003 .xls files are refused (re-save as .xlsx or .csv). Needs a phone column named phone, phoneNumber, phone_number, number or mobile (case-insensitive); rows with an invalid number are skipped. Optional name column (name, first_name, full_name or participantName) and email column (email or email_address). Every column except the email column is also stored in the contact’s customData under its original header, for use as an agent variable. |
mode |
string: append, replace | no | Alternative to the mode query parameter (the query wins). |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns/{campaignId}/contacts/upload" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 201
{ "success": true, "data": { "inserted": 180, "total": 200, "skippedExisting": 15, "skippedInFile": 5, "skippedDuplicates": 20, "removed": 0, "mode": "append", "totalContacts": 430 }, "message": "180 contacts added, 15 already in this campaign and 5 repeated in your file. This campaign now has 430"}Errors
| Code | When |
|---|---|
| 400 | No file, wrong field name, file over 8 MB, empty file, unsupported type or an old .xls file, no phone column or no valid contacts, unreadable file, bad mode, or upload cut off mid-way |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:write scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 409 | Campaign finished or cancelled, its scheduled dates have passed, replace on a scheduled/running/paused campaign, country changed during upload, or campaign busy with another upload |
| 429 | Rate limited |
Add contacts
Section titled “Add contacts”POST /campaigns/{campaignId}/contacts/append
Adds the numbers in a contact file to the campaign, always in append mode whatever the query or body says. Numbers already on the campaign are skipped and existing call history is kept. Works on draft, scheduled, running and paused campaigns.
Scope: campaigns:write
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Body (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | yes | One .csv or .xlsx file, at most 8 MB, sent in the form field “file”. Only the first sheet of a workbook is read and its first row must be the headers. Old Excel 97-2003 .xls files are refused (re-save as .xlsx or .csv). Needs a phone column named phone, phoneNumber, phone_number, number or mobile (case-insensitive); rows with an invalid number are skipped. Optional name column (name, first_name, full_name or participantName) and email column (email or email_address). Every column except the email column is also stored in the contact’s customData under its original header, for use as an agent variable. |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns/{campaignId}/contacts/append" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 201
{ "success": true, "data": { "inserted": 48, "total": 50, "skippedExisting": 2, "skippedInFile": 0, "skippedDuplicates": 2, "removed": 0, "mode": "append", "totalContacts": 298 }, "message": "48 contacts added, 2 already in this campaign. This campaign now has 298"}Errors
| Code | When |
|---|---|
| 400 | No file, wrong field name, file over 8 MB, empty file, unsupported type or an old .xls file, no phone column or no valid contacts, unreadable file, bad mode, or upload cut off mid-way |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:write scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 409 | Campaign finished or cancelled, its scheduled dates have passed, replace on a scheduled/running/paused campaign, country changed during upload, or campaign busy with another upload |
| 429 | Rate limited |
List campaign contacts
Section titled “List campaign contacts”GET /campaigns/{campaignId}/contacts
Lists a campaign’s contacts with each contact’s last call: connectivity outcome, business result (interested, callback, not interested and so on, with the contact’s own words as evidence), recording link, AI overview and the answers to the campaign’s extraction questions. It works in three modes. Default: newest contacts first, filtered by status, result, outcome and search. lightweight=true: slim rows, most recently updated first, for frequent polling. bucket=
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
page |
query | integer | no | Page number, starting at 1. |
limit |
query | integer | no | Items per page (1 to 500). |
status |
query | string: PENDING, IN_PROGRESS, COMPLETED, FAILED, SKIPPED, PENDING_OR_SKIPPED | no | Only contacts in this dialling status. PENDING_OR_SKIPPED matches both PENDING and SKIPPED. Not allowed together with bucket. |
result |
query | string: interested, callback, not_interested, do_not_call, wrong_person, voicemail, hung_up, no_answer, unclear | no | Only contacts whose last call result is this code: interested, callback, not_interested, do_not_call, wrong_person (backed by a quote), voicemail, hung_up, no_answer (from connectivity), or unclear (a conversation with no evidenced result). Unknown codes are a 400. Not allowed together with bucket. |
outcome |
query | string | no | Only contacts whose saved connectivity outcome is exactly this code (completed, voicemail, hung_up, agent_no_audio, rejected, busy, no_answer, canceled, in_progress, queued, provider_throttled, provider_failed, failed, not_settled, unknown). Contacts settled before outcomes were saved never match. Not allowed together with bucket. |
search |
query | string | no | Free-text filter. Without bucket it matches the contact name (case-insensitive) or part of the phone number. With bucket it also matches the result code, the quote and its English rendering, the names and statuses of actions the agent ran, and the answers to the campaign’s extraction questions. |
lightweight |
query | boolean | no | Send true for slim rows (see the second row shape below), ordered by most recently updated first, with resultCounts returned empty. Only the literal value true switches it on. Not allowed together with bucket. |
bucket |
query | string: all, opportunities, completed, no_answer, rejected, hung_up, busy, voicemail, carrier_rejected, carrier_failed, pending, in_progress, other | no | Results tab to list across the whole campaign: all, opportunities (result interested or callback; overlaps the other tabs), or one connectivity tab. Every contact sits in exactly one connectivity tab. Switches on table mode, which adds bucketCounts and snapshot to the response and enables sort, direction and snapshot. Cannot be combined with status, result, outcome or lightweight (400). Default order without sort: connectivity tab order (completed first, other last), then newest contact first. |
sort |
query | string | no | Table mode only (ignored without bucket). One of name, phone, date (last call time), duration, status, actions, result (interested first, then callback, not_interested, do_not_call, wrong_person, unclear, hung_up, voicemail, no_answer), quote, score, classification, sentiment, or q:<field name> to sort by the answer to one of the campaign’s extraction questions. Empty values always sort last. |
direction |
query | string: asc, desc | no | Sort direction for sort (table mode only). |
snapshot |
query | integer | no | Table mode only. The snapshot value returned with an earlier page; send it with the same bucket, search, sort and direction to read the next pages from that exact list. If the server no longer holds it (it is kept briefly, in the memory of the server that built it) the request fails with 409; start again from page 1 without snapshot. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{campaignId}/contacts" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "contacts": [ { "id": "cmf4l5m6n0004cont7890uvwx", "name": "Rahul Verma", "phoneNumber": "+919876543210", "email": null, "status": "COMPLETED", "attemptCount": 1, "lastAttemptAt": "2026-10-01T05:12:00.000Z", "nextRetryAt": null, "customData": { "phone": "9876543210", "name": "Rahul Verma", "city": "Pune" }, "currentSessionId": null, "lastSessionId": "66f1c2e8a4b5c6d7e8f90123", "callStatus": "Completed", "outcome": { "code": "completed", "detail": null }, "result": { "code": "callback", "quote": "Abhi busy hoon, kal shaam ko call karna", "quoteEn": "I'm busy right now, call tomorrow evening", "settledAt": "2026-10-01T05:15:00.000Z", "callbackAt": "2026-10-02T12:30:00.000Z" }, "callDuration": 94000, "callDurationMs": 94000, "lastCallStartedAt": "2026-10-01T05:12:00.000Z", "lastCallEndedAt": "2026-10-01T05:13:34.000Z", "recordingUrl": "/api/v1/voice-sessions/66f1c2e8a4b5c6d7e8f90123/recording", "egressId": "EG_a1b2c3d4e5", "agentName": "Order confirmation", "participantName": "Rahul Verma", "sessionRoomName": "call-7f3a9c2e", "overview": { "score": 72, "classification": "Relevant", "sentiment": "neutral", "summary": [ "Asked to be called back tomorrow evening." ] }, "actions": { "tracking": "not_recorded", "items": [] }, "bucket": "completed", "extractedCallData": { "roomName": "call-7f3a9c2e", "name": "Rahul Verma", "phone": "+919876543210", "preferred_time": "tomorrow evening", "interested_in_offer": true }, "createdAt": "2026-09-25T09:40:00.000Z", "updatedAt": "2026-10-01T05:15:00.000Z" } ], "total": 1, "totalPages": 1, "currentPage": 1, "resultCounts": { "interested": 64, "callback": 9, "not_interested": 90, "no_answer": 42, "voicemail": 11, "unclear": 5 }, "bucketCounts": { "all": 231, "opportunities": 73, "completed": 168, "no_answer": 42, "voicemail": 11, "busy": 4, "pending": 6 }, "snapshot": 1759654800123 }, "message": "Contacts retrieved successfully"}Errors
| Code | When |
|---|---|
| 400 | Invalid page or limit (1 to 500), unknown result code, invalid status value, unknown bucket or sort column, bad direction, search over 100 or outcome over 50 characters, or bucket combined with status, result, outcome or lightweight |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 409 | Table mode: the pinned snapshot is no longer held by the server (the contact list changed while being read). Start again from page 1 without snapshot. |
| 429 | Rate limited |
List campaign call logs
Section titled “List campaign call logs”GET /campaigns/{campaignId}/call-logs
One row per contact with its latest call attempt (status, outcome, duration, recording link). A lighter view of the contacts list without custom data, business result or AI overview.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
page |
query | integer | no | Page number, starting at 1. |
limit |
query | integer | no | Items per page (1 to 100). |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{campaignId}/call-logs" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "calls": [ { "id": "cmf4l5m6n0004cont7890uvwx", "contactId": "cmf4l5m6n0004cont7890uvwx", "contactName": "Rahul Verma", "phoneNumber": "+919876543210", "status": "COMPLETED", "attemptCount": 1, "lastAttemptAt": "2026-10-01T05:12:00.000Z", "nextRetryAt": null, "callStatus": "Completed", "outcome": { "code": "completed", "detail": null }, "callDuration": 94000, "lastCallStartedAt": "2026-10-01T05:12:00.000Z", "lastCallEndedAt": "2026-10-01T05:13:34.000Z", "currentSessionId": null, "lastSessionId": "66f1c2e8a4b5c6d7e8f90123", "sessionRoomName": "call-7f3a9c2e", "recordingUrl": "/api/v1/voice-sessions/66f1c2e8a4b5c6d7e8f90123/recording", "egressId": "EG_a1b2c3d4e5", "agentName": "Order confirmation", "participantName": "Rahul Verma", "actions": { "tracking": "not_recorded", "items": [] }, "createdAt": "2026-09-25T09:40:00.000Z", "updatedAt": "2026-10-01T05:15:00.000Z" } ], "total": 1, "totalPages": 1, "currentPage": 1 }, "message": "Call logs retrieved successfully"}Errors
| Code | When |
|---|---|
| 400 | Invalid page or limit |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited |
Get campaign business outcomes
Section titled “Get campaign business outcomes”GET /campaigns/{id}/business-results
Counts finished call attempts, including retries and calls by previously assigned agents. Each agent’s outcomes, including the built-in ones every agent has, remain separate. Pending and live calls are excluded. An agent-bound key sees only its own agent’s attempts.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID in the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{id}/business-results" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "calls": 3, "outcomes": [ { "key": "hotel_agent:reservation_confirmed", "label": "Reservation confirmed", "count": 2, "agentId": "hotel_agent", "agentName": "Hotel concierge" }, { "key": "hotel_agent:no_result", "label": "No result", "count": 0, "agentId": "hotel_agent", "agentName": "Hotel concierge" }, { "key": "qualifier_agent:meeting_booked", "label": "Meeting booked", "count": 1, "agentId": "qualifier_agent", "agentName": "Lead qualifier" }, { "key": "qualifier_agent:no_result", "label": "No result", "count": 0, "agentId": "qualifier_agent", "agentName": "Lead qualifier" } ] }}Errors
| Code | When |
|---|---|
| 401 | Authentication required. |
| 403 | Organisation or scope access denied. |
| 404 | Campaign not found. |
Get campaign stats
Section titled “Get campaign stats”GET /campaigns/{id}/stats
Live counters for a campaign computed from its contacts, plus a count per connectivity outcome of each contact’s last attempt.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{id}/stats" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "totalContacts": 250, "completedCalls": 168, "failedCalls": 20, "inProgressCalls": 0, "notAnswered": 42, "hungup": 12, "outcomes": { "completed": 168, "no_answer": 42, "hung_up": 10, "agent_no_audio": 2, "busy": 9, "voicemail": 11, "provider_failed": 8 } }, "message": "Campaign stats retrieved successfully"}Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited |
Check if a campaign can dial now
Section titled “Check if a campaign can dial now”GET /campaigns/{id}/eligibility
Says whether the campaign can place calls right now: it must be IN_PROGRESS and inside its daily call window and date range. When outside the window it returns the next call time.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{id}/eligibility" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "eligible": false, "reason": "Outside call window", "nextCallTime": "2026-10-02T04:30:00.000Z", "minutesUntilNext": 845 }, "message": "Campaign eligibility checked successfully"}Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited |
Get campaign calls with transcripts
Section titled “Get campaign calls with transcripts”GET /campaigns/{campaignId}/calls-data
Every call placed by the campaign (including retries), newest first, with the full transcript, the agent prompt snapshot used on the call, and the recording link.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
page |
query | integer | no | Page number, starting at 1. |
limit |
query | integer | no | Items per page (1 to 100). |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{campaignId}/calls-data" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "campaign": { "id": "cmf3k2j9x0001abcd1234efgh", "name": "Diwali offer reminders", "description": null, "status": "COMPLETED", "totalContacts": 250, "completedCalls": 180, "failedCalls": 62, "inProgressCalls": 0 }, "calls": [ { "sessionId": "66f1c2e8a4b5c6d7e8f90123", "roomName": "call-7f3a9c2e", "participantName": "Rahul Verma", "phoneNumber": "+919876543210", "callStatus": "Completed", "callDuration": 94000, "createdAt": "2026-10-01T05:12:00.000Z", "endedAt": "2026-10-01T05:13:34.000Z", "recordingUrl": "/api/v1/voice-sessions/66f1c2e8a4b5c6d7e8f90123/recording", "egressId": "EG_a1b2c3d4e5", "customVariables": { "participantName": "Rahul Verma", "city": "Pune" }, "actions": { "tracking": "not_recorded", "items": [] }, "agentName": "Order confirmation", "agentIdentity": "You are Nina from Acme Stores.", "agentTask": "Tell the customer about the Diwali offer.", "pronunciation": null, "greetingMessage": "Hi, this is Nina from Acme Stores.", "closingMessage": "Thank you, have a good day.", "escalationMessage": null, "transcripts": [ { "id": "66f1c2e8a4b5c6d7e8f90124", "role": "assistant", "text": "Hi, this is Nina from Acme Stores.", "speaker": { "id": "agent", "name": "Nina" }, "timestamps": { "createdAt": "2026-10-01T05:12:03.000Z", "receivedAt": "2026-10-01T05:12:03.000Z" }, "meta": null } ], "conversations": [], "analytics": null } ], "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 } }, "message": "Campaign calls data retrieved successfully"}Errors
| Code | When |
|---|---|
| 400 | Invalid page or limit |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited |
Start campaign data extraction
Section titled “Start campaign data extraction”POST /campaigns/{id}/extract-data
Starts a background job that runs AI analysis over every completed call of a COMPLETED campaign and aggregates the fields in its dataExtractionSchema. Returns immediately with PROCESSING; poll extraction-status or extracted-data. If an extraction is already running or finished, that one is returned instead of starting a new one.
Scope: campaigns:write
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X POST "https://api.foan.ai/api/v1/campaigns/{id}/extract-data" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "id": "cmf6s7t8u0006extr5678efgh", "campaignId": "cmf3k2j9x0001abcd1234efgh", "extractedData": {}, "processingStatus": "PROCESSING", "processedAt": "2026-10-08T09:00:00.000Z" }, "message": "Campaign data extraction started. Use GET to poll for status and results."}Errors
| Code | When |
|---|---|
| 400 | Campaign not COMPLETED, no extraction schema configured, or extraction disabled in the schema |
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:write scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited |
Get extracted campaign data
Section titled “Get extracted campaign data”GET /campaigns/{id}/extracted-data
Returns the latest extraction for the campaign in any status, so it can be polled. When COMPLETED, extractedData holds the aggregated field values.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{id}/extracted-data" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "id": "cmf6s7t8u0006extr5678efgh", "campaignId": "cmf3k2j9x0001abcd1234efgh", "extractedData": { "interested_in_offer": "62% (112/180)", "preferred_store": "Pune: 40, Mumbai: 31", "successful_calls": 112 }, "processedAt": "2026-10-08T09:03:12.000Z", "processingStatus": "COMPLETED", "confidenceScore": 0.86, "createdAt": "2026-10-08T09:00:00.000Z", "updatedAt": "2026-10-08T09:03:12.000Z" }, "message": "Extracted data retrieved successfully"}Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found, or no extraction has been started for it |
| 429 | Rate limited |
Get extraction status
Section titled “Get extraction status”GET /campaigns/{id}/extraction-status
Lightweight polling endpoint: the status of the campaign’s latest extraction and its error, if any.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/{id}/extraction-status" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": { "status": "PROCESSING" }, "message": "Extraction status retrieved successfully"}Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found, or no extraction has been started |
| 429 | Rate limited |
Get per-call extracted data
Section titled “Get per-call extracted data”GET /campaigns/extracted-call-data/{campaignId}
Returns the per-call data extraction results for a campaign: one flat object per analysed call with the caller’s name and phone plus each extracted field.
Scope: campaigns:read
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | Campaign ID. The campaign must belong to the key’s organisation. |
Example request
curl -X GET "https://api.foan.ai/api/v1/campaigns/extracted-call-data/{campaignId}" \ -H "Authorization: Bearer $FOAN_API_KEY"Response 200
{ "success": true, "data": [ { "roomName": "call-7f3a9c2e", "name": "Rahul Verma", "phone": "+919876543210", "interested_in_offer": true, "preferred_store": "Pune" } ], "message": "Extracted call data retrieved successfully"}Errors
| Code | When |
|---|---|
| 401 | Missing, invalid or revoked API key |
| 403 | Key lacks the campaigns:read scope, or the campaign belongs to another organisation |
| 404 | Campaign not found |
| 429 | Rate limited |
| 500 | Failed to retrieve extracted call data |

