Athos Developer Docs
Backend

Reading calls

List an agency's or a rep's scored calls — practice or real, one source per request — and read one call's transcript, score and recording.

Two endpoints, both server-to-server with your API key: a slim list that answers "all calls for this agency" and "all calls for this rep", and a full detail for one call's transcript, score and recording. Practice calls (roleplay) and real calls (real_call) have the same shape, and you read them one source at a time: source is required on the list.

List — GET /v1/calls

Returns scored calls of one source for your tenant, newest first (roleplay by endedAt, real_call by startedAt), cursor-paginated.

Query parameters

ParamRequiredNotes
from✅Lower bound (ISO 8601 UTC, inclusive) on when a roleplay call ended / a real_call started.
source✅roleplay (practice calls) or real_call (an agency's real calls). One per request; omitting it is 400 INVALID_REQUEST.
toUpper bound (ISO 8601, inclusive), same axis as from.
agencyIdAll calls for one agency, by its Athos id (athos_agency_…). Checked against your roster: an unknown or foreign id is 404 AGENCY_NOT_FOUND.
agentIdAll calls for one rep, by their Athos id (athos_agent_…). Checked against your roster: an unknown or foreign id is 404 AGENT_NOT_FOUND. With an agencyId the rep is not in, the page is empty.
platformAgentIdOne rep, by the platformAgentId you registered them under. An unknown value is an empty page. Not together with agentId.
platformAgencyIdOne agency, by the platformAgencyId you registered it under. An unknown value is an empty page. Not together with agencyId.
limitPage size, 1–100, default 50. Out of range or not an integer is 400 INVALID_REQUEST — it is not clamped.
cursorOpaque token from a previous nextCursor. A cursor you did not get from this endpoint is 400 INVALID_REQUEST.

Two filter pairs. agencyId / agentId are Athos ids and are checked: an agency or rep that is not yours is a 404, never an empty page. platformAgencyId / platformAgentId are your own ids and are plain filters: an unknown value is an empty page. Name each rep and each agency one way per request — both names for one is 400 INVALID_REQUEST. Neither pair is required.

Response — a slim row per call (no score, no transcript):

{
  "data": [
    {
      "id": "athos_call_GZ1DjOkZv_VEPoE-CJ8mp",
      "source": "roleplay",
      "agencyId": "athos_agency_V1StGXR8Z5jdHi6B-myT",
      "agentId": "athos_agent_bR4kQ2wZ9Lp0nH3xVq1Yt",
      "platformAgentId": "rep_8842",
      "platformAgencyId": "acme-7731",
      "drillKey": "ma-full-sale",
      "startedAt": "2026-06-05T14:39:40.295Z",
      "endedAt": "2026-06-05T14:49:52.295Z",
      "durationSec": 612,
      "scoredAt": "2026-06-05T14:50:29.005Z"
    }
  ],
  "nextCursor": "eyJ0IjoxNzQ5…"
}

Every key is always present. platformAgentId / platformAgencyId are null when you never registered one — join on agentId / agencyId then (see Core concepts). drillKey is null on a real_call. Page with cursor and the same source, as described in Paging.

Reconciling by polling

from and to filter on the call's own time, not on when it was scored — when it ended for a roleplay call, when it started for a real_call — and results are ordered on that same axis. Scoring finishes minutes after a call ends, so a call can become readable inside a window you have already read past. When you reconcile (the backstop for a missed webhook delivery), re-scan a window that overlaps the last one by an hour plus the longest real call you expect, and deduplicate on id. A forward-only watermark misses calls that were scored after you read past them.

# every practice call for one agency (source=real_call for its real calls)
curl -G https://app.useathos.ai/api/external/v1/calls \
  -H "Authorization: Bearer $ATHOS_API_KEY" \
  --data-urlencode "from=2026-06-01T00:00:00Z" \
  --data-urlencode "source=roleplay" \
  --data-urlencode "agencyId=athos_agency_V1StGXR8Z5jdHi6B-myT" \
  --data-urlencode "limit=50"
async function* callsFor(
  from: string,
  source: 'roleplay' | 'real_call',
  scope: { agencyId?: string; agentId?: string },
) {
  let cursor: string | null = null;
  do {
    const url = new URL('https://app.useathos.ai/api/external/v1/calls');
    url.searchParams.set('from', from);
    url.searchParams.set('source', source);
    if (scope.agencyId) url.searchParams.set('agencyId', scope.agencyId);
    if (scope.agentId) url.searchParams.set('agentId', scope.agentId);
    if (cursor) url.searchParams.set('cursor', cursor);
    const r = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ATHOS_API_KEY}` } });
    if (r.status === 404) throw new Error((await r.json()).error.code); // AGENCY_NOT_FOUND / AGENT_NOT_FOUND
    const page = await r.json();
    yield* page.data;
    cursor = page.nextCursor;
  } while (cursor);
}

// one rep's practice calls, newest first — a second loop with 'real_call' reads their real calls
for await (const call of callsFor('2026-06-01T00:00:00Z', 'roleplay', { agentId: rep.agentId })) {
  console.log(call.id, call.drillKey, call.scoredAt);
}

GET /v1/calls is a history read — scored calls only, never in-progress ones. The call.scored webhook is your notification; use this endpoint to render history and to reconcile.

Detail — GET /v1/calls/:callId

Look up one call of either source by its public id (from a list row, a webhook callId, or the SDK ended event). An unknown id and another tenant's id both answer 404 CALL_NOT_FOUND.

curl https://app.useathos.ai/api/external/v1/calls/athos_call_GZ1DjOkZv_VEPoE-CJ8mp \
  -H "Authorization: Bearer $ATHOS_API_KEY"

Response (roleplay; complianceScorecard abridged):

{
  "id": "athos_call_GZ1DjOkZv_VEPoE-CJ8mp",
  "source": "roleplay",
  "status": "scored",
  "agencyId": "athos_agency_V1StGXR8Z5jdHi6B-myT",
  "agentId": "athos_agent_bR4kQ2wZ9Lp0nH3xVq1Yt",
  "platformAgentId": "rep_8842",
  "platformAgencyId": "acme-7731",
  "drillKey": "ma-full-sale",
  "startedAt": "2026-06-05T14:39:40.295Z",
  "endedAt": "2026-06-05T14:49:52.295Z",
  "durationSec": 612,
  "scoredAt": "2026-06-05T14:50:29.005Z",
  "transcript": [
    { "role": "agent", "content": "Hi, this is Sam from the benefits line." },
    { "role": "beneficiary", "content": "Yeah, I saw something about the grocery card." }
  ],
  "score": {
    "overallScore": 82,
    "summary": "Prospect asked about Medicare options; the agent built rapport but rushed the close.",
    "feedback": "Lead with a clear greeting and one discovery question; slow down before presenting the plan.",
    "compliancePercent": 85,
    "complianceScorecard": {
      "requiredCallOpening": {
        "statedNameAndAgency": {
          "yesNo": "yes",
          "score": 5,
          "explanation": "The agent gave their name and the agency in the first ten seconds."
        },
        "statedCallIsRecorded": {
          "yesNo": "yes",
          "score": 3,
          "explanation": "The agent said the call was recorded before asking any questions."
        }
      },
      "scopeOfAppointment": {
        "confirmedProductsToDiscuss": {
          "yesNo": "no",
          "explanation": "The agent moved into the plan comparison without confirming the scope on file."
        }
      }
    }
  },
  "audioUrl": "https://…/athos_call_GZ1DjOkZv_VEPoE-CJ8mp.ogg?X-Amz-Expires=3600&X-Amz-Signature=…",
  "audioUrlExpiresAt": "2026-06-05T15:50:29.005Z",
  "waveform": null,
  "createdAt": "2026-06-05T14:39:45.000Z",
  "updatedAt": "2026-06-05T14:50:29.100Z"
}

The detail is a list row plus status (always "scored"), transcript, score, audioUrl, audioUrlExpiresAt, waveform (reserved, always null), createdAt and updatedAt.

The transcript

A flat list of turns, in the order they were spoken. The same shape for both sources.

FieldTypeWhat it is
role"agent" or "beneficiary"Who spoke. agent is your rep — the human being graded. beneficiary is the customer — the AI persona on a practice call, the real person on the line on a real_call.
contentstringWhat was said on that turn.
startTime, endTimenumber (optional)Turn boundaries in seconds from the start of the call. Present on real_call turns only.

The score object

FieldTypeWhat it is
overallScorenumberOverall call performance, 0–100 (higher is better); on a real_call the scale runs 60–100. 0 means no headline score could be produced.
summarystringA short natural-language summary of the call.
feedbackstringCoaching feedback for the rep.
compliancePercentnumber (optional)How compliant the call was — 0–100 (higher is better), comparable across calls whichever scorecard graded them. Present whenever the call was assessed for compliance.
complianceScorecardobject (optional)The default Medicare rubric's compliance review, section by section — the graded items and their per-item detail. No total; read compliancePercent for the number.

summary and feedback are always present but either can be an empty string — on a very short call, or when the analysis step did not complete, the call still scores but has no prose.

compliancePercent is the compliance number. It answers how compliant was this call — 0–100, comparable across calls whichever scorecard graded them, and the one to render and to compare. complianceScorecard is the default Medicare rubric's section-by-section detail — the graded items, not a total. A call can carry compliancePercent on its own.

complianceScorecard is optional — check for it, don't assume it. On practice calls it appears on the two Medicare Advantage enrollment drills only — ma-full-sale and ma-t65 — and only when the call ran at least five minutes; a shorter enrollment call and every other drill (the other MA drills included) omit the key. On a real call it appears only on a Medicare Advantage call that ended in a sale — never on a Final Expense one. And an agency graded by its own compliance scorecard rather than the default rubric carries compliancePercent with no complianceScorecard, because this blob is the default rubric's detail and that rubric did not run. When there is none the key is absent, not null. Its sections are graded content and may gain items over time — read compliancePercent for the number and treat the sections as display detail.

Recording playback

audioUrl is either a short-lived signed link to Athos's own archived recording — with audioUrlExpiresAt set to the moment it stops working, about an hour after the request — or null, in which case audioUrlExpiresAt is null too and there is nothing to play (a real call whose recording was not archived, a real call older than 30 days — its archived recording has been deleted — or the rare case where a recording exists but could not be prepared, which a later request usually resolves).

Use the link directly in an <audio> element or a download link. Do not store it, email it or embed it anywhere long-lived: when you need it again, fetch the call again and you get a fresh one. The link is signed for GET only — a HEAD request answers 403; to inspect the file without downloading it, send a ranged GET (Range: bytes=0-0). On a real_call it is never the dialer's own recording URL.

On this page