Skip to main content

API Endpoints

Complete reference for all InsightAgent API endpoints.

Interviews

List Interviews

GET /api/interviews

Query parameters (optional):

ParameterTypeDescription
pagenumberPage number (1-indexed)
limitnumberResults per page (max: 100)
statusstringFilter by status: NOT_SCHEDULED, SCHEDULED, IN_PROGRESS, COMPLETED
searchstringSearch by interview title or expert name (case-insensitive)
completedAfterstring (ISO 8601)Return only interviews with completedAt strictly after this timestamp. Switches the response to be sorted by completedAt.
sortOrderstringasc (default) or desc. Only honored when completedAfter is provided.

Note: The IN_PROGRESS status filter also includes interviews with DIALING status.

Incremental sync pattern

Use completedAfter to fetch only interviews completed since your last sync — useful when reconciling state after a missed webhook. Pair it with status=COMPLETED for the typical case:

GET /api/interviews?status=COMPLETED&completedAfter=2026-04-28T12:00:00Z

Comparison is strictly greater than (>, not >=). Store the completedAt of the last interview you ingested and pass it on the next poll. Using > semantics avoids double-processing rows with equal completedAt.

By default the response is sorted by completedAt ascending (oldest-first) so consumers can iterate completion-order with offset pagination. Pass sortOrder=desc to get newest-first instead. Malformed values return a 400 with a descriptive error.

When page and limit are provided, returns paginated response:

{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8
}
}

Without pagination parameters, returns a plain array of interviews.

Get Interview

GET /api/interviews/{id}

Returns the full interview details including sessions with media URLs and AI-generated analysis.

Response:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Technical Interview",
"interviewType": "FULL_INTERVIEW",
"status": "COMPLETED",
"statusReason": null,
"questions": ["Tell me about your experience..."],
"expertData": {
"name": "John Doe",
"title": "Senior Engineer",
"employmentHistory": [...]
},
"scheduledAt": "2024-12-01T10:00:00.000Z",
"completedAt": "2024-12-01T10:45:00.000Z",
"totalConversationSeconds": 2700,
"sessionCount": 1,
"summary": "The interview covered the candidate's extensive experience in distributed systems...",
"quotes": [
{
"quote": "The key to scaling is understanding your bottlenecks before they become problems",
"speaker": "expert",
"context": "Discussing approach to system design",
"timestamp_secs": 342
}
],
"analysisGeneratedAt": "2024-12-01T11:00:00.000Z",
"sessions": [
{
"id": "session-uuid",
"startedAt": "2024-12-01T10:00:00.000Z",
"endedAt": "2024-12-01T10:45:00.000Z",
"durationSeconds": 2700,
"platform": "WEB",
"isActive": true,
"isProcessed": true,
"disconnectReason": "completed_by_expert",
"audioUrl": "https://storage.example.com/signed-audio-url",
"transcriptUrl": "https://storage.example.com/signed-transcript-url"
}
],
"resolvedAgent": {
"id": "agent-uuid",
"name": "Technical Interview Agent",
"agentType": "FULL_INTERVIEW"
}
}

Interview Status

FieldTypeDescription
statusstringLifecycle stage: NOT_SCHEDULED, SCHEDULED, DIALING, WAITING_FOR_EXPERT, IN_PROGRESS, COMPLETED
statusReasonstring | nullHuman-readable reason the interview reached its current status. Populated on dial-related failures (e.g. Call busy (attempt 2/3), IVR navigation failed: ..., expert_no_show, Failed to initiate call after N attempts). For web-only interviews, this field is typically null — session-level outcomes live in each session's disconnectReason.

Analysis Fields

FieldTypeDescription
summarystring | nullAI-generated executive summary of the interview
quotesarray | nullNotable quotes extracted from the expert's responses
quotes[].quotestringExact verbatim quote from the transcript
quotes[].speakerstringAlways expert (agent quotes are filtered out)
quotes[].contextstringWhy this quote is significant
quotes[].timestamp_secsnumberTimestamp in seconds
analysisGeneratedAtstring | nullISO8601 datetime when analysis was generated

