---
title: "REST API reference"
description: "Every TalaLevel REST endpoint: leads, notes, texts, calls, emails, lists, pipelines, deals, sending, batches and AI caller runs, with bodies and answers."
url: https://talalevel.com/docs/api/
date: 2026-11-12
updated: 2026-11-12
---

# REST API reference

The base is https://app.talalevel.com/api/v1. Leads, notes, lists and deals can be read and changed; a lead's texts, calls and emails can be read; texts, emails, batches, waves and AI caller runs can be sent by a key with send. The same reference is at /openapi.json as OpenAPI 3.1.

Every endpoint below is under `https://app.talalevel.com/api/v1` and takes a workspace key as `Authorization: Bearer tl_live_…`. Requests and answers are JSON, times are ISO 8601 in UTC, and money is whole cents. Each endpoint names the [permission](https://talalevel.com/docs/authentication/#permissions) its key needs, as `area: level`. The same reference is in [openapi.json](https://talalevel.com/openapi.json).

**Base URL**

```text
https://app.talalevel.com/api/v1
```

## Leads

A lead is a business or a person you are selling to, with its contacts. Its `status` is one of your workspace's own statuses, and `fields` are your custom fields.

#### The lead object

- `id` *integer*
- `name` *string* The business, or the person when there is no business.
- `status` *string* One of your workspace's statuses.
- `email`, `city`, `state` *string or null* `state` is two letters.
- `time_zone` *string or null* An IANA zone, worked out from the lead's place and numbers.
- `contacts` *array* Each with a `name`, its `phones` (E.164, in dial order) and its `emails`.
- `fields` *object* Your custom fields by name.
- `owner` *object or null* The person on your team who owns the lead.
- `starred` *boolean*
- `updated_at` *string* When it last changed.
- `url` *string* The lead's page in the app.

**The lead object**

```json
{
  "id": 123,
  "name": "Reed & Co Insurance",
  "status": "Call booked",
  "email": "marcus@reedco.example.com",
  "city": "Austin",
  "state": "TX",
  "time_zone": "America/Chicago",
  "contacts": [
    {
      "name": "Marcus Reed",
      "phones": [{ "phone": "+15125550147", "type": "mobile" }],
      "emails": []
    }
  ],
  "fields": { "Team size": "12", "Source": "Website form" },
  "owner": { "id": 4, "name": "Dana" },
  "starred": true,
  "updated_at": "2026-11-12T09:37:45.678Z",
  "url": "https://app.talalevel.com/leads/123"
}
```

### List leads

`GET /leads`

Leads in the order they last changed, oldest first. Call again with `next_cursor` while `has_more` is true; once it is false, keep `next_cursor` and pass it on your next poll to get only what changed since. A change is listed 30 seconds after it happens, so a lead still being saved is never skipped. Needs `leads: read`.

#### Parameters

- `cursor` *string* Where to carry on from: the last answer's `next_cursor`.
- `limit` *integer* How many to answer with. 50 when left out.
- `status` *string* Only leads with this status, in any case.

#### Returns

`leads`, `next_cursor` and `has_more`.

**Request**

```sh
curl "https://app.talalevel.com/api/v1/leads?limit=50" \
  -H "Authorization: Bearer $TALALEVEL_KEY"
```

**Response**

```json
{
  "leads": [{ "id": 123, "name": "Reed & Co Insurance", "status": "Call booked" }],
  "next_cursor": "MjAyNi0xMS0xMlQwOTozNzo0NS42Nzg…",
  "has_more": false
}
```

### Get a lead

`GET /leads/{id}`

One lead. Another workspace's lead answers 404, the same as one that does not exist. Needs `leads: read`.

**Request**

```sh
curl https://app.talalevel.com/api/v1/leads/123 \
  -H "Authorization: Bearer $TALALEVEL_KEY"
```

**Response**

```json
{ "lead": { "id": 123, "name": "Reed & Co Insurance", "status": "Call booked" } }
```

### Create a lead

`POST /leads`

Makes a lead. It needs a phone or an email. A phone already on a lead in the workspace is a 409 with that lead's `lead_id`: update that lead instead, or use upsert. Needs `leads: write`.

#### Parameters

- `name` *string, required* The business, or the person.
- `contact_name` *string*
- `phone` *string* A number in the United States or Canada, in any common form.
- `email` *string*
- `status` *string* One of your statuses, in any case. Left out, the status new leads start at. A wrong one is a 400 that names the real ones.
- `city`, `state` *string* `state` is two letters.
- `fields` *object* Custom fields by name, up to 50. A value is a string, number or boolean.

#### Returns

`201` with the `lead`.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/leads \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Reed & Co Insurance",
    "contact_name": "Marcus Reed",
    "phone": "(512) 555-0147",
    "fields": { "Source": "Website form" }
  }'
