OpenPhonexDocs

Campaigns

Call a list of your own customers with a hosted agent inside the hours you choose, with per-outcome retries and a do-not-call register.

A campaign is one contact list, one hosted agent, one or more caller-ID numbers, a set of local calling hours and a per-outcome retry policy. OpenPhonex places every attempt as an ordinary outbound call, so the same policy, wallet, queue and evidence rules apply as for a single POST /v1/calls.

Use a campaign when your list is ready to be called and you want OpenPhonex to own the scheduling. Use a customer application when your own system owns the cadence and must decide each call.

Who you may call

Campaigns are self-serve for two consent bases, declared per campaign:

consent_basisMeaning
existing_customerPeople with an active relationship with your business
explicit_consentPeople who agreed in writing to be called by you

Cold sales, telemarketing, lead generation and purchased, scraped or listed contacts stay blocked platform-wide; a campaign with such a purpose is refused when it is created and again at launch. Campaigns are open to every active workspace -- there is no per-profile campaign switch -- and launch runs one authorization against your workspace capabilities: a sanctioned destination blocks it, a suspended or not-yet-active workspace or a record without voice holds it, a destination outside your allowlist holds it for the usual new-country review, a missing consent basis blocks it, and on the custom implementation-services profile every launch goes to an operator (needs_review.custom_always_review). The full order and reason codes are in Policy & outbound. Your caps (calls per day, per minute, unique destinations, spend) are enforced per call while the campaign runs: the wizard's review step shows each check for your draft and how many days the list takes under the tighter of your daily call cap and your unique-destinations cap -- a 5,000-contact list runs for 25 days under a 200-per-day cap, 34 under the default profile's 150 unique destinations per day.

Contact lists

Create a list and import rows in one request. A row needs phone_e164; every other field is optional.

The workspace wizard builds the same request from a CSV or an .xlsx workbook: it reads the file in your browser — the first sheet, the first row as column names — maps the columns, shows you every row it could not use, and posts the rows this endpoint takes. The API itself never parses a spreadsheet, so a workbook and its CSV export produce the same list and the same per-row report. Date and number cells arrive as the text the sheet displays, not as Excel serial numbers; keep an account reference or a phone number in a text-formatted column, because a spreadsheet stores a number as a 15-significant-digit float and a longer one has already lost digits before any export.

curl -X POST https://api.openphonex.com/v1/contact-lists \
  -H "Authorization: Bearer $OPENPHONEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "organization_id": "org_123",
    "project_id": "proj_123",
    "name": "September renewals",
    "contacts": [
      {"phone_e164": "+15550100", "name": "Ama", "external_reference": "crm-4812",
       "variables": {"plan": "gold", "renewal_date": "2026-09-30"}}
    ]
  }'
  • time_zone defaults from the number's country (+233… becomes Africa/Accra); set it per row when you know better.

  • variables are at most 28 keys, never secrets. They reach the agent as call_context together with the campaign metadata every attempt carries (campaign_id, campaign_run_id, attempt_no, contact_name), and the whole context must fit 32 keys / 4 KB: a row that would not is rejected at import, never at dial time. Those four names are reserved.

  • A variable is a string or a number, or a list of flat objects of them: one debtor, several loans. A list holds at most 16 items of at most 16 keys each; item keys are letters, digits and underscores (a workflow names them in placeholders), values are strings or numbers under 512 characters, true/false is refused, and the 4 KB budget still applies to the whole row. A list is spoken only through a workflow section — see Several obligations on one account.

    {"phone_e164": "+12025550100", "name": "Nimal Perera", "language": "si",
     "variables": {"obligations": [
       {"reference_id": "LN-1", "overdue_amount": "45300.00", "currency": "LKR", "due_date": "2026-08-01"},
       {"reference_id": "LN-2", "overdue_amount": "1200.00", "currency": "LKR", "due_date": "2026-08-28"}
     ]}}

    In the workspace wizard, turn on Several rows per phone number when the sheet has one row per loan: rows sharing a number become one contact, the first row supplies the contact fields, and every row's other columns become one item of that contact's obligations list. Grouped rows are reported as joined, not as duplicates; a seventeenth row for one number is flagged, never dropped. Through the API, aggregate before you send: a second row with the same number in one request is still skipped_duplicates.

  • source records where the rows came from and is returned on the list: api, csv, xlsx, or mcp. It is a label, not a parser — send whichever one is true of your own import. It defaults to api on this endpoint and to mcp through the MCP tool.

  • A row's country is derived from its number; a declared value only fills in for a number no calling code attributes, so a mislabelled row cannot steer the launch-time country check.

  • The response carries a per-row report: accepted, rejected, skipped_duplicates and rejections[] with the row number and reason. Re-uploading the same numbers is idempotent. At most 10,000 rows per request.