Analysis is automatically generated when an interview completes. Fields are null for interviews that haven't been analyzed yet.

The sessions array contains all interview sessions with signed media URLs (valid for 1 hour). Use the convenience endpoints below for simpler access to the primary session's media.

Session Fields

FieldTypeDescription
idstring (uuid)Session identifier
startedAtstring | nullISO8601 datetime when the session started
endedAtstring | nullISO8601 datetime when the session ended
durationSecondsnumberConnected time in seconds (always tracked, even without a transcript)
platformstringWEB or PHONE
isActivebooleanWhether this is the currently active session
isProcessedbooleanWhether post-processing has produced audio and transcript
disconnectReasonstring | nullWhy the session ended — see values below
audioUrlstring | nullSigned URL to the session audio (1-hour expiry, populated once processed)
transcriptUrlstring | nullSigned URL to the session transcript (1-hour expiry, populated once processed)

disconnectReason values:

ValuePlatformMeaning
completed_by_expertWeb, PhoneExpert ended the session intentionally (clicked "I'm done" or otherwise signalled completion)
voice_session_disconnectWebVoice session closed without an explicit completion signal, after expert speech had been captured
user_audio_silentWebSession ended without any detectable expert speech — typically caused by a microphone or device problem on the expert's side (wrong default input, muted hardware, broken capture pipeline). Distinct from voice_session_disconnect, which indicates the session had audio but ended unexpectedly
client_disconnectWebExpert's browser/client disconnected (tab closed, network drop)
grace_period_expiredWebSession ended after the expert went silent past the standard grace window without an explicit completion signal
switched_to_phoneWebExpert switched from the web interview to the phone fallback mid-session. A new PHONE session is created on the same interview and continues from there
call_endedPhoneCall ended mid-conversation without an explicit completion signal
call_ended_earlyPhoneCall ended before meaningful dialogue (duration below the early-disconnect threshold)

disconnectReason is null while a session is still active. Only completed_by_expert indicates a successful/intentional end — all other values represent unexpected disconnections or transitions. A completed_by_expert session does not guarantee a transcript: if the expert ended within the first few seconds, transcriptUrl will be null, but durationSeconds is always populated.

Multi-session interviews

A single interview can contain one or more sessions — the sessions array is ordered by startedAt descending. The number and type of sessions depends on how the expert engaged:

  • Web only: one WEB session, ending with one of the web disconnectReason values.
  • Phone only (expert went straight to phone, never opened the web link): one PHONE session, ending with one of the phone disconnectReason values. No switched_to_phone marker is written — that value is only emitted when an active web session was closed in favor of phone.
  • Web → phone fallback: two sessions. The WEB session ends with switched_to_phone; a new PHONE session is created on the same interview and ends with one of the phone values.

The final outcome of an interview is reflected by the most recent session's disconnectReason. switched_to_phone is a handoff, not an outcome — skip it when classifying success/failure. durationSeconds on each session is that session's connected time; totalConversationSeconds on the interview is the aggregate across all sessions.

Phone sessions share the same lifecycle regardless of how the call was initiated — scheduled automatic dial-in, manual "Launch AI Agent" from the dashboard, web-to-phone fallback, or inbound dial-in (expert dials a provided number). All produce a PHONE session ending with one of the phone disconnectReason values.

Failed outbound call attempts do not create a session. If a scheduled or manual dial-in hits busy, no-answer, canceled, or failed on the carrier side, no row appears in sessions. The attempt is tracked on the interview itself via statusReason (e.g. Call busy (attempt 2/3), Failed to initiate call after N attempts). Only calls that actually connect produce a session record.

Create Interview

POST /api/interviews

Body:

{
"interviewType": "FULL_INTERVIEW | VETTING_INTERVIEW (required)",
"title": "string (required)",
"callSubject": "string (required)",
"expertData": {
"name": "string (required)",
"title": "string (required)",
"employmentHistory": [
{
"title": "string (required)",
"company": "string (required)",
"period": "string"
}
]
},
"questions": ["string"] (at least one required),
"status": "NOT_SCHEDULED | SCHEDULED (optional)",
"scheduledAt": "ISO8601 datetime (optional)",
"dialInPhone": "string",
"meetingId": "string",
"dialInPin": "string",
"agentGroupId": "uuid (optional)",
"compliance": "string",
"customVariables": {
"company_background": "string",
"research_focus": "string"
},
"notifications": [
{
"endpoint": "https://your-server.com/webhook",
"attributes": {
"method": "POST",
"headers": { "Authorization": "Bearer your-token" }
}
}
]
}