```

**Response**

```json
{ "lead": { "id": 123, "name": "Reed & Co Insurance", "status": "New" } }
```

### Upsert a lead

`POST /leads/upsert`

Finds the lead with this phone or email and updates it, or makes a new one. Use it for forms and imports so a lead is never made twice. Matching by email counts the lead's own address and its contacts' addresses; an email on two leads is a 409 with their `lead_ids`, never a guess. Matched by one, the other is added when the lead lacks it, and nothing the lead already holds is replaced. Needs `leads: write`.

#### Parameters

- `match` *string, required* `phone` or `email`.
- `name` *string* Required when a new lead is made.
- Any other field of Create a lead.

#### Returns

`200` with `"created": false` for a lead found, `201` with `"created": true` for a new one.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/leads/upsert \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "match": "phone", "name": "Reed & Co Insurance", "phone": "+15125550147", "city": "Austin", "state": "TX" }'
```

**Response**

```json
{ "lead": { "id": 123, "name": "Reed & Co Insurance" }, "created": false }
```

### Update a lead

`PATCH /leads/{id}`

Changes what is given and nothing else. `null` clears the email, the city, the state or a field. A typed custom field refuses a value of another kind, and then nothing in the call is written. Fields for a social security number or a date of birth are refused: we never store them. A status set here shows in the lead's history as set by the key. Needs `leads: write`.

#### Parameters

- `name`, `email`, `status`, `city`, `state` *string*
- `fields` *object* `null` for a field removes it.

**Request**

```sh
curl -X PATCH https://app.talalevel.com/api/v1/leads/123 \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "Interested", "fields": { "Team size": "14" } }'
```

**Response**

```json
{ "lead": { "id": 123, "status": "Interested", "fields": { "Team size": "14" } } }
```

## Notes

Notes people wrote on a lead. A note added through the API is kept exactly as written and signed by the key's owner.

### List a lead's notes

`GET /leads/{id}/notes`

Newest first, with `cursor` and `limit`. Each note has `id`, `text`, `at` and `by`. Needs `notes: read`.

**Request**

```sh
curl https://app.talalevel.com/api/v1/leads/123/notes \
  -H "Authorization: Bearer $TALALEVEL_KEY"
```

**Response**

```json
{
  "notes": [
    { "id": 881, "text": "Team of twelve, deciding by Friday.", "at": "2026-11-12T09:52:00.000Z", "by": "Dana" }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### Add a note

`POST /leads/{id}/notes`

Needs `notes: write`.

#### Parameters

- `text` *string, required* Up to 10,000 characters.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/leads/123/notes \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Wants the proposal by Wednesday." }'
```

**Response**

```json
{ "note": { "id": 882, "text": "Wants the proposal by Wednesday." } }
```

## Activity

