Signal API OpenAPI spec Get an API key
Developer documentation

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.

Base URL https://api.letssignal.com/v1

Quickstart

  1. 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.
  2. Check it works.
    curl https://api.letssignal.com/v1/whoami \
      -H "Authorization: Bearer $SIGNAL_API_KEY"
  3. Pull your calls. Store the next_cursor you 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_…
Never put a key in a URL. A key in a query string ends up in proxy and server logs. Keys sent that way are refused.

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
SituationWhat to do
First runLeave out cursor. Add since (a UTC date) to skip older history.
has_more is trueA full page came back. Request the next one straight away.
data is emptyYou're caught up. Keep the same cursor and poll again in 5–15 minutes.
Your job crashedRestart from the last cursor you saved. Use id to skip any call you already logged.
Calls arrive 10–15 minutes after they end. A call stays out of the feed until its transcript has finished, so you never log a call and then find a transcript turning up for it afterwards.

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:

ValueMeaning
readytranscript holds the full text. summary, action_items and sentiment are filled in.
noneThe call wasn't recorded or transcribed. Recording is set per practice. Ask Signal support if you expected transcripts.
purgedThe 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.

StatusMeaning
400The cursor isn't one we issued. Send back only a next_cursor you received.
401The key is missing, invalid, expired or revoked.
403The key doesn't have permission for this endpoint, or the account is suspended.
404No call with that id exists on this account.
422A parameter is out of range, for example a limit above 200.
5xxSomething 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

Loading the reference…

The Call object

One item of data in GET /v1/calls, and the whole response of GET /v1/calls/{id}.

Loading…

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

Download signal-openapi.json