Required fields:

  • interviewType - Must be FULL_INTERVIEW or VETTING_INTERVIEW
  • title - Interview title
  • callSubject - Subject/topic for the call
  • questions - Array with at least one question
  • expertData.name - Expert's full name
  • expertData.title - Expert's job title
  • expertData.employmentHistory - At least one employment entry
  • expertData.employmentHistory[].title - Job title for each employment entry
  • expertData.employmentHistory[].company - Company name for each employment entry

Status:

  • status - Explicitly set the interview status to NOT_SCHEDULED or SCHEDULED. If omitted, status is derived from scheduledAt (set → SCHEDULED, omitted → NOT_SCHEDULED).

Agent Selection:

  • agentGroupId - UUID of the agent group to use for this interview. Get available agent group IDs from GET /api/agents. If omitted, the account's default agent for the given interviewType is used.

Custom Variables:

  • customVariables - Key-value pairs for agent prompt variables
  • Required variables (defined in Settings → Variables) must be provided
  • Returns 400 MISSING_REQUIRED_VARIABLES error if required variables are missing

Webhooks:

  • notifications - Array of webhook subscriptions to notify when the interview completes. See Webhooks for full details.

Update Interview

PUT /api/interviews/{id}

Complete Interview with Session

POST /api/interviews/{id}/complete-with-session/{sessionId}

Promotes the chosen session as the canonical one for the interview and marks the interview as COMPLETED. Use this when an interview has been left in IN_PROGRESS because the session ended without a completed_by_expert signal (network drop, tab closed, expert went silent, etc.) and you've decided the conversation is actually finished.

This is the same operation the dashboard performs when an admin completes an interview using a specific session.

Path parameters:

ParameterDescription
idInterview UUID
sessionIdSession UUID — must belong to the interview and have isProcessed: true

No request body is required.

What this does:

  1. Promotes the chosen session's transcript, recording, and analysis to the interview level (matches what GET /api/interviews/{id} returns at the top level).
  2. Marks the chosen session isActive: true and all other sessions on the interview isActive: false.
  3. Sets the interview's status to COMPLETED and populates completedAt.
  4. Regenerates AI analysis using the chosen session's transcript.

Calling the endpoint again with a different sessionId swaps the canonical session and regenerates analysis (recorded in the changelog as a session switch).

Response (200):

{ "success": true }

Error responses:

StatusCause
400The chosen session has isProcessed: false — wait for processing to complete and try again
403The interview belongs to a different account
404Bad id or sessionId, or the session belongs to a different interview
500Server-side promotion or analysis error

When to use it:

  • An interview is IN_PROGRESS because the expert disconnected without an explicit completion (e.g. closed the tab, network drop, went silent past the grace window). The captured session contains a usable conversation and you want to mark the interview done.
  • An interview has multiple sessions (e.g. expert reconnected after a drop) and you want to designate one of them as the canonical recording/transcript.
  • You have a programmatic sweep that retroactively closes interviews left IN_PROGRESS for longer than your acceptable window with at least one substantive session.

Interviews left IN_PROGRESS after a non-completed_by_expert disconnect are kept that way deliberately so experts can rejoin and continue. Use this endpoint when you've decided the interview is actually finished.

Delete Interview

DELETE /api/interviews/{id}

Get Transcription

GET /api/interviews/{id}/transcription

Returns the transcript from the primary session as an array of entries:

[
{
"role": "agent | user",
"message": "string",
"timestamp_secs": number
}
]

For interviews with multiple sessions, use the sessions array from the Get Interview endpoint to access individual session transcripts via their transcriptUrl.

Get Live Transcript

GET /api/interviews/{id}/transcript/live

