Skip to main content

Calls

The calls API is the entry point of Scryon — upload an audio file, get a callId, poll for completion.

POST /api/calls/analyze

Submit a recording for analysis.

Request

Content-Type: multipart/form-data with two parts:

PartTypeRequiredNotes
fileaudio fileyes.m4a, .mp3, .wav, .aac, .ogg. Max MAX_FILE_SIZE (default 50 MB).
metadataapplication/json partyesThe envelope below.
titletext partnoConvenience for clients that don't want to build a JSON envelope.

metadata envelope

{
"title": "Quarterly review",
"contactName": "Ravi",
"contactId": "contacts:42",
"phoneNumber": "+91 98765 43210",
"organization": "Acme",
"direction": "OUTGOING",
"recordedAt": "2026-05-29T13:00:00Z",
"durationSeconds": 240
}

All fields are optional. The more you send, the better the speaker resolution.

Headers

  • Authorization: Bearer … (Firebase)
  • Idempotency-Key: … (recommended for flaky networks)
  • Content-Type: multipart/form-data; boundary=…

Response — 202 Accepted

{
"callId": "f0a1d2e3-...",
"status": "QUEUED"
}

Errors

StatuscodeCause
400validation_failedMissing file, bad metadata JSON.
401auth_invalidSee Authentication.
413payload_too_largeFile exceeds MAX_FILE_SIZE.
415unsupported_media_typeAudio MIME not recognised.

GET /api/calls

List the authenticated user's calls.

Query parameters

ParamTypeDefaultNotes
limitint50Max 100.
cursorstringFrom nextCursor on the previous page.
statusstringFilter (QUEUED, TRANSCRIBING, COMPLETED, FAILED).

Response — 200 OK

{
"items": [
{
"callId": "f0a1d2e3-...",
"title": "Quarterly review",
"contactName": "Ravi",
"direction": "OUTGOING",
"status": "COMPLETED",
"durationSeconds": 240,
"createdAt": "2026-05-29T13:00:00Z",
"actionItemCount": 2,
"firstActionTitle": "Send revised pricing quote to Ravi"
}
],
"nextCursor": "eyJjcmVhdGVk..."
}

actionItemCount / firstActionTitle are a lightweight peek so the calls list can show an action-item badge without a second round-trip to GET /api/actions. firstActionTitle is the oldest live (non-dismissed) item's title, or null when there are none.

GET /api/calls/status

Bulk poll the status of many calls at once.

GET /api/calls/status?ids=f0a1d2e3-...,a1b2c3d4-...

Response — 200 OK

{
"items": [
{ "callId": "f0a1d2e3-...", "status": "COMPLETED" },
{ "callId": "a1b2c3d4-...", "status": "TRANSCRIBING" }
]
}

Cheap, idempotent, safe to poll on a 3–5 second cadence.

GET /api/calls/{id}

Single call detail (no transcript / analysis — those have dedicated endpoints).

Response — 200 OK

{
"callId": "f0a1d2e3-...",
"title": "Quarterly review",
"status": "COMPLETED",
"direction": "OUTGOING",
"contactName": "Ravi",
"phoneNumber": "+91 98765 43210",
"durationSeconds": 240,
"recordedAt": "2026-05-29T13:00:00Z",
"createdAt": "2026-05-29T13:00:00Z",
"errorReason": null
}

PATCH /api/calls/{id}

Edit user-editable call metadata — title, summary, and notes (AND-003/004/005). All fields are optional: omit a field to leave it unchanged, pass an empty string to clear it.

Request

{
"title": "Q3 pricing follow-up",
"summary": "Agreed on a Friday demo; revised quote due Wednesday.",
"notes": "Ravi is price-sensitive — lead with the annual discount next time."
}
FieldEffect
titleOverrides the AI-suggested or upload-supplied title.
summaryOverrides the AI-generated one-line summary (shortSummary).
notesFreeform user notes attached to the call. Not generated by the LLM.

Response — 200 OK

The updated call detail (same shape as GET /api/calls/{id}).

Errors

StatusCause
404Call not found or not owned by the caller.

PATCH /api/calls/{callId}/contact

Assign or remove a Scryon contact on a call, so it appears in that contact's timeline.

Pass {"contactId": null} to remove a previously-assigned contact. Both the call and the contact must belong to the authenticated user; a mismatched id returns 404 (same as not-found, to avoid id enumeration). This does not touch the legacy contactId / contactName fields captured from the phone's call log at upload time — those two concepts (platform contact vs. Scryon contact) are independent.

Request

{ "contactId": "c1a2b3c4-..." }

Response — 200 OK

The updated call detail.

Errors

StatusCause
404Call or contact not found, or not owned by the caller.

POST /api/calls/{id}/reanalyze (dev/staging only)

Re-run the LLM analysis for a completed call using its stored NORMALIZED_TRANSCRIPT_JSON — no audio is re-transcribed or re-diarized. Overwrites the ANALYSIS_JSON artifact and re-applies action-item side effects (delete-then-insert), so it is idempotent and safe to re-run after a prompt change.

Gated to non-prod. The controller is annotated @Profile("!prod"), so the route is not registered when the server runs the prod profile (returns 404 there). The Android client mirrors this with a DEVELOPER_TOOLS build flag (true for dev/staging flavours only) and surfaces it under Settings → Developer → Reanalyze.

Scoped to the authenticated user; foreign/unknown ids return 404.

Response — 200 OK

The updated call detail (same shape as GET /api/calls/{id}), reflecting the refreshed analysis.

Errors

StatusCause
404Call missing, owned by another user, or has no stored transcript to re-analyze.
409Call is not in a re-analyzable (COMPLETED) state. error carries the offending status.
404 (prod)Route not registered — server is running the prod profile.

DELETE /api/calls/{id}

Hard-delete a call, all artifacts, all action items.

Response — 204 No Content

Irreversible. The artifacts in object storage are deleted before the row, so a partial delete on failure simply leaves orphans for the sweeper.

DELETE /api/calls

Bulk delete.

Request

{ "callIds": ["f0a1d2e3-...", "a1b2c3d4-..."] }

Response — 200 OK

{
"deleted": 2,
"missing": 0,
"failed": []
}

Errors common to all calls endpoints

StatuscodeCause
404call_not_foundThe call doesn't exist or belongs to another user.
422call_not_completedTrying to read transcript / analysis before completion.