API Endpoints
Complete reference for all InsightAgent API endpoints.
Interviews
List Interviews
GET /api/interviews
Query parameters (optional):
| Parameter | Type | Description |
|---|---|---|
page | number | Page number (1-indexed) |
limit | number | Results per page (max: 100) |
status | string | Filter by status: NOT_SCHEDULED, SCHEDULED, IN_PROGRESS, COMPLETED |
search | string | Search by interview title or expert name (case-insensitive) |
completedAfter | string (ISO 8601) | Return only interviews with completedAt strictly after this timestamp. Switches the response to be sorted by completedAt. |
sortOrder | string | asc (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
| Field | Type | Description |
|---|---|---|
status | string | Lifecycle stage: NOT_SCHEDULED, SCHEDULED, DIALING, WAITING_FOR_EXPERT, IN_PROGRESS, COMPLETED |
statusReason | string | null | Human-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
| Field | Type | Description |
|---|---|---|
summary | string | null | AI-generated executive summary of the interview |
quotes | array | null | Notable quotes extracted from the expert's responses |
quotes[].quote | string | Exact verbatim quote from the transcript |
quotes[].speaker | string | Always expert (agent quotes are filtered out) |
quotes[].context | string | Why this quote is significant |
quotes[].timestamp_secs | number | Timestamp in seconds |
analysisGeneratedAt | string | null | ISO8601 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
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Session identifier |
startedAt | string | null | ISO8601 datetime when the session started |
endedAt | string | null | ISO8601 datetime when the session ended |
durationSeconds | number | Connected time in seconds (always tracked, even without a transcript) |
platform | string | WEB or PHONE |
isActive | boolean | Whether this is the currently active session |
isProcessed | boolean | Whether post-processing has produced audio and transcript |
disconnectReason | string | null | Why the session ended — see values below |
audioUrl | string | null | Signed URL to the session audio (1-hour expiry, populated once processed) |
transcriptUrl | string | null | Signed URL to the session transcript (1-hour expiry, populated once processed) |
disconnectReason values:
| Value | Platform | Meaning |
|---|---|---|
completed_by_expert | Web, Phone | Expert ended the session intentionally (clicked "I'm done" or otherwise signalled completion) |
voice_session_disconnect | Web | Voice session closed without an explicit completion signal, after expert speech had been captured |
user_audio_silent | Web | Session 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_disconnect | Web | Expert's browser/client disconnected (tab closed, network drop) |
grace_period_expired | Web | Session ended after the expert went silent past the standard grace window without an explicit completion signal |
switched_to_phone | Web | Expert 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_ended | Phone | Call ended mid-conversation without an explicit completion signal |
call_ended_early | Phone | Call 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
WEBsession, ending with one of the webdisconnectReasonvalues. - Phone only (expert went straight to phone, never opened the web link): one
PHONEsession, ending with one of the phonedisconnectReasonvalues. Noswitched_to_phonemarker is written — that value is only emitted when an active web session was closed in favor of phone. - Web → phone fallback: two sessions. The
WEBsession ends withswitched_to_phone; a newPHONEsession 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 beFULL_INTERVIEWorVETTING_INTERVIEWtitle- Interview titlecallSubject- Subject/topic for the callquestions- Array with at least one questionexpertData.name- Expert's full nameexpertData.title- Expert's job titleexpertData.employmentHistory- At least one employment entryexpertData.employmentHistory[].title- Job title for each employment entryexpertData.employmentHistory[].company- Company name for each employment entry
Status:
status- Explicitly set the interview status toNOT_SCHEDULEDorSCHEDULED. If omitted, status is derived fromscheduledAt(set →SCHEDULED, omitted →NOT_SCHEDULED).
Agent Selection:
agentGroupId- UUID of the agent group to use for this interview. Get available agent group IDs fromGET /api/agents. If omitted, the account's default agent for the giveninterviewTypeis used.
Custom Variables:
customVariables- Key-value pairs for agent prompt variables- Required variables (defined in Settings → Variables) must be provided
- Returns
400 MISSING_REQUIRED_VARIABLESerror 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:
| Parameter | Description |
|---|---|
id | Interview UUID |
sessionId | Session UUID — must belong to the interview and have isProcessed: true |
No request body is required.
What this does:
- Promotes the chosen session's transcript, recording, and analysis to the interview level (matches what
GET /api/interviews/{id}returns at the top level). - Marks the chosen session
isActive: trueand all other sessions on the interviewisActive: false. - Sets the interview's
statustoCOMPLETEDand populatescompletedAt. - 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:
| Status | Cause |
|---|---|
400 | The chosen session has isProcessed: false — wait for processing to complete and try again |
403 | The interview belongs to a different account |
404 | Bad id or sessionId, or the session belongs to a different interview |
500 | Server-side promotion or analysis error |
When to use it:
- An interview is
IN_PROGRESSbecause 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_PROGRESSfor 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_PROGRESSorCOMPLETED(during processing) - Returns
400if 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
}
| Field | Description |
|---|---|
notScheduled | Interviews created but not yet scheduled |
scheduled | Interviews scheduled for a future time |
inProgress | Interviews currently active (includes DIALING status) |
completed | Finished interviews |
total | Total interview count |
abandoned | In-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:
| Parameter | Values | Default | Description |
|---|---|---|---|
role | ROOT, TRANSFER_ONLY, ALL | ROOT | Filter 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:
| Field | Type | Description |
|---|---|---|
targetGroupId | string (UUID) | Sub-agent to hand off to |
condition | string | Plain-language condition; the agent decides mid-call whether it is met |
ordering | integer | Evaluation order when multiple rules exist (ascending) |
delayMs | integer | Optional delay before the transfer, in milliseconds |
transferMessage | string | null | Optional audible message at the handoff; omit/null for a silent transfer |
enableTargetFirstMessage | boolean | Whether 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:
| Parameter | Description |
|---|---|
language | Optional 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 requirements409 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
namefield cannot be changed after creation isRequiredcan only be changed fromtruetofalse(not vice versa)
Error responses:
400 CANNOT_MAKE_VARIABLE_REQUIRED- Cannot change optional variable to required404- 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 required404— 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
| Field | Type | Description |
|---|---|---|
id | string | UUID |
qbInvoiceNumber | string | Invoice number as shown on the issued document |
billingDate | string (ISO 8601) | The date the invoice was issued / billed |
amountCents | integer | Total invoice amount in cents — equals sum(lineItems.amountCents) including any negatives |
status | string | DRAFT, ISSUED, PAID, OVERDUE, or VOID |
issuedAt / dueAt / paidAt | string | null | Timestamps for the corresponding lifecycle events |
qbUrl | string | null | Optional deep-link to the source invoice |
lineItems | array | See 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.
| Field | Type | Description |
|---|---|---|
label | string | Human-readable description |
amountCents | integer | Per-line amount in cents. Negative for credit, discount, or adjustment types |
periodStart | string (ISO 8601) | Start of the period this line covers |
periodEnd | string (ISO 8601) | End of the period this line covers |
type | string | null | One of base, addon, overage, credit, discount, adjustment |
quantity | integer | null | Units consumed (e.g. minutes for overage) |
unitLabel | string | null | The unit name (e.g. "minutes") |
unitPriceCents | integer | null | Per-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
}
]
}
| Field | Type | Description |
|---|---|---|
billingDate | string (ISO 8601) | When the next invoice will be issued (typically your subscription's currentPeriodEnd + 1 second) |
amountCents | integer | Projected invoice total — sum(lineItems.amountCents) |
lineItems | array | Same 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"
}
}
}
]
}
| Field | Type | Description |
|---|---|---|
endpoint | string | The URL to receive the webhook POST |
attributes.method | string | HTTP method (defaults to POST) |
attributes.headers | object | Custom 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"
}
| Field | Type | Description |
|---|---|---|
interviewId | string | UUID of the completed interview |
success | boolean | Whether the interview completed successfully |
completedAt | string | ISO8601 timestamp of completion |
transcriptionUrl | string | URL to fetch the full transcript (requires API key) |
audioUrl | string | URL 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.