Append later with POST /v1/contact-lists/{contact_list_id}/contacts.

Languages

Each contact's language is the language its calls are made in, and the campaign's language is the default for contacts without one. The AI agent must publish that language (multilingual agents): every attempt is admitted against the agent's published profiles and dials with that language's voice stack, so one campaign can carry si, ta and en contacts on one multilingual agent. A contact whose language the agent does not publish is never dialed; it ends pre_dial_failed with skip_reason language_not_published, and a contact with no language on a multilingual agent with no campaign default ends language_required. Both appear in the contact page and the export.

Two things a language does not change: a conversation workflow speaks the text its author wrote, word for word, in whichever language it was written — a workflow campaign that must sound different per language uses one agent (and one campaign) per language; and the calling window is the contact's time zone, not its language.

Do-not-call register

POST /v1/do-not-call with phone_numbers[] adds numbers to the organization register. It is honoured on every outbound path, campaigns and the direct calls API alike, and by every campaign at launch (those contacts are marked and never dialed). A hosted agent adds a number itself with the set_call_disposition tool and the do_not_call outcome, and a workflow cease_contact interrupt does the same. GET /v1/do-not-call lists the register; DELETE /v1/do-not-call/{phone_number} removes one entry.

Create and launch

curl -X POST https://api.openphonex.com/v1/campaigns \
  -H "Authorization: Bearer $OPENPHONEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "organization_id": "org_123",
    "project_id": "proj_123",
    "name": "September renewals",
    "agent_id": "agent_123",
    "from_number_ids": ["num_123", "num_456"],
    "contact_list_id": "clist_123",
    "purpose": "customer_outreach",
    "consent_basis": "existing_customer",
    "time_zone_mode": "contact",
    "time_zone": "Africa/Accra",
    "calling_windows": {"monday": [{"start": "08:00", "end": "19:00"}], "tuesday": [{"start": "08:00", "end": "19:00"}]},
    "retry_policy_preset": "sales",
    "max_concurrency": 2,
    "reserved_concurrency": 1,
    "scheduled_start_at": "2026-09-08T08:00:00Z"
  }'

Then POST /v1/campaigns/{campaign_id}/launch. The response carries authorization.decision (allowed, needs_review or blocked) and reason_code, plus the run and its counters. After launch the audience and authorization are fixed; run settings can still change while the campaign is paused (see Edit a campaign), and POST /v1/campaigns/{campaign_id}/duplicate copies it into a new draft for a new audience. pause, resume and cancel control the run; cancel also cancels the campaign's own attempts that are still queued or held by the live-call gate (a direct POST /v1/calls request that reuses the campaign id as its external_group_reference is not touched).

Calling hours

calling_windows is {weekday: [{start, end}]} in HH:MM, at most four windows a day, never spanning midnight. With time_zone_mode: "contact" each contact's own zone applies and time_zone is the fallback; with "fixed" the one zone applies to everyone. A contact outside its window is deferred to the next open window, never dropped. Defaults follow the destination country (Ghana: 08:00–19:00 Monday to Saturday).

