> For the complete documentation index, see [llms.txt](https://help.ringtonic.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.ringtonic.app/guides/api.md).

# API

API Keys let you access Ring Tonic data programmatically. Use them to build custom integrations, sync call data with your CRM, or power dashboards.

{% hint style="info" %}
API Keys are available on the **Agency plan** only.
{% endhint %}

{% hint style="success" %}
**Want an AI assistant to use Ring Tonic for you instead?** Connect Claude, Cursor, or Windsurf with an [MCP key](/guides/mcp.md) and ask in plain language — same data, no code.
{% endhint %}

***

### Creating an API Key

1. Go to **Settings** → **API Keys**
2. Enter a descriptive name (e.g., "SEO Utils Integration")
3. Choose the key's scopes — pick the **Read-only** preset, **Full access**, or toggle individual scopes yourself
4. *(Optional)* Set an **Expiration**: Never, 30, 90, or 365 days
5. Click **Create API Key**
6. Copy your key immediately — you won't see it again

<figure><img src="https://2905537093-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FXSbXRgC7knjpaMCHwuKc%2Fuploads%2FYQw0xlommmA1asMv4Vx7%2FXnapper-2025-12-29-19.42.53.png?alt=media&amp;token=f53b4a7f-d2e9-4927-afe0-258737d0d1d5" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
**Store your key securely.** Treat it like a password. Anyone with your key can access your data.
{% endhint %}

{% hint style="info" %}
**Only grant the scopes an integration actually needs.** A dashboard that displays call data needs read scopes alone — giving it write scopes means a mistake in that integration can change or delete your records.
{% endhint %}

***

### Using Your API Key

Include the key in the `Authorization` header of all requests:

```bash
curl "https://ringtonic.app/api/v1/campaigns" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
```

***

### Managing Keys

| Action        | How                            |
| ------------- | ------------------------------ |
| View keys     | **Settings** → **API Keys**    |
| See last used | Check the "Last Used" column   |
| Revoke key    | Click the trash icon → Confirm |

{% hint style="danger" %}
**Revoking is permanent.** Any integrations using that key will immediately stop working.
{% endhint %}

***

### Available Endpoints

Full interactive documentation — every endpoint with request and response schemas, plus a built-in "Try It" console — lives here:

{% embed url="<https://ringtonic.app/docs/api>" %}

You can **read** campaigns, call logs (with recordings and transcriptions), contacts, conversions, tracking numbers, call flows, tags, webhook endpoints, form submissions, blocked numbers, products & services, and appointments.

***

### Creating and Changing Data

The API can now build and maintain your account, not just read from it. Onboarding a new client no longer has to be a session in the dashboard — it can be a script.

| What you can manage | Create | Update | Delete |
| ------------------- | ------ | ------ | ------ |
| Campaigns           | ✅      | ✅      | ✅      |
| Call flows          | ✅      | ✅      | ✅      |
| Tracking numbers    | ✅      | ✅      | ✅      |
| Tags                | ✅      | ✅      | ✅      |
| Products & services | ✅      | ✅      | ✅      |
| Webhook endpoints   | ✅      | ✅      | ✅      |
| Appointments        | ✅      | ✅      | —      |
| Blocked numbers     | ✅      | —      | ✅      |
| Call logs           | —      | ✅      | ✅      |
| Contacts            | —      | —      | ✅      |

A few actions go beyond simple create and update:

* **Attach or detach a call flow** on a campaign, to change how its calls are answered
* **Tag a campaign** when you create or update it, and **list campaigns by tag**
* **Duplicate a call flow**, to reuse a working setup for another client
* **Rotate a webhook signing secret**, or **send a test event** to confirm your endpoint is reachable
* **Book, reschedule, cancel, or mark an appointment** completed or no-show — the same booking engine the AI agent and the dashboard use
* **Place a call** that rings one of your agents first, then the customer — see [Click-to-Call](#click-to-call) below

{% hint style="info" %}
**Campaign tags use the workspace's existing tags.** Name each tag by its ID or exact name. A tag that doesn't exist yet is refused and nothing is saved, so create it with the tags endpoint first. The tags you send replace the campaign's whole tag list: include the ones it should keep, or send an empty list to remove them all. Every campaign the API returns lists its tags.
{% endhint %}

{% hint style="warning" %}
**Deleting a call flow changes how calls are answered.** Every campaign using that flow is detached and falls back to its simple forwarding settings. Check which campaigns use a flow before removing it.
{% endhint %}

{% hint style="success" %}
**Call flows and phone numbers can now be managed end to end.** You can design a flow, publish it, and buy the number that answers it without opening the dashboard. Both are covered in their own sections below.
{% endhint %}

***

### Building Call Flows

A call flow is a set of steps — greet the caller, check business hours, ring a team, take a message — connected together. The API works with the same flows the [Call Flow Builder](/guides/call-flow-builder.md) produces, so you can design one by hand and then reproduce it for every client with a script.

Each flow has two copies: the **draft** you are editing, and the **published** version that is answering calls. Writing to a flow only ever changes the draft.

{% stepper %}
{% step %}

#### Create the flow

Create a flow with its starting steps, or with none at all to begin from a blank flow.
{% endstep %}

{% step %}

#### Edit the draft

Send the steps and the connections between them. Callers are unaffected — the published version keeps answering.
{% endstep %}

{% step %}

#### Publish

Publishing takes a snapshot of the draft and puts it into service for every campaign using that flow.
{% endstep %}

{% step %}

#### Roll back if needed

Rolling back restores an earlier version **into the draft**. Publish again to actually return that version to service.
{% endstep %}
{% endstepper %}

{% hint style="warning" %}
**A flow is written whole, not step by step.** There is no way to add or remove a single step: whatever you send becomes the draft, so a step you leave out is removed along with its connections.

Always read the flow first, change the parts you want, and send the whole thing back. Building the request from scratch for a small edit will delete the rest of the flow.
{% endhint %}

{% hint style="info" %}
**Drafts are checked as though you were publishing them.** The builder lets you save half-finished work because you can see it on the canvas; a script cannot. So an incomplete flow is refused straight away, naming the step and the setting at fault, rather than being accepted and failing later when you publish.
{% endhint %}

<details>

<summary>Working safely when more than one thing edits a flow</summary>

* Read, change, and write back as one operation, then publish
* Two publishes of the same flow at the same time will not both apply — one succeeds and the other is asked to try again
* A flow that contains AI-generated voice audio publishes in the background, so check its version list to confirm the new version went live
* If you send back a webhook step's hidden signing secret exactly as you received it, the stored secret is kept — so a read-and-write round trip never destroys your credentials

</details>

***

### Buying and Releasing Numbers

Onboarding a client can now include getting them a number. The API can search what is available, buy one, point it at a campaign, and give it back when the client leaves.

{% stepper %}
{% step %}

#### Search what is available

Search by country, and narrow it down by area code, the digits the number contains, whether it is local, toll-free or mobile, and whether it needs to handle calls, texts or both. Prices come back per number type, so a toll-free number is quoted at its own rate.
{% endstep %}

{% step %}

#### Buy a number

Buy a specific number from those results, or give the criteria and let Ring Tonic pick the first match. You can assign it to a campaign and give it a label in the same request.
{% endstep %}

{% step %}

#### Release it when you are done

Releasing gives the number back to the carrier and stops the monthly charge for it.
{% endstep %}
{% endstepper %}

{% hint style="danger" %}
**Releasing a number is permanent.** The number returns to the carrier's pool, and anyone can buy it next. Calls to it stop reaching you immediately, and existing marketing that still shows the number will ring a stranger. If you only want to stop using a number on a campaign, unassign it instead — it stays in your inventory.
{% endhint %}

{% hint style="info" %}
**Numbers you already own in Twilio are imported from the dashboard.** Go to **Phone Numbers** → **Import from Twilio** to bring existing numbers into a workspace; the API buys new ones rather than adopting numbers already on your account.
{% endhint %}

<details>

<summary>What happens if a purchase fails half way through</summary>

Buying a number happens at the carrier before Ring Tonic can record it, so the two steps cannot be completed as one.

1. If the carrier sells the number but Ring Tonic cannot record it, the purchase is undone — the number is given straight back rather than left on your bill with nothing in your account to show for it.
2. If a purchase takes you past your daily cap, that number is released again and the response tells you the cap was reached.
3. If a request times out and you are unsure whether it went through, retry it with the same **Idempotency-Key** and you will get the original answer rather than a second number.

</details>

***

### 📅 Booking Appointments

Ring Tonic can book and manage appointments from your own tools — a scheduler, a CRM, or a script — using the same booking engine the AI agent and the dashboard use. A booking made this way shows up in the [Appointments](/guides/appointments.md) hub, syncs to Google Calendar when it's connected, and is attributed back to the call it came from.

This needs the **appointments** write scope on the key. Booking for a brand-new caller — someone who isn't a contact yet — additionally needs the **contacts** write scope, because it creates a contact record.

{% stepper %}
{% step %}

#### Book an appointment

Give the time, how long it runs, and who it's for — either an existing contact or a new one (a name and phone number). Add a title, notes, and a location, and turn on a text or email confirmation and reminders if you want them.
{% endstep %}

{% step %}

#### Reschedule or edit

Change the time, the length, or any detail. Send only what changes — anything you leave out stays as it was.
{% endstep %}

{% step %}

#### Cancel

Cancel the appointment with an optional reason, and choose whether to let the customer know. A cancelled appointment stays on record; it is not deleted.
{% endstep %}

{% step %}

#### Mark the outcome

Mark it **completed** or **no-show** once it has happened, or re-open it to **scheduled** if it was cancelled by mistake.
{% endstep %}
{% endstepper %}

A booking request looks like this:

```bash
curl -X POST "https://ringtonic.app/api/v1/appointments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 9b1f2c84-0d6a-4e21-9f77-c3a2b6e40d15" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_uuid": "8d2c7a10-4f19-4b83-9c2e-1a6f0b3d5e77",
    "start_at": "2026-08-20T18:00:00Z",
    "duration_minutes": 30,
    "title": "Roof inspection"
  }'
```

{% hint style="info" %}
**The start time is a full timestamp with a timezone.** Send an exact instant such as `2026-08-20T18:00:00Z` (the `Z` means UTC) or with an offset like `2026-08-20T14:00:00-04:00`. A time without a zone is refused, so there is never any doubt about which moment you mean.
{% endhint %}

{% hint style="warning" %}
**A clashing time warns, but does not block.** If the slot overlaps something already booked, the request comes back with a conflict you can act on. To book anyway, send it again with `ignore_conflicts` turned on — and, because it creates a record, a fresh Idempotency-Key.
{% endhint %}

{% hint style="info" %}
**Confirmations and reminders are off unless you ask for them.** Turn on a text or email confirmation per booking, and add reminder times. A text only sends if the workspace has a text-capable number and the contact has a mobile; an email only sends if the contact has an email address.
{% endhint %}

<details>

<summary>Working with Google Calendar</summary>

* With Google Calendar connected, a booking creates the calendar event, a reschedule moves it, and a cancel removes it
* Without it, appointments still work — they simply stay inside Ring Tonic
* Connecting Google Calendar later does not back-fill appointments booked while it was disconnected

</details>

***

### 📞 Click-to-Call

Put a **Call** button in your own app or CRM. Ring Tonic rings your agent's phone first. When the agent presses **1**, it calls the customer from one of your tracking numbers. The call is recorded when its campaign records calls, and it appears in your call logs like any other call.

{% hint style="warning" %}
**Each click-to-call is two real calls** — one to the agent and one to the customer — billed by Twilio or SignalWire on your own account at their usual rates.
{% endhint %}

**Before you start:**

* An API key with the **calls:place** scope. It is listed under **Sensitive scopes** and is not part of **Full access**, so tick it yourself.
* A tracking number with calling turned on, on Twilio or SignalWire.
* A verified phone for each agent who takes these calls (below). Owners, admins and agents can verify one.

<figure><img src="https://2905537093-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FXSbXRgC7knjpaMCHwuKc%2Fuploads%2Fgit-blob-473f01a9da505c43df1431758109cae9400b682a%2Fapi-click-to-call-scope.png?alt=media" alt="The New API key form with the calls:place scope ticked under Sensitive scopes"><figcaption><p>The calls:place scope sits under Sensitive scopes, outside Full access</p></figcaption></figure>

#### Verify an agent's phone

The API never sends the agent's number. Each agent proves once that they hold the phone they answer calls on, and only that phone can be rung.

{% stepper %}
{% step %}

#### Open your profile

Sign in as the agent and go to **Settings** → **Profile**. The **Click-to-call phone** card is below your profile details.
{% endstep %}

{% step %}

#### Ask for a code

Enter the phone you answer calls on, in international format (for example +61 412 345 678), and click **Call me with a code**. The phone rings from one of the workspace's numbers and reads out a 6-digit code twice.

<figure><img src="https://2905537093-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FXSbXRgC7knjpaMCHwuKc%2Fuploads%2Fgit-blob-127a1be096372bb879e2680f5f147bb67896e8d9%2Fapi-click-to-call-code.png?alt=media" alt="The Click-to-call phone card asking for the 6-digit code"><figcaption><p>Enter the code the call read out</p></figcaption></figure>
{% endstep %}

{% step %}

#### Enter the code

Type the code and click **Verify**. The card shows your number with a **Verified** badge.

<figure><img src="https://2905537093-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FXSbXRgC7knjpaMCHwuKc%2Fuploads%2Fgit-blob-69020b1f30709420e3d7a2b85a04b71da64c1af9%2Fapi-click-to-call-verified.png?alt=media" alt="The Click-to-call phone card showing a verified number"><figcaption><p>A verified phone, ready for click-to-calls</p></figcaption></figure>
{% endstep %}
{% endstepper %}

{% hint style="info" %}
**Changing your number?** Click **Change** and verify the new one. Your verified phone keeps ringing until you enter the new number's code, so a typo never leaves you unreachable.
{% endhint %}

#### Place a call

Send the customer's number, the tracking number to call from, and the agent's email:

```bash
curl -X POST "https://ringtonic.app/api/v1/calls" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 3c9a7e21-5b4f-4d8a-8e16-2f0b9c7d4a51" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+61298765432",
    "tracking_number_uuid": "1f6d2b90-3c4e-4a7b-9e21-8d5c0f7a3b64",
    "agent_email": "agent@example.com"
  }'
```

The response returns the new call straight away, in the same shape as a call log. Then:

1. The agent's phone rings, showing the tracking number.
2. The agent hears who the call is for — and what the lead asked for, when you [send the lead's details](#brief-the-agent-with-the-leads-details) — and presses **1**. Any other key, or no answer, ends the call — the customer is never dialed.
3. The customer's phone rings, showing the tracking number. When the campaign records calls, its recording announcement plays first.

Follow the result with the call logs endpoint or the [webhooks](/guides/webhooks.md):

| What happened                                | Call status     | Webhook          |
| -------------------------------------------- | --------------- | ---------------- |
| The customer answered                        | Completed       | `call.completed` |
| The customer did not answer or was busy      | No answer, Busy | `call.missed`    |
| The agent did not pick up or did not press 1 | Canceled        | `call.missed`    |

When the request itself is refused — the provider will not call that number, for example — the error comes back in the response and no webhook is sent.

{% hint style="warning" %}
**Always send an Idempotency-Key.** Without one, a retry after a timeout rings the agent a second time.
{% endhint %}

<details>

<summary>Limits on placing calls</summary>

* One call per agent at a time, and at most 10 at once per workspace. A request over either limit is refused; try again when a call ends.
* At most 10 calls placed per minute per workspace. Verification calls count toward this too.
* Any country your Twilio or SignalWire account is allowed to call. Numbers on your block list and your own tracking numbers are refused.

</details>

#### Brief the agent with the lead's details

When the call comes from a form on your website, send what the visitor filled in. Before pressing **1**, the agent hears:

> "You have a click to call from Marcus Webb, about Office coffee subscription. Wants weekly whole-bean delivery for a 40-person studio and a grinder pick, call before Friday. Press 1 to call Marcus Webb, …"

Add any of these optional fields to the request:

```json
{
  "to": "+61298765432",
  "tracking_number_uuid": "1f6d2b90-3c4e-4a7b-9e21-8d5c0f7a3b64",
  "agent_email": "agent@example.com",
  "lead_name": "Marcus Webb",
  "lead_topic": "Office coffee subscription",
  "lead_message": "We're a 40-person design studio. Looking for a weekly whole-bean delivery and a grinder recommendation. Could someone call me before Friday?"
}
```

| Field          | What the agent hears                                  | Limit            |
| -------------- | ----------------------------------------------------- | ---------------- |
| `lead_name`    | Read as written                                       | 100 characters   |
| `lead_topic`   | Read as written — for example, the service they chose | 100 characters   |
| `lead_message` | Never read aloud. AI sums it up in one short sentence | 2,000 characters |

{% hint style="info" %}
**The AI summary uses your workspace's OpenAI key**, the one under **Settings → Workspaces →** your workspace **→ AI Automation** ([how to add it](https://help.ringtonic.app/guides/pages/crg7tMxXMDacQLAiSpgl#id-4.-set-up-ai-automation)). OpenAI bills it to your account, a fraction of a cent per call. Without an OpenAI key, the agent hears the name and topic only.
{% endhint %}

The summary is written while the agent's phone rings, so the call never waits for it. If the agent picks up before it is ready, they hear the name and topic, then the **Press 1** prompt.

Everything you sent, and the summary, stays with the call. Open the call in **Analytics → Call Activity** to see the **Click-to-call request** section.

<figure><img src="https://2905537093-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FXSbXRgC7knjpaMCHwuKc%2Fuploads%2Fgit-blob-7b5229837629d74a3e1701616542218ac8886967%2Fapi-click-to-call-lead-details.png?alt=media" alt="The Click-to-call request section of a call, showing the lead&#x27;s name, topic, AI summary and full message"><figcaption><p>The lead's request on the call: the AI summary (1) and the full message (2)</p></figcaption></figure>

***

### Safe Retries

If a request times out, you rarely know whether it succeeded. Send it again and you risk creating the same campaign twice.

To avoid that, include an **Idempotency-Key** header on any request that creates something. Choose a unique value per action — a UUID works well:

```bash
curl -X POST "https://ringtonic.app/api/v1/campaigns" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 7f3c1e94-2b6a-4d5e-9a10-c8f2b7d41e63" \
  -H "Content-Type: application/json" \
  -d '{"name": "Spring Promo", "tracking_type": "static", "forward_to": "+14155550123"}'
```

Retry with the same key and you get the original response back instead of a second campaign. Use a new key for each genuinely new action.

***

### Limiting What the API Can Spend

Workspace owners can cap how many phone numbers the API is allowed to buy per day, so an integration with a bug can't run up a bill overnight.

{% stepper %}
{% step %}

#### Open your workspace settings

Go to **Settings** → **Workspace**. The **API Access** section appears near the bottom of the **Basic** tab.
{% endstep %}

{% step %}

#### Set the daily cap

Enter a number between 0 and 100. Setting it to **0** prevents the API from buying numbers at all.

<figure><img src="https://2905537093-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FXSbXRgC7knjpaMCHwuKc%2Fuploads%2F5eEuPZSpWoiU3l5N5FHz%2FXnapper-2026-07-29-22.59.54.png?alt=media&amp;token=e6d238d0-2a56-4d20-9f58-e1a76406ca23" alt=""><figcaption><p>The API Access section in workspace settings, showing the daily purchase cap field</p></figcaption></figure>
{% endstep %}
{% endstepper %}

{% hint style="info" %}
Only the **workspace owner** can change this — team members with admin access cannot.

Numbers that were bought and later released still count toward the day's total, because they were paid for. The count resets at midnight in your workspace's timezone.
{% endhint %}

***

### Common Questions

<details>

<summary>How many API keys can I create?</summary>

Unlimited. Create separate keys for each integration so you can revoke them independently.

</details>

<details>

<summary>Do API keys expire?</summary>

By default, no — keys stay active until you revoke them. When creating a key you can optionally set it to expire after 30, 90, or 365 days, and it will stop working automatically once that period passes.

</details>

<details>

<summary>What happens if my key is compromised?</summary>

Revoke it immediately from **Settings** → **API Keys** and create a new one.

</details>

<details>

<summary>Can I stop an integration from deleting things?</summary>

Yes. Scopes are granted per key, and read and write are separate.

* Give a reporting dashboard only the **read** scopes it needs
* Give a provisioning script the **write** scopes for the resources it manages, and nothing else
* Create a separate key per integration, so you can revoke one without breaking the others

</details>

<details>

<summary>Can the API book an appointment for someone who isn't a contact yet?</summary>

Yes. Send the new person's name and phone number instead of an existing contact, and a contact record is created for them.

Because that creates a contact, the key needs the **contacts** write scope on top of the **appointments** write scope. A key with only appointments access can still book, but only for contacts that already exist.

</details>

<details>

<summary>Does deleting through the API also delete the call recordings?</summary>

Deleting a call log removes the call and its recording and transcription, exactly as deleting it in the dashboard does. This cannot be undone, so confirm you have exported anything you need first.

</details>

<details>

<summary>Why did my request come back with an error I did not expect?</summary>

Errors include a short machine-readable `type` and a human-readable explanation, so you can tell an invalid value apart from a missing permission or a record that belongs to another workspace.

Common causes:

1. **The key is missing a scope.** Check the key's scopes in **Settings** → **API Keys**.
2. **The record belongs to another workspace.** A key only ever sees the workspace it was created in.
3. **A value failed validation.** The response names the field and what was wrong with it.

</details>

<details>

<summary>I edited one step and the rest of my flow disappeared — why?</summary>

A flow is written whole. The request replaces the draft with exactly what you send, so any step missing from it is removed.

To change one step:

1. Read the flow first
2. Change that step in what you read
3. Send the whole thing back

Building the request from scratch, listing only the step you wanted to change, deletes everything else. If this has already happened, roll the flow back to the last published version and publish again.

</details>

<details>

<summary>Can I cap what the API is allowed to spend?</summary>

Yes — workspace owners set a daily limit on how many numbers the API may buy. See [Limiting What the API Can Spend](#limiting-what-the-api-can-spend) above.

* The default is 10 numbers per day
* Setting it to **0** stops the API buying numbers entirely
* Numbers bought and then released still count toward the day, because they were paid for

</details>

<details>

<summary>I can't see the Click-to-call phone card — why?</summary>

The card appears only when both are true:

* The workspace is on the **Agency plan**, because click-to-call is part of the API
* You are an **owner, admin or agent** in that workspace — members who cannot take calls do not get one

The card is per workspace. Switch to the workspace you take calls in before you open **Settings** → **Profile**.

</details>

<details>

<summary>Can two integrations safely write at the same time?</summary>

Yes. Requests are handled independently and your workspace is never left half-updated by a failed call.

Two behaviours worth knowing:

* Actions that create something accept an **Idempotency-Key**, so retries after a timeout do not duplicate records
* Where two requests genuinely conflict — publishing the same call flow twice at once, for example — one succeeds and the other is told to try again, rather than both partially applying

</details>
