API reference
Two endpoints: send a call, read its result. Machine-readable description: OpenAPI 3.1 (JSON). Results are QA signals for human review.
Authentication
Create a key in Settings & API (organization owners and admins). Send it on every request as Authorization: Bearer gnk_… or x-api-key: gnk_…. A key belongs to one organization and can only read and write that organization’s calls. Only a one-way hash is stored, so a lost key cannot be recovered: revoke it and create another. Keep keys on your server, never in a browser or mobile app. MCP connection credentials are a different kind and are refused here (and API keys are refused on the MCP endpoint).
POST /api/v1/calls
Ingest a call transcript. Unknown fields are rejected. Provide exactly one of transcript or transcriptText. Audio recordings are uploaded in the app or arrive through a Twilio connection; this endpoint accepts transcripts.
| Field | Type | Notes |
|---|---|---|
externalCallId | string, required | 1–200 characters: letters, digits and . _ : @ / + = ~ -. Your identifier; the idempotency key. |
agent | { id, name? } | The voice agent that handled the call. |
startedAt, endedAt | ISO-8601 with offset | endedAt must not precede startedAt. |
durationSeconds | number | 0 to 24 hours. |
recordingUrl | https:// URL | A reference only. The server never fetches it. |
transcript | array of segments | { speaker, text, startTime?, endTime? }, 1–5,000 segments; startTime and endTime together, in seconds. speaker (case-insensitive): agent / assistant / bot / ai / model / voice_agent / voiceagent → agent; customer / user / caller / client / human / contact / lead → customer; anything else or missing → unknown. |
transcriptText | string | Alternative to transcript: lines such as Agent: … and Customer: …, optional [mm:ss] prefixes. Up to 400,000 characters. |
metadata | object | Up to 50 keys and 16 KB; scalar values, short arrays or flat objects. |
curl -X POST https://goldnovaai.com/api/v1/calls \
-H "Authorization: Bearer $GOLDNOVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalCallId": "call-1042",
"agent": { "id": "support-bot-1", "name": "Support bot" },
"transcript": [
{ "speaker": "agent", "text": "Hello, this call is recorded for quality purposes.", "startTime": 0, "endTime": 4 },
{ "speaker": "customer", "text": "Okay.", "startTime": 4, "endTime": 5 },
{ "speaker": "agent", "text": "We guarantee a full refund.", "startTime": 41.2, "endTime": 44.0 }
]
}'{ "id": "6f1c0a52-8d1e-4c43-9a7b-2f6a3d9e1b10", "externalCallId": "call-1042", "status": "ready", "duplicate": false }Idempotency
externalCallId is unique per organization. Replaying the identical call returns 200 with "duplicate": true and stores nothing twice (it also does not count against your allowance twice, and is still accepted when the allowance is used up). Sending the same id with different content returns 409 call_conflict; the stored call and its evidence are kept.
GET /api/v1/calls/{id}
The status, the QA result and the findings of a call in the key’s organization. evaluation is null until the call has been evaluated.
curl https://goldnovaai.com/api/v1/calls/6f1c0a52-8d1e-4c43-9a7b-2f6a3d9e1b10 \
-H "Authorization: Bearer $GOLDNOVA_API_KEY"{
"id": "6f1c0a52-8d1e-4c43-9a7b-2f6a3d9e1b10",
"externalCallId": "call-1042",
"status": "evaluated",
"transcriptSource": "provided",
"transcriptQuality": null,
"failureReason": null,
"evaluation": {
"result": "violation",
"resultLabel": "Potential violation",
"qaScore": 62.5,
"policyVersion": 3,
"evaluatedAt": "2026-10-01T09:31:02.114Z"
},
"findings": [
{
"rule": "Prohibited claims",
"ruleType": "prohibited_claim",
"severity": "high",
"status": "violation",
"explanation": "The agent used a prohibited phrase.",
"evidence": [{ "quote": "We guarantee a full refund.", "startTime": 41.2, "endTime": 44.0 }]
}
],
"notice": "QA signal for human review. Findings are automated indications, not legal or compliance determinations."
}Call lifecycle
received (accepted) → ready (transcript attached) → evaluating → evaluated. A call that cannot be processed ends as failed with a failureReason; a failed call is never shown as evaluated and never carries a QA result. Re-sending a call that already has a transcript changes nothing. Evaluation runs after the response, so read the call again until status is evaluated.
Limits and allowances
- Request body: 1.5 MB (
413above that). - Rate limit: 120 requests per minute per key.
429 rate_limitedcarriesRetry-After: 30(seconds). If the limiter itself is unavailable the request is also refused with429rather than let through, so a429is always safe to retry after the delay. - Allowances are per organization and plan, and are enforced on the server for every route that creates work. This API consumes the QA call allowance only (transcripts need no transcription); audio transcription minutes and AI assessments are separate allowances. When the call allowance is used up, new calls are refused with
402 quota_exceeded(body:limit,used,resetsAt); stored calls and reports stay available. See pricing.
Allowances per organization and billing period (the numbers the server enforces):
| Plan | Price | QA calls | Audio transcription | AI assessments |
|---|---|---|---|---|
| Free | €0 | 200 | 300 min | 50 |
| Starter | €99 / month | 2,000 | 3,000 min | 500 |
| Team | €299 / month | 10,000 | 15,000 min | 3,000 |
| Enterprise | Individual terms: contact us | |||
Audio recordings
There is no REST endpoint for audio. Recordings are uploaded in the app (up to 50 MiB and 2 hours; they are converted and, if needed, split before speech-to-text) or arrive from a Twilio connection (developer preview). Calls created from audio have the transcript source asr, carry no speaker labels, and use the audio-minutes allowance. If transcription is temporarily unavailable on our side the call is marked failed with a retryable message; the recording is kept and can be retried in the app.
Retry semantics
- Safe to repeat:
POST /api/v1/callswith the sameexternalCallIdand content (returns the stored call), and everyGET. - Retry after
Retry-After:429, and5xxresponses (use exponential back-off). - Do not retry unchanged:
400,401,402(allowance used up untilresetsAtor a plan change),409,413,422. - API keys carry a single scope,
calls:write; there is no read-only REST key, and the same key is needed to read a call.
Deletion and retention
There is no public API to delete a call. An organization admin can delete a call in the app: this removes its recording, transcript, evaluations and evidence, but does not give back the allowance it used. Raw recordings are scheduled for automatic deletion 90 days after upload by default; transcripts, reports and evidence are kept until you delete them. See security.
Versioning
The version is in the path (/api/v1) and in the OpenAPI document (info.version). Within v1 we only add optional request fields, response fields and error codes; a breaking change ships under a new path version with notice in these docs. Clients should ignore response fields they do not know.
Errors
Error bodies are JSON: { "error": "…", "code": "…" }; validation errors add details. Some 400 responses (a malformed call id or unreadable JSON) carry no code. There is no outbound provider-error code on this API: transcripts need no provider call.
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_payload | Validation failed; details lists each problem. |
| 401 | invalid_api_key | Missing, malformed, unknown or revoked key. |
| 402 | quota_exceeded | The organization used its call allowance for the current period. The body carries limit, used and resetsAt. Re-sending a call that is already stored is still accepted. |
| 403 | insufficient_scope | The key lacks the calls:write scope, or it is an MCP credential (those only work on /api/mcp). |
| 404 | — | No such call in the key’s organization (also returned for another organization’s call id). |
| 409 | call_conflict | The same externalCallId is already stored with different content. |
| 413 | — | The JSON body is larger than the limit. |
| 503 | temporarily_unavailable | The key store is briefly unavailable; retry after the Retry-After seconds. Not an invalid key. |
| 429 | rate_limited | Too many requests for this key; retry after the Retry-After seconds. |