Returns cached transcript events for an in-progress interview. Use this to observe the conversation in real-time.

Response:

{
"events": [
{
"type": "agent",
"text": "Hello, thank you for joining today's interview.",
"timestamp": "2026-01-26T10:00:05.000Z",
"sessionId": "session-uuid"
},
{
"type": "user",
"text": "Thanks for having me.",
"timestamp": "2026-01-26T10:00:12.000Z",
"sessionId": "session-uuid"
}
]
}

Notes:

  • Available for interviews with status IN_PROGRESS or COMPLETED (during processing)
  • Returns 400 if interview is not in progress
  • Events are cached temporarily during the interview

Get Audio

GET /api/interviews/{id}/audio

Returns a signed URL for the audio recording from the primary session.

Response:

{
"url": "https://storage.example.com/signed-audio-url",
"expiresIn": 3600
}

The signed URL is valid for 1 hour. For interviews with multiple sessions, use the sessions array from the Get Interview endpoint to access individual session recordings.

Get Interview Stats

GET /api/interviews/stats

Returns aggregate counts of interviews by status for the current account.

Response:

{
"notScheduled": 5,
"scheduled": 12,
"inProgress": 2,
"completed": 87,
"total": 106,
"abandoned": 1
}
FieldDescription
notScheduledInterviews created but not yet scheduled
scheduledInterviews scheduled for a future time
inProgressInterviews currently active (includes DIALING status)
completedFinished interviews
totalTotal interview count
abandonedIn-progress interviews with no activity for 5+ minutes

AI Agents

List Agents

GET /api/agents

Returns all agents available to the current account, including system default agents and custom agents.

Query parameters:

ParameterValuesDefaultDescription
roleROOT, TRANSFER_ONLY, ALLROOTFilter by agent role. ROOT returns agents that can start an interview. TRANSFER_ONLY returns only sub-agents reachable via transfer rules. ALL returns both. An invalid value returns 400.

Response:

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Technical Interview Agent",
"agentType": "FULL_INTERVIEW",
"language": "en",
"agentPersonaName": "Riley",
"firstMessage": "Hello! Can you hear me well?",
"customPrompt": "# Goal\nConduct thorough technical interviews...",
"description": "Custom agent for senior engineering interviews",
"role": "ROOT",
"voiceId": "9b1c2d3e-4f5a-6b7c-8d9e-0a1b2c3d4e5f",
"isDefault": false,
"versionNumber": 3,
"createdAt": "2025-12-06T20:04:24.513Z",
"updatedAt": "2025-12-08T11:10:44.837Z"
}
]

role is ROOT (can start an interview and transfer to sub-agents) or TRANSFER_ONLY (reachable only via a transfer rule). voiceId is the voice the agent speaks with (see List Voices) and may be null for agents created before voice selection.

Get Agent

GET /api/agents/{id}

Retrieve a specific agent configuration by its unique identifier.

Response:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Technical Interview Agent",
"agentType": "FULL_INTERVIEW",
"language": "en",
"agentPersonaName": "Riley",
"firstMessage": "Hello! Can you hear me well?",
"customPrompt": "# Goal\nConduct thorough technical interviews...",
"description": "Custom agent for senior engineering interviews",
"role": "ROOT",
"voiceId": "9b1c2d3e-4f5a-6b7c-8d9e-0a1b2c3d4e5f",
"isDefault": false,
"versionNumber": 3,
"createdAt": "2025-12-06T20:04:24.513Z",
"updatedAt": "2025-12-08T11:10:44.837Z"
}

Create Agent

POST /api/agents

Body:

{
"name": "string (required)",
"agentType": "FULL_INTERVIEW | VETTING_INTERVIEW (required)",
"language": "string (required, ISO 639-1 code)",
"agentPersonaName": "string (optional)",
"customPrompt": "string (optional, max 10000 chars)",
"description": "string (optional, max 500 chars)",
"firstMessage": "string (optional)",
"role": "ROOT | TRANSFER_ONLY (optional, default ROOT)",
"voiceId": "string (optional, UUID — see List Voices)"
}

