Your call data, in your systems.
The Signal API gives a practice's own systems — an EMR, a reporting tool, a script — read access to its calls: who called, when, for how long, the transcript and the AI summary.
Quickstart
- Create a key. An owner or admin signs in at admin.letssignal.com → Settings → API keys, names the key after the system that will use it, and copies it. The full key is shown once.
- Check it works.
curl https://api.letssignal.com/v1/whoami \ -H "Authorization: Bearer $SIGNAL_API_KEY" - Pull your calls. Store the
next_cursoryou get back and send it on the next request.curl "https://api.letssignal.com/v1/calls?since=2026-10-01" \ -H "Authorization: Bearer $SIGNAL_API_KEY"
Authentication
Every request carries an API key in the Authorization header. Keys start
with sig_live_, belong to one practice, and only ever see that practice's data.
Authorization: Bearer sig_live_…
Keys expire one year after they're created. Settings shows when each key was last used, so before you revoke an old key you can check that nothing still depends on it.
Polling for calls
GET /v1/calls is a feed. Calls come back oldest first. Each response
carries a next_cursor: store it, and send it as cursor next time. You'll
receive every call exactly once, in order.
import os, time, requests
KEY = os.environ["SIGNAL_API_KEY"]
cursor = load_cursor() # None on the very first run
while True:
params = {"cursor": cursor} if cursor else {"since": "2026-10-01"}
r = requests.get("https://api.letssignal.com/v1/calls", params=params,
headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
r.raise_for_status()
page = r.json()
for call in page["data"]:
if call["direction"] != "internal":
log_to_emr(call) # idempotent on call["id"]
cursor = page["next_cursor"]
save_cursor(cursor)
if not page["has_more"]:
time.sleep(300) # caught up — check again in five minutes
curl "https://api.letssignal.com/v1/calls?cursor=$CURSOR&limit=50" \
-H "Authorization: Bearer $SIGNAL_API_KEY"
| Situation | What to do |
|---|---|
| First run | Leave out cursor. Add since (a UTC date) to skip older history. |
has_more is true | A full page came back. Request the next one straight away. |
data is empty | You're caught up. Keep the same cursor and poll again in 5–15 minutes. |
| Your job crashed | Restart from the last cursor you saved. Use id to skip any call you already logged. |
Matching patients
Each call has external_number: the phone number of the party outside the practice,
in E.164 format (+13475550101). Use it to find the patient in your system.
If Signal already holds that number against a patient and knows the ID your system uses for them,
contact.external_ref carries it and you don't need to search.
Internal calls (one extension calling another) have no outside number. Most chart integrations skip them.
Transcripts
transcript_status tells you what to expect:
| Value | Meaning |
|---|---|
ready | transcript holds the full text. summary, action_items and sentiment are filled in. |
none | The call wasn't recorded or transcribed. Recording is set per practice. Ask Signal support if you expected transcripts. |
purged | The transcript was deleted under the practice's retention policy. The summary is kept. |
Errors
Errors come back with an HTTP status and a JSON body with a detail sentence.
| Status | Meaning |
|---|---|
| 400 | The cursor isn't one we issued. Send back only a next_cursor you received. |
| 401 | The key is missing, invalid, expired or revoked. |
| 403 | The key doesn't have permission for this endpoint, or the account is suspended. |
| 404 | No call with that id exists on this account. |
| 422 | A parameter is out of range, for example a limit above 200. |
| 5xx | Something went wrong on our side. Retry with backoff. Your cursor is still valid. |
Privacy & keys
This API returns patient information. Every request is written to the practice's access log under the name of the key that made it.
- Give each system its own key, so revoking one doesn't break the others.
- Keep keys in a secret store, never in source code or a spreadsheet.
- Revoke a key the day the system using it is retired. Revoking takes effect immediately.
- Only send this data to systems covered by the practice's business associate agreements.
Endpoints
The Call object
One item of data in GET /v1/calls, and the whole response of GET /v1/calls/{id}.
OpenAPI spec
Everything above, in machine-readable form: every endpoint, parameter, field, type and error. Import it into Postman or Insomnia, or use it to generate a client.
curl -o signal-openapi.json https://api.letssignal.com/openapi.json