A lead's texts, calls and emails, newest first, with `cursor` and `limit`. Read only. Recordings and files are not given out; a call's transcript and summary are read with the `get_call` [tool](#tools).

### List a lead's texts

`GET /leads/{id}/texts`

Each text has `id`, `direction`, `body`, `at`, `number` (the lead's), `line` (yours), `state` and `receipt`, the carrier's word as it came. A text the carrier accepted is not proof it arrived. Needs `texts: read`.

**Response**

```json
{
  "texts": [
    {
      "id": 9912,
      "direction": "inbound",
      "body": "This week ideally. We decide Friday",
      "at": "2026-11-12T09:14:07.000Z",
      "number": "+15125550147",
      "line": "+15125550100",
      "state": null,
      "receipt": null
    }
  ]
}
```

### List a lead's calls

`GET /leads/{id}/calls`

Each call has `id`, `direction`, `at`, `duration` in seconds, `disposition`, `number` and `line`. Needs `calls: read`.

**Response**

```json
{
  "calls": [
    { "id": 4410, "direction": "outbound", "at": "2026-11-12T15:37:20.000Z", "duration": 402, "disposition": "answered", "number": "+15125550147", "line": "+15125550100" }
  ]
}
```

### List a lead's emails

`GET /leads/{id}/emails`

Each email has `id`, `direction`, `at`, `from`, `to`, `cc`, `subject` and `body`. Needs `emails: read`.

## Lists

A saved list is a group of leads your team calls and texts from. A **fixed** list is exactly the leads on it. A **filter** list is rules, and who is on it changes as leads change.

### List the lists

`GET /lists`

Each list has `id`, `name`, `kind` (`fixed` or `filter`), `size` (`null` for a filter list) and `updated_at`. Needs `lists: read`.

**Response**

```json
{ "lists": [{ "id": 300, "name": "Proposal follow-ups", "kind": "fixed", "size": 4, "updated_at": "2026-11-12T21:30:00.000Z" }] }
```

### Create a list

`POST /lists`

Makes a fixed list. Needs `lists: write`.

#### Parameters

- `name` *string, required*
- `lead_ids` *array of integers, required*

#### Returns

`201` with the `list`. A lead not in the workspace is a 404 that names it in `missing`.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/lists \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Proposal follow-ups", "lead_ids": [123, 124, 131] }'
```

### Add leads to a list

`POST /lists/{id}/leads`

Puts leads on a fixed list and answers `added` and `size`. A filter list takes no leads by hand: 409. Needs `lists: write`.

#### Parameters

- `lead_ids` *array of integers, required*

### Remove leads from a list

`DELETE /lists/{id}/leads`

Takes leads off a fixed list and answers `removed` and `not_here`. A change that would empty the list is a 409: an empty list reads as every lead, and a dial or a batch on it would reach the whole workspace. Needs `lists: write`.

#### Parameters

- `lead_ids` *array of integers, required*

## Deals

A deal is one sale on a pipeline, at a stage whose `outcome` is `open`, `won` or `lost`. A lead can have several. Every change is checked and written into the deal's history exactly as one made on the Pipeline page.

#### The deal object

- `id`, `lead_id` *integer*
- `pipeline` *object* `id` and `name`.
- `stage` *object* `id`, `name` and `outcome`.
- `name` *string or null* What is sold.
- `value_cents` *integer or null* A monthly or yearly deal's amount is one month's or one year's.
- `value_period` *string* `one_time`, `monthly` or `annual`.
- `close_on`, `won_on`, `lost_on`, `cancelled_on` *date or null*
- `owner`, `closed_by` *object or null*
- `commission` *object or null* Set once the deal is won.

**The deal object**

```json
{
  "id": 12,
  "lead_id": 123,
  "lead_name": "Reed & Co Insurance",
  "pipeline": { "id": 3, "name": "Sales" },
  "stage": { "id": 9, "name": "Proposal", "outcome": "open" },
  "name": "Annual plan, 12 seats",
  "value_cents": 840000,
  "value_period": "annual",
  "close_on": "2026-11-30",
  "won_on": null,
  "lost_on": null,
  "cancelled_on": null,
  "owner": { "id": 4, "name": "Dana" },
  "closed_by": null,
  "commission": null,
  "updated_at": "2026-11-12T09:52:00.000Z"
}
```

### List pipelines

`GET /pipelines`

Every pipeline with its stages: the ids a deal opens at and moves to. Needs `pipeline: read`.

**Response**

```json
{
  "pipelines": [
    {
      "id": 3,
      "name": "Sales",
      "stages": [
        { "id": 7, "name": "Qualified", "outcome": "open" },
        { "id": 9, "name": "Proposal", "outcome": "open" },
        { "id": 11, "name": "Won", "outcome": "won" },
        { "id": 12, "name": "Lost", "outcome": "lost" }
      ]
    }
  ]
}
```

### List deals

`GET /deals`

Deals in the order they last changed, oldest first, paged as leads are. Needs `pipeline: read`.

#### Parameters

- `cursor`, `limit`
- `lead_id`, `stage_id`, `pipeline_id` *integer*
- `outcome` *string* `open`, `won` or `lost`.
- `changed_since` *string* A time, such as `2026-11-11T09:00:00Z`.

### Get a deal

`GET /deals/{id}`

Needs `pipeline: read`.

### Open a deal

`POST /deals`

Opens a deal on a lead, on the workspace's pipeline at its first open stage unless you say otherwise. It is the key owner's unless `owner_id` names someone else. Needs `pipeline: write`.

#### Parameters

- `lead_id` *integer, required*
- `pipeline_id`, `stage_id` *integer* A stage in progress.
- `name` *string* What is sold.
- `value_cents` *integer*
- `value_period` *string* `one_time`, `monthly` or `annual`.
- `close_on`, `renews_on` *date*
- `owner_id`, `term_months` *integer*

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/deals \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "lead_id": 123, "name": "Annual plan, 12 seats", "value_cents": 840000, "value_period": "annual", "close_on": "2026-11-30" }'
```

**Response**

```json
{ "deal": { "id": 12, "lead_id": 123, "stage": { "id": 7, "name": "Qualified", "outcome": "open" } } }
```

### Update a deal

`PATCH /deals/{id}`

Changes a deal's facts: any field of Open a deal but `lead_id` and the stage. `null` clears. A changed amount on a won deal changes its commission with it. An empty body is a 400. Needs `pipeline: write`.

### Move a deal

`POST /deals/{id}/move`

Moves a deal to another stage of its pipeline. Moving to a **won** stage is the sale: it needs an amount, here or already on the deal. Moving to a **lost** stage, the stage is the reason. Moving back to an open stage reopens it, unless its commission was already marked paid (409). Needs `pipeline: write`.

#### Parameters

- `stage_id` *integer, required*
- `won_on` *date* For a won stage. Today where the lead is, when left out.
- `closed_by` *integer* A person on the team. The deal's owner, when left out.
- `rep_bps` *integer* The closer's share in hundredths of a percent. The workspace's share, when left out.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/deals/12/move \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "stage_id": 11 }'
```

**Response**

```json
{ "deal": { "id": 12, "stage": { "id": 11, "name": "Won", "outcome": "won" }, "won_on": "2026-11-12" } }
```

### Cancel a sale

`POST /deals/{id}/cancel`

Marks a won sale as cancelled by the customer, or with `null`, undoes that. Whether the commission is taken back is the workspace's own rule. Needs `pipeline: write`.

#### Parameters

- `on` *date or null, required* The day the customer cancelled.

## Texts and emails

Sending needs the area at **send**. A send goes through every check a send in the app goes through: opt-outs, quiet hours in the lead's time zone, message length and your numbers' registration. Where a person in the app would see a warning, the API refuses with a 422 and the reason, and nothing is sent. Texts and emails sent with a key count against its daily limit, 20 in 24 hours unless the owner changes it.

### Send a text

`POST /texts`

Texts one of the lead's own numbers from your registered numbers. Never a number typed in. Needs `texts: send`.

#### Parameters

- `lead_id` *integer* The lead. Or `conversation_id` to answer in one thread.
- `text` *string, required* Up to three segments.
- `number` *string* One of the lead's own numbers, when not the first.

#### Returns

`201` with `state`, `messageId`, `conversationId`, `to`, `from` and `segments`.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/texts \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "lead_id": 123, "text": "Hi Marcus, Dana here. Does 9:30 tomorrow still work?" }'
```

**Response**

```json
{
  "state": "sent",
  "messageId": 20417,
  "conversationId": 5521,
  "to": "+15125550147",
  "from": "+15125550100",
  "segments": 1
}
```

### Send an email

`POST /emails`

Emails one of the lead's own addresses, from the key owner's Gmail or one of the workspace's addresses. Needs `emails: send`.

#### Parameters

- `lead_id` *integer, required*
- `subject` *string, required*
- `body` *string, required* Plain text, a blank line between paragraphs.
- `to` *string* One of the lead's own addresses, when not the first.
- `from` *string* One of your sending addresses, when not the default.

#### Returns

`201` with `id`, `from` and `to`.

## Batches and waves

A batch sends one template to a list. A wave texts a list in several touches on a schedule. Each is a **preview**, then a **start** with the preview's id. A preview changes nothing; it is good once, for 15 minutes, for the key that made it. The start checks everything again and is refused as changed if anything moved since.

### List templates

`GET /templates`

The text templates that are on, each with `id`, `name` and a `sample`. Needs `texts: read`.

### Preview a batch

`POST /batches/preview`

Says how many texts would go, what they cost, who is left out and why, and what waits for the lead's morning. `previewId` is `null` when nothing can go. Needs `texts: send`.

#### Parameters

- `name` *string, required* The batch's name on the Texts screen.
- `template_id` *integer, required*
- `list_id` *integer* Or `lead_ids`.
- `template_b_id` *integer* A second template, for an A/B.
- `split` *integer* With two templates, the first one's share in percent.
- `max_rows` *integer* Send to at most this many.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/batches/preview \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "November follow-up", "template_id": 31, "list_id": 300 }'
```

### Start a batch

`POST /batches`

Starts a previewed batch. It waits a minute before it sends; `cancelBy` is the end of that minute. Needs `texts: send`.

#### Parameters

- `preview_id` *string, required*

#### Returns

`201` with `batchId`, `cancelBy` and `texts`.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/batches \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "preview_id": "<previewId from the preview>" }'
```