Supported languages: en, es, fr, de, it, pt, pl, hi, zh, ko, ru, nl, tr, sv, id, fil, ja, uk, el, cs, fi, ro, da, bg, ms, sk, hr, ar, ta, vi, hu, no

role is honored on create only — an existing agent's role cannot be changed. voiceId defaults to the first voice for the selected language. A TRANSFER_ONLY agent never appears in interview agent selection and is reachable only as the target of a transfer rule.

Returns 202 Accepted - agent creation is asynchronous and creates both PHONE and WEB platform versions.

Update Agent

PUT /api/agents/{id}

Update an existing agent's configuration. Creates a new version in the agent's history. Accepts the same body as Create Agent except role (immutable after creation); voiceId may be updated and applies to both PHONE and WEB versions.

Delete Agent

DELETE /api/agents/{id}

Soft delete an agent (marked as inactive, not permanently removed).

Activate Agent

POST /api/agents/{id}/activate

Set an agent as the default for its type (affects both PHONE and WEB platforms).

Get Agent Variables

GET /api/agents/{id}/variables

Returns variables used by an agent's prompt, with their definitions and requirements.

Response:

[
{
"name": "company_background",
"displayName": "Company Background",
"description": "Context about the target company",
"isRequired": true,
"defaultValue": null
},
{
"name": "research_focus",
"displayName": "Research Focus",
"description": "Specific area to explore",
"isRequired": false,
"defaultValue": "general market research"
}
]

Multi-Agent Routing

A ROOT agent can hand the live conversation to a sub-agent partway through a call. Transfer rules define when those handoffs fire. Rules are scoped to the source agent's group ID ({groupId} is the agent id from the Agents endpoints).

A transfer rule has these fields:

FieldTypeDescription
targetGroupIdstring (UUID)Sub-agent to hand off to
conditionstringPlain-language condition; the agent decides mid-call whether it is met
orderingintegerEvaluation order when multiple rules exist (ascending)
delayMsintegerOptional delay before the transfer, in milliseconds
transferMessagestring | nullOptional audible message at the handoff; omit/null for a silent transfer
enableTargetFirstMessagebooleanWhether the target re-introduces itself (default false)

Mutating responses also return syncWarning (null when fully applied, otherwise the channels that failed to sync), and voiceWarning / nameWarning flags when the source and target agents differ in voice or persona name (the handoff would be audible to the expert).

List Transfer Rules (outbound)

GET /api/agents/{groupId}/transfer-rules

Rules where this agent is the source, in evaluation order, each annotated with voiceWarning / nameWarning.

List Transfer Rules (inbound)

GET /api/agents/{groupId}/transfer-rules/inbound

Rules where this agent is the target — every source agent that can hand off into it.

Create Transfer Rule

POST /api/agents/{groupId}/transfer-rules

Body:

{
"targetGroupId": "string (required, UUID)",
"condition": "string (required)",
"ordering": "integer (optional)",
"delayMs": "integer (optional)",
"transferMessage": "string | null (optional)",
"enableTargetFirstMessage": "boolean (optional, default false)"
}

Returns 201 Created with { rule, syncWarning, voiceWarning, nameWarning }. Returns 400 if the target would create a cycle or self-transfer.

Update Transfer Rule

PATCH /api/agents/{groupId}/transfer-rules/{ruleId}

Partial update — only the provided fields change. Same body fields as Create.

Delete Transfer Rule

DELETE /api/agents/{groupId}/transfer-rules/{ruleId}

Returns 204 No Content, or 200 with { ok, syncWarning } if the rule was deleted but the upstream re-sync partially failed.

Re-sync Transfer Rules

POST /api/agents/{groupId}/transfer-rules/resync

Force-pushes the agent's current transfer rules. Use after a create/update/delete reported a syncWarning. Returns { ok, failedChannels }.

List Agent Handoffs

GET /api/interviews/{id}/agent-handoffs

Returns the agent-to-agent handoffs that occurred during an interview, in order. Empty when the interview ran on a single agent.

Response:

