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 its key needs, as area: level. The same reference is in openapi.json.
https://app.talalevel.com/api/v1Leads
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
idintegernamestring The business, or the person when there is no business.statusstring One of your workspace’s statuses.email,city,statestring or nullstateis two letters.time_zonestring or null An IANA zone, worked out from the lead’s place and numbers.contactsarray Each with aname, itsphones(E.164, in dial order) and itsemails.fieldsobject Your custom fields by name.ownerobject or null The person on your team who owns the lead.starredbooleanupdated_atstring When it last changed.urlstring The lead’s page in the app.
{
"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
cursorstring Where to carry on from: the last answer’snext_cursor.limitinteger How many to answer with. 50 when left out.statusstring Only leads with this status, in any case.
Returns
leads, next_cursor and has_more.
curl "https://app.talalevel.com/api/v1/leads?limit=50" \
-H "Authorization: Bearer $TALALEVEL_KEY"{
"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.
curl https://app.talalevel.com/api/v1/leads/123 \
-H "Authorization: Bearer $TALALEVEL_KEY"{ "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
namestring, required The business, or the person.contact_namestringphonestring A number in the United States or Canada, in any common form.emailstringstatusstring 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,statestringstateis two letters.fieldsobject Custom fields by name, up to 50. A value is a string, number or boolean.
Returns
201 with the lead.
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" }
}'{ "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
matchstring, requiredphoneoremail.namestring 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.
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" }'{ "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,statestringfieldsobjectnullfor a field removes it.
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" } }'{ "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.
curl https://app.talalevel.com/api/v1/leads/123/notes \
-H "Authorization: Bearer $TALALEVEL_KEY"{
"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
textstring, required Up to 10,000 characters.
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." }'{ "note": { "id": 882, "text": "Wants the proposal by Wednesday." } }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.
{
"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.
{
"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.
{ "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
namestring, requiredlead_idsarray of integers, required
Returns
201 with the list. A lead not in the workspace is a 404 that names it in missing.
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_idsarray 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_idsarray 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_idintegerpipelineobjectidandname.stageobjectid,nameandoutcome.namestring or null What is sold.value_centsinteger or null A monthly or yearly deal’s amount is one month’s or one year’s.value_periodstringone_time,monthlyorannual.close_on,won_on,lost_on,cancelled_ondate or nullowner,closed_byobject or nullcommissionobject or null Set once the deal is won.
{
"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.
{
"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,limitlead_id,stage_id,pipeline_idintegeroutcomestringopen,wonorlost.changed_sincestring A time, such as2026-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_idinteger, requiredpipeline_id,stage_idinteger A stage in progress.namestring What is sold.value_centsintegervalue_periodstringone_time,monthlyorannual.close_on,renews_ondateowner_id,term_monthsinteger
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" }'{ "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_idinteger, requiredwon_ondate For a won stage. Today where the lead is, when left out.closed_byinteger A person on the team. The deal’s owner, when left out.rep_bpsinteger The closer’s share in hundredths of a percent. The workspace’s share, when left out.
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 }'{ "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
ondate 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_idinteger The lead. Orconversation_idto answer in one thread.textstring, required Up to three segments.numberstring One of the lead’s own numbers, when not the first.
Returns
201 with state, messageId, conversationId, to, from and segments.
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?" }'{
"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_idinteger, requiredsubjectstring, requiredbodystring, required Plain text, a blank line between paragraphs.tostring One of the lead’s own addresses, when not the first.fromstring 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
namestring, required The batch’s name on the Texts screen.template_idinteger, requiredlist_idinteger Orlead_ids.template_b_idinteger A second template, for an A/B.splitinteger With two templates, the first one’s share in percent.max_rowsinteger Send to at most this many.
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_idstring, required
Returns
201 with batchId, cancelBy and texts.
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_idstring, 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_idinteger Orlead_ids.
Start a run
POST/ai-runs
Needs calls: send.
Parameters
preview_idstring, requiredcalls_per_dayinteger
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 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 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.
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 }'{
"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.
© 2026 TalaLevel · talalevel.com · openapi.json · llms.txt · Privacy · Terms