### Cancel a batch

`POST /batches/{id}/cancel`

Within its first minute nothing goes. After that, texts not yet handed to the carrier stay unsent. Needs `texts: send`.

### List waves

`GET /waves`

Needs `texts: read`.

### Preview a wave

`POST /waves/{id}/preview`

Answers `previewId` and the wave's plan. Needs `texts: send`.

### Run a wave

`POST /waves/run`

Runs a previewed wave now and answers `waveId` and `name`. Needs `texts: send`.

#### Parameters

- `preview_id` *string, required*

## AI caller

Joey, the AI caller, calls a list for you, one run at a time in a workspace, inside its calling hours. On the Scale plan.

### Get the AI caller

`GET /ai-runs`

The run now and today's calls. Needs `calls: read`.

### Preview a run

`POST /ai-runs/preview`

Answers `previewId` and the leads it would call. Needs `calls: send`.

#### Parameters

- `list_id` *integer* Or `lead_ids`.

### Start a run

`POST /ai-runs`

Needs `calls: send`.

#### Parameters

- `preview_id` *string, required*
- `calls_per_day` *integer*

#### Returns

`201` with `runId` and `queued`.

### Stop the AI caller

`POST /ai-runs/stop`

No new calls start. A call already on carries on to its end. Needs `calls: send`.