Retries

retry_policy maps a disposition to {retry, backoff_seconds[], max_attempts}, where max_attempts counts the first dial and the last backoff entry repeats. Only attempts that reached the person count: an attempt refused before the line rang because the campaign was paused, the contact's calling window had closed since it was claimed, or the number joined your do-not-call register is deferred (or, for the register, closed as do_not_call) and spends none of the contact's attempts. Presets: sales (no answer ×4 over three days, busy ×3, voicemail ×2), collections (more persistent on no answer), single_attempt. A completed conversation, a wrong party, a refusal, an invalid number or a do-not-call request is never redialed by any policy; a requested callback dials at the requested time.

Concurrency

max_concurrency caps the campaign's simultaneous calls and never exceeds your plan's concurrent-call allowance; reserved_concurrency keeps lines free for inbound and other calls.

Dispositions

Every attempt ends in exactly one disposition, derived from the call's structured termination cause and the agent's own signal:

connected_completed, connected_wrong_party, connected_callback_requested, connected_refused, no_answer, busy, voicemail, invalid_number, carrier_rejected, do_not_call, cancelled, failed_technical.

A hosted agent declares the connected outcomes with the set_call_disposition builtin tool (a callback requires callback_at). Telephony outcomes come from the SIP result and are never model-supplied.