{
"handoffs": [
{
"id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
"from": { "groupId": null, "displayName": null },
"to": { "groupId": "550e8400-e29b-41d4-a716-446655440000", "displayName": "Senior IC Interview" },
"occurredAt": "2026-05-18T14:22:09.117Z",
"turnOffsetMs": 84213,
"rule": { "id": "f1e2d3c4-b5a6-9788-7766-554433221100" },
"condition": "Transfer here when the expert is a senior engineering leader"
}
]
}

from.groupId and from.displayName are null for the initial agent. Handoff records are read-only.


Voices

List Voices

GET /api/voices

Returns the curated voices an agent can speak with. Use a voice's id as the voiceId when creating or updating an agent.

Query parameters:

ParameterDescription
languageOptional ISO 639-1 code (e.g. en). When provided, only voices supporting that language are returned.

Response:

{
"voices": [
{
"id": "9b1c2d3e-4f5a-6b7c-8d9e-0a1b2c3d4e5f",
"name": "Riley",
"supportedLanguages": ["en", "es", "fr"]
}
]
}

Variables

Manage custom variables for agent prompts.

List Variables

GET /api/account/variables

Returns all custom variables defined for your account.

Response:

{
"variables": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "company_background",
"displayName": "Company Background",
"description": "Context about the target company",
"isRequired": true,
"defaultValue": null,
"usedByAgents": ["Technical Interview Agent"],
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}
]
}

Create Variable

POST /api/account/variables

Body:

{
"name": "string (required)",
"displayName": "string (required)",
"description": "string (optional)",
"isRequired": false,
"defaultValue": "string (optional)"
}

Variable name rules:

  • Must start with a letter (a-z, A-Z)
  • Can contain letters, numbers, and underscores
  • Must be unique within the account

Response: 201 Created with the created variable.

Error responses:

  • 400 INVALID_VARIABLE_NAME - Name doesn't meet format requirements
  • 409 VARIABLE_NAME_EXISTS - A variable with this name already exists

Get Variable

GET /api/account/variables/{id}

Returns a specific variable with its agent usage information.

Update Variable

PUT /api/account/variables/{id}

Body:

{
"displayName": "string (optional)",
"description": "string (optional)",
"isRequired": false,
"defaultValue": "string (optional)"
}

Notes:

  • The name field cannot be changed after creation
  • isRequired can only be changed from true to false (not vice versa)

Error responses:

  • 400 CANNOT_MAKE_VARIABLE_REQUIRED - Cannot change optional variable to required
  • 404 - Variable not found

Delete Variable

DELETE /api/account/variables/{id}

Deletes a variable. Variables currently used by agent prompts cannot be deleted.

Error responses:

  • 400 VARIABLE_IN_USE - Variable is referenced in agent prompts (includes list of agents)
  • 404 - Variable not found

API Keys

Manage API keys for programmatic access. All endpoints require authentication via Bearer token.

List API Keys

GET /api/api-keys

Returns all API keys for the authenticated user.

Response:

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production",
"maskedKey": "sk_live_a1b2c3d4...",
"prefix": "a1b2c3d4",
"createdAt": "2026-02-15T10:00:00.000Z",
"revokedAt": null
}
]

Create API Key

POST /api/api-keys

Body:

{
"name": "string (required)"
}

Response (201 Created):

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production",
"key": "sk_live_a1b2c3d4e5f6...",
"createdAt": "2026-02-15T10:00:00.000Z"
}

The full API key is returned only once at creation time. Store it securely.

Error responses:

  • 400 — Name is required, or maximum of 5 active keys reached

Rename API Key

PATCH /api/api-keys/{id}

Body:

{
"name": "string (required)"
}

Error responses:

  • 400 — Name is required
  • 404 — API key not found

Revoke API Key

DELETE /api/api-keys/{id}

Revokes the key immediately. Any requests using this key will be rejected.

Error responses:

  • 404 — API key not found

Auth

Get Current User

GET /api/auth/me

Returns the authenticated user's profile and account information.

Response:

{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"fullName": "John Doe",
"accountId": "660e8400-e29b-41d4-a716-446655440001",
"createdAt": "2025-01-01T00:00:00.000Z"
},
"account": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "Acme Corp",
"status": "ACTIVE",
"billingType": "MANUAL_INVOICE"
}
}