### Resume a run

`POST /ai-runs/{id}/resume`

Needs `calls: send`.

## Tools

Everything the [MCP server](https://talalevel.com/docs/mcp/) can do is also a REST call, by the tool's name. Use it for what has no route of its own: searching calls and transcripts, tasks, the calendar, activity figures, drafts. This is what the [command line](https://talalevel.com/docs/cli/) uses.

### List tools

`GET /tools`

The tools this key may run now, each with `name`, `title`, `description`, `area`, `level`, `changes`, `sends` and `input` as JSON Schema.

### Run a tool

`POST /tools/{name}`

The body is the tool's input. Answers `text`, written for a model to read, and `data` with the same as fields where the tool has them. A refusal is a 422 with the reason, a tool the key may not run a 403, and a name that does not exist a 404.

**Request**

```sh
curl -X POST https://app.talalevel.com/api/v1/tools/search_calls \
  -H "Authorization: Bearer $TALALEVEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reached": "human", "from": "2026-11-10", "limit": 5 }'
```

**Response**

```json
{
  "text": "call 4410 · Nov 12, 9:37 AM · lead 123 Reed & Co Insurance · outbound · 6m42s · human · Dana\n<<<summary\nTeam of twelve, deciding by Friday. Wants the proposal by Wednesday.\n>>>"
}
```

## Not in the API

- Placing a call as a person. Calls are made in the app's dialer.
- Deleting a lead or a deal.
- Recordings and files.
- Marking a commission paid.

## Questions

### Can I send a text message through the TalaLevel API?

Yes. POST /texts with a lead_id and the text, using a key that has texts at send. It goes from your own registered numbers and through the same checks as a text sent in the app.

### How do I avoid duplicate leads when importing?

Use POST /leads/upsert with match set to phone or email. It updates the lead that has that phone or email, or makes a new one.

### Can the API start the AI caller?

Yes. POST /ai-runs/preview with a list, then POST /ai-runs with the preview's id, using a key that has calls at send.
