Contacts

The workspace Contacts page — your contact lists, a search for one person across every list, and the do-not-call register — and the API behind it.

The Contacts page holds the people your campaigns call. It sits under Tools & Operations, next to Campaigns, and has three tabs:

  • Lists — every contact list, and each list's contacts.
  • All contacts — find one person by number or name across every list.
  • Do-not-call — the workspace's do-not-call register.

The routes for editing, exporting and searching contacts are listed under The API below.

Lists

The Lists tab shows every contact list with:

  • its number of contacts;
  • the custom value names its contacts carry, read from the list's first 25 contacts (a longer list says so);
  • which campaigns use it;
  • when it last changed.

New list creates an empty list you can add contacts to one at a time or by CSV.

Import CSV is the same import as the campaign wizard: a CSV or an Excel workbook is read in your browser, you map the columns, and every row is checked before the list is created. Every column you do not map becomes a custom value.

A list's contacts

Open a list to see its contacts in import order, 50 per page. Search by name, number or reference, and filter with Show: All contacts, Not called yet, Reached or On do-not-call. Reached means a campaign call reached a person, with a connected_* disposition.

Each row shows the phone number, name, first name, language, time zone, custom values and Last call, the latest campaign outcome for that contact. The first name is the contact's own field, never taken from the name; you can set it when you add or edit a contact, or map a first-name column on import.

Under the name, a row shows the contact's consent basis: why you may call them, Existing customer (existing_customer) or Explicit consent (explicit_consent). The selected contact and the All contacts view show it too. Set it when you add or edit a contact, or map a consent column on import (in the Contacts import and in the campaign wizard). A cell may say either value in any case, with spaces or hyphens ("Existing customer"); a blank cell leaves it unset, and any other value is reported as a row problem rather than imported.

From the list you can:

  • Append rows from a CSV. Rows go to the end of the list; numbers already in the list are skipped.
  • Export the list as a CSV download. Import CSV reads an exported file back as the same contacts, custom values and consent basis included.
  • Add contact one at a time.
  • Edit or Delete the selected contact.
  • Rename the list. Campaigns that use it keep using it under the new name.
  • Delete list, after a confirmation on the page. See Deleting a list.

A contact that a campaign run has already taken can still be edited, but its number cannot change and it cannot be deleted. Deleting a contact keeps the other contacts' import order.

Deleting a list

A list can't be deleted while a campaign that uses it can still call from it: a campaign that is launching, scheduled, running or paused. The page names those campaigns. Cancel them, or wait until they finish, then delete the list. A list is never taken away from a campaign.

When a list is deleted:

  • it no longer appears on the Contacts page, in searches, in the campaign wizard's list picker or in the API, and it can't be imported into or exported;
  • contacts that no campaign has called from are removed;
  • contacts a campaign already took from the list stay in that campaign's records, so its contacts, results export and reviews read exactly as before. They are no longer found as contacts;
  • a draft campaign that used the list needs another list before it can launch.

Preview

When a campaign uses the list, the page previews what that campaign's AI agent says to the selected contact. The preview is rendered with this contact's values by the same code a call uses, so what you see is what the contact hears. Call variables explains how an AI agent declares and uses values.

What it shows depends on the AI agent:

  • An AI agent hosted on Skysay is rendered with the contact's values: the greeting, the script's first line and every script line that uses a value, and its text message templates. A value the contact doesn't have takes the fallback wording written in the template, for example Hi {{#contact.first_name}}{{contact.first_name}}{{/contact.first_name}}{{^contact.first_name}}there{{/contact.first_name}}, and the preview names the values it fell back for. Words the AI agent writes itself in a text message are shown in brackets. If a script reads a value this contact doesn't have, the call takes the script's "case unavailable" route, and the preview says so and shows only the opening. If the contact's values would stop the call being placed, for example a required value is missing, has the wrong type, or makes a line empty or too long, the preview says so and names the value, never its contents.
  • An AI agent whose greeting is generated when the call starts has no greeting to preview.
  • An AI agent on your own endpoint decides what it says, so there is nothing to preview.
  • An AI agent that speaks several languages needs the contact or the campaign to name one. If neither does, the call would not be placed, and the preview says so. The same goes for a language the AI agent doesn't speak.
  • An AI agent that hasn't been published isn't ready for calls, and the preview says so.

If no campaign uses the list, there is no preview.

All contacts

All contacts finds a person by number or name across every list in the workspace:

  • a search that starts with + matches the start of the number;
  • other digits match anywhere in the number;
  • text matches the name, the first name or your reference.

Choose a result to see which lists that number is in, whether it is on the do-not-call register, and its most recent calls and SMS conversations, with links to Calls and SMS.

Do-not-call

The Do-not-call tab is the workspace's do-not-call register. Numbers on it are never called from the workspace: outbound admission refuses them, and queued calls to a number are cancelled when it is added. The register applies to calls only.

  • Add number by typing numbers, or import them from a CSV (the first column is read), with an optional reason.
  • Remove takes a number off the register, after a confirmation.
  • Search the register by number.

Each entry shows when it was added, its reason, and its source:

SourceAdded by
APIPOST /v1/do-not-call
CSV importA CSV import
MCPAn MCP client
AI agent's call outcomeAn AI agent's Record the call outcome tool, before it stopped adding numbers
Script's cease contact stepA script's Cease contact step, before it stopped adding numbers
Skysay supportSkysay support
Contacts pageYou, on this page

Numbers are added by you: on this page, by CSV, or through the API or MCP. Skysay support can also add a number to your list. Nothing is added automatically from what a caller says: an AI agent's Record the call outcome tool and a script's Cease contact step record that the caller asked not to be contacted, on the call, for you to review, and never add the number. Entries they added before that change keep their source.

The API

The generated API reference lists each route's request and response shape. All of them need the calls:read or calls:write scope.

  • PATCH /v1/contact-lists/{contact_list_id} renames a list. Send name only: required, at most 120 characters. Two lists may share a name.
  • DELETE /v1/contact-lists/{contact_list_id} deletes a list, as described in Deleting a list. It returns 409 with reason: contact_list_in_use and up to five of the campaigns that hold the list (id, name, status, with total and has_more). Otherwise it returns removed_contacts and kept_for_campaign_history.
  • PATCH /v1/contact-lists/{contact_list_id}/contacts/{contact_id} edits one contact, including its first_name and consent_basis (an empty value clears it). It is validated like an import row. It returns 409 if the number would duplicate another contact, or if a campaign run holds the contact and the number would change.
  • DELETE /v1/contact-lists/{contact_list_id}/contacts/{contact_id} deletes one contact. It returns 409 if a campaign run holds the contact.
  • GET /v1/contact-lists/{contact_list_id}/export returns the list as CSV with the columns phone_e164, name, first_name, language, time_zone, country, external_reference, consent_basis, then one variables.<name> column per custom value for reading (at most 200), then a variables column with every custom value as JSON, types included. Text that looks like a spreadsheet formula, or that starts with an apostrophe, is prefixed with one apostrophe. The dashboard's Import CSV reads the variables column and removes that apostrophe, so an export imports back exactly. The file streams in import order, 1,000 contacts at a time, so it has no Content-Length. If it fails partway, the download fails rather than ending early. It holds at most 50,000 contacts, the list limit. The row count when the download started and whether the export was truncated are in the X-Skysay-Row-Count and X-Skysay-Truncated headers.
  • GET /v1/contacts?search= finds contacts across every list. It returns a bounded page (limit up to 100) in a stable order, with has_more. An empty search returns nothing.
  • POST /v1/do-not-call accepts an optional source: api (the default), csv, mcp or dashboard.

On this page