Read progress

  • GET /v1/campaigns/{campaign_id} returns the campaign, its latest run and contact counters (pending, scheduled, dispatched, completed, failed, skipped, pre_dial_failed, do_not_call, audience, remaining).
  • GET /v1/campaigns/{campaign_id}/contacts?status= pages through the run's contacts with disposition, attempt count, next attempt and last call id, which opens the ordinary call evidence; GET /v1/campaigns/{campaign_id}/results adds each contact's last-call business outcome.
  • Webhooks: campaign.launched, campaign.contact.completed, campaign.completed. Per-call events are unchanged and carry external_reference (the contact's reference) and external_group_reference (the campaign id).

Edit a campaign

A draft (or a campaign held for review) accepts every setting through PATCH /v1/campaigns/{campaign_id}, the same validation as create. After launch the audience and the authorization are fixed: agent, contact list, purpose and consent basis cannot change (duplicate the campaign for a new audience). While a campaign is scheduled or paused you can still change:

  • name, from_number_ids and language;
  • calling_windows, time_zone_mode and time_zone. Contacts that were waiting for the old window are re-evaluated on the next dispatcher tick; retries waiting on a backoff keep their time;
  • retry_policy or retry_policy_preset, max_concurrency and reserved_concurrency;
  • ends_at, and scheduled_start_at while the run has not started.

A draft may also receive a new audience in the same request: send contacts (the same rows as a contact-list import) and an optional contact_list_name and the list is created inside the edit, so a status that locks the audience refuses before any list exists.

A running campaign must be paused first. Every campaign carries editable_fields for its current status, and the PATCH response adds changed, the settings that actually changed. A setting that is locked in the current status answers 409, as does a status change that raced the edit (pause, resume, cancel or launch): reload and try again. In the workspace, Edit on a campaign opens the same wizard with the locked steps disabled; Duplicate opens the copy in the wizard before it is launched.

Export results

GET /v1/campaigns/{campaign_id}/export downloads the latest run as a CSV attachment with a fixed column set per contact: phone_e164, name, external_reference, language, time_zone, status, disposition, attempt_count, next_attempt_at, callback_at, skip_reason, last_call_id, last_call_status, termination_cause, termination_detail and updated_at, then the last call's business outcome (below). Rows follow the list order and cover the whole run up to the list cap (50,000 rows); X-OpenPhonex-Row-Count carries the count and X-OpenPhonex-Truncated says whether rows beyond the cap were left out. phone_e164 is written verbatim: import that column as text in a spreadsheet so the leading plus survives. The export is bound to the credential's own workspace (a caller-selected organization_id is ignored). Uploaded variables are not echoed back, and free-text cells that start with a formula character are prefixed so a spreadsheet does not execute them. The workspace offers the same file as Export CSV on the campaign.

Business outcomes

Every exported row carries what the contact's last call concluded, read from the call's own evidence:

  • workflow_status, workflow_outcome_type (review_candidate, informational or terminal_note), workflow_outcome (the outcome step the conversation workflow landed on, for example promise_review) and workflow_outcome_label (that step's label);
  • workflow_interrupt: the global interrupt that ended the conversation, if one did (wrong_party, cease_contact, dispute, hardship, callback, human_request, language_change);
  • extraction_status (empty when no extraction was requested for the call; otherwise pending, running, ready or failed), extraction_schema_version (the schema version the row's cells were extracted under; the column layout follows the AI agent's current schema) and one extraction.<field> column per field of the AI agent's post-call extraction schema, filled only when the status is ready. Extraction is advisory model output; an empty cell means the field was not extracted, never that it was extracted as empty;
  • recording_available and transcript_available (true/false): whether the call's recording and transcript exist, which GET /v1/calls/{call_id}/recording and the call evidence serve.

A contact that was never dialed carries the empty outcome. Booleans are written as true/false; the outcome label and every extraction cell are formula-guarded like the other free text.

GET /v1/campaigns/{campaign_id}/results?status=&limit=&offset= is the same information as JSON: the contact page of GET /v1/campaigns/{campaign_id}/contacts with an outcome object per contact (the fields above, extraction as an object, extraction_schema_version) and extraction_fields, the AI agent's current field list. Bounded and paged like the contact page (has_more).

Review inbox

A conversation workflow ends most calls that need a person on a review_candidate outcome: a promise to pay, a payment already made, a dispute, hardship, a callback request, a language request, a wrong party. The review inbox lists those attempts for a campaign's latest run with the call's evidence inline, so a reviewer can decide and record what they did.

  • GET /v1/campaigns/{campaign_id}/reviews?state=open|resolved|all&outcome=&language=&limit=&offset= pages through the candidates, newest first. Each item carries the attempt (attempt_id, attempt_no, call_id, disposition, termination_cause, settled_at), the contact (phone_e164, name, external_reference, language, status), the call's outcome (the business outcome above, including the extraction fields and the recording/transcript availability) and its resolution (null while open; id, note, resolved_at and resolved_by once resolved). counts gives the open and resolved totals; outcomes (outcome step id, label, count) and languages are the facets over the whole candidate set, whatever the page's filters; extraction_fields is the field list.
  • POST /v1/campaigns/{campaign_id}/reviews/{attempt_id}/resolve with an optional note (at most 2,000 characters) resolves one candidate. The platform records who resolved it (the signed-in user, or the API key) and when, writes a campaign.review.resolved audit event with the campaign, call, workflow run, outcome and note, and returns the item as the inbox lists it. A candidate resolved before answers 409 and keeps the first reviewer's note; an attempt that is not a review candidate of this campaign answers 404.
  • GET /v1/campaigns/{campaign_id}/reviews/export?state=resolved|open|all downloads the set as CSV: the contact, the attempt, the business-outcome and extraction columns of the results export, then resolved_at, resolved_by and note. Bounded and bound to the credential's own workspace like the results export.

In the workspace, a campaign shows Review inbox (N) with the number of candidates still open; the inbox page filters by state, outcome and language, plays the recording, links the transcript, shows the extracted fields, and resolves each item with a note. The inbox covers the campaign's latest run.

The same operations are available over MCP as create_contact_list, add_contacts, add_do_not_call, create_campaign, update_campaign, launch_campaign, pause_campaign, resume_campaign, cancel_campaign, list_campaigns and get_campaign. The CSV exports, the results page and the review inbox are REST-only.

On this page