Billing

Read-only endpoints for the authenticated user's account billing. All endpoints are scoped server-side to req.user.accountId — calls only ever see the caller's own account.

List Invoices

GET /api/billing/invoices

Returns every invoice issued for your account, sorted by billingDate descending (newest first).

Response:

{
"invoices": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"accountId": "660e8400-e29b-41d4-a716-446655440001",
"qbInvoiceNumber": "1004",
"billingDate": "2026-06-01T00:00:00.000Z",
"amountCents": 124850,
"status": "ISSUED",
"issuedAt": "2026-06-01T00:00:00.000Z",
"dueAt": "2026-06-11T00:00:00.000Z",
"paidAt": null,
"qbUrl": null,
"lineItems": [
{
"label": "GROWTH subscription",
"amountCents": 99900,
"periodStart": "2026-06-01T00:00:00.000Z",
"periodEnd": "2026-07-01T00:00:00.000Z",
"type": "base"
},
{
"label": "White Label",
"amountCents": 50000,
"periodStart": "2026-06-01T00:00:00.000Z",
"periodEnd": "2026-07-01T00:00:00.000Z",
"type": "addon"
},
{
"label": "Overage (99 min × $0.50)",
"amountCents": 4950,
"periodStart": "2026-05-01T00:00:00.000Z",
"periodEnd": "2026-06-01T00:00:00.000Z",
"type": "overage",
"quantity": 99,
"unitLabel": "minutes",
"unitPriceCents": 50
},
{
"label": "SLA downtime credit (May 12 outage, per §7.1)",
"amountCents": -5000,
"periodStart": "2026-05-12T00:00:00.000Z",
"periodEnd": "2026-05-12T23:59:59.000Z",
"type": "credit"
}
],
"createdAt": "2026-06-01T00:00:00.000Z",
"updatedAt": "2026-06-01T00:00:00.000Z"
}
]
}

Invoice fields

FieldTypeDescription
idstringUUID
qbInvoiceNumberstringInvoice number as shown on the issued document
billingDatestring (ISO 8601)The date the invoice was issued / billed
amountCentsintegerTotal invoice amount in cents — equals sum(lineItems.amountCents) including any negatives
statusstringDRAFT, ISSUED, PAID, OVERDUE, or VOID
issuedAt / dueAt / paidAtstring | nullTimestamps for the corresponding lifecycle events
qbUrlstring | nullOptional deep-link to the source invoice
lineItemsarraySee below

Line item fields

Each invoice can carry line items from more than one period. The advance lines (next-cycle base + add-ons) reference the upcoming period; arrears overage lines reference the just-closed period; credit / discount / adjustment lines reference the affected dates of the incident.

FieldTypeDescription
labelstringHuman-readable description
amountCentsintegerPer-line amount in cents. Negative for credit, discount, or adjustment types
periodStartstring (ISO 8601)Start of the period this line covers
periodEndstring (ISO 8601)End of the period this line covers
typestring | nullOne of base, addon, overage, credit, discount, adjustment
quantityinteger | nullUnits consumed (e.g. minutes for overage)
unitLabelstring | nullThe unit name (e.g. "minutes")
unitPriceCentsinteger | nullPer-unit price in cents

Next Payment Preview

GET /api/billing/next-payment

Returns a forward-looking preview of the upcoming invoice — combining the next cycle's base + add-ons (advance) with the current cycle's projected overage (arrears). Computed each request from your live usage; not persisted as an invoice until the cycle closes.

Response:

{
"billingDate": "2026-06-01T00:00:00.000Z",
"amountCents": 217250,
"lineItems": [
{
"label": "GROWTH subscription",
"amountCents": 99900,
"periodStart": "2026-06-01T00:00:00.000Z",
"periodEnd": "2026-07-01T00:00:00.000Z",
"type": "base"
},
{
"label": "White Label",
"amountCents": 50000,
"periodStart": "2026-06-01T00:00:00.000Z",
"periodEnd": "2026-07-01T00:00:00.000Z",
"type": "addon"
},
{
"label": "SLA Addendum",
"amountCents": 25000,
"periodStart": "2026-06-01T00:00:00.000Z",
"periodEnd": "2026-07-01T00:00:00.000Z",
"type": "addon"
},
{
"label": "Overage (846 min × $0.50)",
"amountCents": 42350,
"periodStart": "2026-05-01T00:00:00.000Z",
"periodEnd": "2026-06-01T00:00:00.000Z",
"type": "overage",
"quantity": 846,
"unitLabel": "minutes",
"unitPriceCents": 50
}
]
}
FieldTypeDescription
billingDatestring (ISO 8601)When the next invoice will be issued (typically your subscription's currentPeriodEnd + 1 second)
amountCentsintegerProjected invoice total — sum(lineItems.amountCents)
lineItemsarraySame shape as the List Invoices response, but computed rather than persisted. Advance lines use the next-period plan rates (after any scheduled change). Arrears overage uses the current period's snapshot rates — so an upgrade that takes effect at the next cycle does not retroactively change pricing for usage already metered.

Returns 404 No active subscription found if the account has no active or trialing subscription.

Subscription

GET /api/billing/subscription

Returns the current subscription plus any pending scheduled change. The scheduledChange field reflects an upgrade or downgrade queued to apply at the next billing cycle boundary.

Response (excerpt):

{
"tier": "STARTER",
"status": "active",
"monthlyPriceCents": 49900,
"includedMinutes": 1000,
"overageRateCents": 50,
"addons": [
{ "name": "white_label", "label": "White Label", "monthlyCents": 50000 },
{ "name": "sla", "label": "SLA Addendum", "monthlyCents": 25000 }
],
"currentPeriodStart": "2026-05-01T00:00:00.000Z",
"currentPeriodEnd": "2026-06-01T00:00:00.000Z",
"scheduledChange": {
"effectiveDate": "2026-06-01T00:00:00.000Z",
"tier": "GROWTH",
"monthlyPriceCents": 99900,
"includedMinutes": 2500,
"overageRateCents": 40
}
}

scheduledChange is null when no change is queued. When set, the listed fields are the only fields that change at the boundary — omitted fields keep their current values (e.g. add-ons preserve unless explicitly replaced). Plan changes always take effect at the next cycle boundary; the current cycle continues at the existing rates and any current-cycle overage bills at the existing overage rate.


Webhooks (Post-Interview)

Receive a notification when an interview completes. Webhooks are configured per interview via the notifications field in the Create Interview request.

Setup

Add a notifications array when creating an interview:

{
"title": "Technical Interview",
"interviewType": "FULL_INTERVIEW",
"notifications": [
{
"endpoint": "https://your-server.com/webhook",
"attributes": {
"method": "POST",
"headers": {
"Authorization": "Bearer your-token",
"x-custom-header": "value"
}
}
}
]
}
FieldTypeDescription
endpointstringThe URL to receive the webhook POST
attributes.methodstringHTTP method (defaults to POST)
attributes.headersobjectCustom headers sent with each request

You can configure multiple webhook endpoints per interview.

Webhook Payload

When the interview completes, each configured endpoint receives:

{
"interviewId": "550e8400-e29b-41d4-a716-446655440000",
"success": true,
"completedAt": "2026-02-17T15:30:00.000Z",
"transcriptionUrl": "https://api.insightagent.io/api/interviews/{id}/transcription",
"audioUrl": "https://api.insightagent.io/api/interviews/{id}/audio"
}
FieldTypeDescription
interviewIdstringUUID of the completed interview
successbooleanWhether the interview completed successfully
completedAtstringISO8601 timestamp of completion
transcriptionUrlstringURL to fetch the full transcript (requires API key)
audioUrlstringURL to fetch the audio recording (requires API key)

The payload is sent as Content-Type: application/json.

Authentication

There is no platform-level request signing (HMAC, etc.) at this time. To secure your webhook endpoint, pass your own authentication headers via attributes.headers — these are included with every webhook request.

Retries

Webhooks are attempted once. If your endpoint is unreachable or returns an error, the notification is marked as failed with no automatic retry. We recommend ensuring high availability on your receiving endpoint, or polling GET /api/interviews/{id} as a fallback.