# Aural Developer API — Full Reference > Complete API reference for the Aural REST API. For the OpenAPI spec, see /api/v1/openapi.json This document describes the public v1 HTTP API. All paths are relative to the base URL below unless noted. --- ## Authentication Every request must include a valid API key in the Authorization header: Authorization: Bearer dlv_ Create and manage keys in the Aural web app: Settings > API Keys. Keys are stored and transmitted as secrets. They are prefixed with "dlv_" so they are easy to recognize in code and logs (never log full keys). If the header is missing, malformed, or the key is invalid, revoked, or expired, the API returns 401 with code UNAUTHORIZED. If the key’s user has no organization membership, the API may return 403 FORBIDDEN with an explanatory message. Project scoping: the key resolves to a set of project IDs the user can access. Interview and session operations are limited to resources in those projects. Creating a new interview uses the first accessible project when multiple exist. --- ## Base URL Production: https://aural-ai.com/api/v1 When self-hosting, replace the host with your deployment’s public origin; the path prefix remains /api/v1. --- ## Response Format Success (single resource): { "data": { ... } } Success (list): { "data": [ ... ], "cursor": "" } The cursor field is used for pagination on list endpoints. Pass the returned cursor as a query parameter to fetch the next page. A null cursor means there is no next page. Error: { "error": { "code": "", "message": "", ...optional_fields } } Common error codes: UNAUTHORIZED — Missing/invalid API key (401) FORBIDDEN — Authenticated but not allowed (403) NOT_FOUND — Resource missing or not visible (404) BAD_REQUEST — Invalid input or JSON (400) PLAN_LIMIT_EXCEEDED — Organization plan limit hit (403, extra quota fields) INTERNAL_ERROR — Server or database error (500) --- ## Content type Request bodies must be JSON: Content-Type: application/json --- ## Common Workflow 1. POST /interviews — Create an interview template (draft; invite-only until published). 2. POST /interviews/{id}/questions — Add one or more questions (array or single object). 3. POST /interviews/{id}/publish — Activate the interview, disable invite-only requirement, ensure public slug, return shareable candidate URL. 4. Share the returned URL (or use invite links from POST /interviews/{id}/candidates) so candidates complete sessions with the AI. 5. GET /interviews/{id}/sessions — List sessions for the interview (paginated). 6. GET /sessions/{id} — Fetch full transcript (messages), summary, insights, themes, sentiment, and interview metadata. --- ## Endpoints Paths below omit the base URL prefix for readability (e.g. /interviews means https://aural-ai.com/api/v1/interviews). --- ### POST /interviews Create a new interview template in the API key user’s first accessible project. Request: Content-Type: application/json Authorization: Bearer dlv_xxx Body fields: title string REQUIRED Non-empty interview title description string optional Shown to candidates objective string optional What you want to learn assessmentCriteria array optional Array of { "name": string, "description": string } chatEnabled boolean default true voiceEnabled boolean default false videoEnabled boolean default false aiName string default "Aural" Display name for the AI interviewer aiTone string default "PROFESSIONAL" One of: CASUAL, PROFESSIONAL, FORMAL, FRIENDLY followUpDepth string default "MODERATE" One of: LIGHT, MODERATE, DEEP language string default "en" timeLimitMinutes integer optional Positive integer; session time cap antiCheatingEnabled boolean default false New interviews are created with requireInvite true and a generated publicSlug. They are not fully “public” until you call POST .../publish. Example: curl -X POST https://aural-ai.com/api/v1/interviews \ -H "Authorization: Bearer dlv_xxx" \ -H "Content-Type: application/json" \ -d '{"title": "Senior Engineer Interview", "voiceEnabled": true, "followUpDepth": "DEEP"}' Response (200): { "data": { "id": "", "title": "Senior Engineer Interview", "publicSlug": "", "projectId": "", "userId": "", "chatEnabled": true, "voiceEnabled": true, "...": "additional interview columns from database" } } Errors: 400 BAD_REQUEST — Invalid JSON, missing title, invalid assessmentCriteria shape, invalid timeLimitMinutes 400 BAD_REQUEST — No accessible project for this API key 401 UNAUTHORIZED — Invalid or missing API key 403 PLAN_LIMIT_EXCEEDED — Template (interview) limit reached for the organization Extra fields may include: limit: "templates", current, max (null if unlimited), upgrade_url 404 NOT_FOUND — Project not found (unexpected if projects resolved correctly) 500 INTERNAL_ERROR — Database or server error --- ### GET /interviews List interviews across all projects the API key can access, newest first. Query parameters: limit integer optional Page size, default 20, max 100 cursor string optional Interview id from previous page for stable pagination List items are interview rows with embedded counts; full question text is not included in this list (use GET /interviews/{id} for questions). Each item includes _count: "_count": { "questions": , "sessions": } Example: curl -s "https://aural-ai.com/api/v1/interviews?limit=10" \ -H "Authorization: Bearer dlv_xxx" Response (200): { "data": [ { "id": "...", "title": "...", "_count": { "questions": 3, "sessions": 12 }, "...": "..." } ], "cursor": "last_interview_id_or_null" } If the user has no accessible projects, data is an empty array and cursor is null. Errors: 401 UNAUTHORIZED 500 INTERNAL_ERROR --- ### GET /interviews/{id} Fetch one interview with all questions (ordered by question order) and a session count. Path: id uuid Interview id Response shape merges interview fields with: "questions": [ { ...question rows... } ], "_count": { "sessions": } Example: curl -s "https://aural-ai.com/api/v1/interviews/" \ -H "Authorization: Bearer dlv_xxx" Response (200): { "data": { "id": "", "title": "...", "questions": [ { "id": "...", "text": "...", "order": 0, "type": "OPEN_ENDED", "...": "..." } ], "_count": { "sessions": 5 } } } Errors: 401 UNAUTHORIZED 404 NOT_FOUND — Interview not found or not in an accessible project 500 INTERNAL_ERROR --- ### PATCH /interviews/{id} Partially update an interview. Only fields present in the body are updated. Updatable fields (same semantics as POST where applicable): title, description, objective, assessmentCriteria (array or null to clear), chatEnabled, voiceEnabled, videoEnabled, aiName, aiTone, followUpDepth, language, timeLimitMinutes (positive integer or null to clear), antiCheatingEnabled Validation highlights: title — non-empty string when provided assessmentCriteria — array of { name, description } or null aiTone / followUpDepth — same enums as POST timeLimitMinutes — positive integer or null If no recognized fields are supplied, returns 400 BAD_REQUEST "No valid fields to update." Example: curl -X PATCH "https://aural-ai.com/api/v1/interviews/" \ -H "Authorization: Bearer dlv_xxx" \ -H "Content-Type: application/json" \ -d '{"description": "Updated copy for candidates", "voiceEnabled": true}' Response (200): { "data": { ...updated interview row... } } Errors: 400 BAD_REQUEST — Invalid JSON or field validation failure 401 UNAUTHORIZED 404 NOT_FOUND 500 INTERNAL_ERROR --- ### DELETE /interviews/{id} Archive the interview (sets isActive to false). This is a soft delete from the API perspective. Response (200): { "data": { "id": "", "archived": true } } Errors: 401 UNAUTHORIZED 404 NOT_FOUND 500 INTERNAL_ERROR --- ### POST /interviews/{id}/publish Make the interview live for public link access: sets isActive true, requireInvite false, and ensures a publicSlug exists (generates one if missing). Response (200): { "data": { "id": "", "publicSlug": "", "url": "https://aural-ai.com/i/" } } The base URL in "url" uses NEXT_PUBLIC_APP_URL when set, otherwise https://aural-ai.com. Errors: 401 UNAUTHORIZED 404 NOT_FOUND — Interview not in accessible project 500 INTERNAL_ERROR --- ### GET /interviews/{id}/questions List all questions for an interview, ordered by order ascending. Response (200): { "data": [ { "id": "...", "interviewId": "...", "text": "...", "order": 0, "type": "OPEN_ENDED", ... } ] } Errors: 401 UNAUTHORIZED 403 FORBIDDEN — Interview exists but not accessible 404 NOT_FOUND — Interview not found 500 INTERNAL_ERROR --- ### POST /interviews/{id}/questions Add one or more questions. Body may be a single JSON object or an array of objects. Per-question fields: text string REQUIRED Non-empty question text type string optional Default OPEN_ENDED. One of: OPEN_ENDED, SINGLE_CHOICE, MULTIPLE_CHOICE, CODING, WHITEBOARD, RESEARCH order integer optional Non-negative; if omitted, appends after current max order required boolean optional Maps to isRequired; default true options string[] optional For choice-style questions; array of strings followUpEnabled boolean optional Maps to probeOnShort; default true Example (batch): curl -X POST "https://aural-ai.com/api/v1/interviews//questions" \ -H "Authorization: Bearer dlv_xxx" \ -H "Content-Type: application/json" \ -d '[{"text": "Describe a complex system you built."},{"text": "Pick your stack","type":"SINGLE_CHOICE","options":["Node","Python","Go"]}]' Response (200): { "data": [ { ...created question rows... } ] } Errors: 400 BAD_REQUEST — Empty array, invalid types, invalid options, invalid order, etc. 401 UNAUTHORIZED 403 FORBIDDEN 404 NOT_FOUND 500 INTERNAL_ERROR --- ### PATCH /questions/{id} Update a single question by id. Body is a JSON object with any subset of: text string non-empty when provided type string one of the QUESTION_TYPES (cannot be null) order integer non-negative (cannot be null) required boolean maps to isRequired options string[] or null followUpEnabled boolean maps to probeOnShort At least one valid field must be present. Example: curl -X PATCH "https://aural-ai.com/api/v1/questions/" \ -H "Authorization: Bearer dlv_xxx" \ -H "Content-Type: application/json" \ -d '{"text": "Clarified wording", "order": 2}' Response (200): { "data": { ...updated question... } } Errors: 400 BAD_REQUEST 401 UNAUTHORIZED 403 FORBIDDEN — Question’s interview is not in an accessible project 404 NOT_FOUND — Question not found 500 INTERNAL_ERROR --- ### DELETE /questions/{id} Permanently delete a question. Response (200): { "data": { "id": "", "deleted": true } } Errors: 401 UNAUTHORIZED 403 FORBIDDEN 404 NOT_FOUND 500 INTERNAL_ERROR --- ### GET /interviews/{id}/sessions Paginated list of sessions for an interview. Ordered by createdAt descending, then id descending. Query parameters: limit integer optional Default 20, max 100 cursor string optional Must be a valid session id from this interview; used for keyset pagination Each session in the list includes summary fields and a message count via _count (full message text is not included here; use GET /sessions/{id}). Response fields per row (typical): id, status, participantName, participantEmail, summary, insights, themes, sentiment, totalDurationSeconds, createdAt, updatedAt, "_count": { "messages": } Session status values (enum): IN_PROGRESS, COMPLETED, ABANDONED Example: curl -s "https://aural-ai.com/api/v1/interviews//sessions?limit=20" \ -H "Authorization: Bearer dlv_xxx" Response (200): { "data": [ { "id": "...", "status": "COMPLETED", "_count": { "messages": 42 }, "...": "..." } ], "cursor": "next_session_id_or_null" } Errors: 400 BAD_REQUEST — Invalid cursor 401 UNAUTHORIZED 403 FORBIDDEN 404 NOT_FOUND — Interview not found 500 INTERNAL_ERROR --- ### GET /sessions/{id} Fetch a single session with ordered messages (transcript) and parent interview metadata. Response (200): { "data": { "id": "", "status": "COMPLETED", "participantName": "...", "participantEmail": "...", "summary": "...", "insights": ..., "themes": ..., "sentiment": ..., "totalDurationSeconds": 1234, "createdAt": "...", "messages": [ { "id": "...", "role": "USER", "content": "...", "timestamp": "..." }, { "id": "...", "role": "ASSISTANT", "content": "...", "timestamp": "..." } ], "interview": { "id": "", "title": "...", "objective": "..." } } } Errors: 401 UNAUTHORIZED 403 FORBIDDEN — Session’s interview project not accessible 404 NOT_FOUND — Session or interview linkage missing 500 INTERNAL_ERROR --- ### GET /interviews/{id}/candidates List candidate invite rows for an interview. Response fields per row typically include: id, name, email, phone, notes, inviteToken, sessionId, createdAt Example: curl -s "https://aural-ai.com/api/v1/interviews//candidates" \ -H "Authorization: Bearer dlv_xxx" Response (200): { "data": [ { ... } ] } Errors: 401 UNAUTHORIZED 403 FORBIDDEN 404 NOT_FOUND 500 INTERNAL_ERROR --- ### POST /interviews/{id}/candidates Create one or more candidate invites. Body may be a single object or array (max 500 per request). Per-candidate fields: name string optional in schema (empty string allowed); display name for the invite email string optional; normalized to lowercase, empty becomes null phone string optional notes string optional Each row gets a unique inviteToken. Response includes inviteUrl for each candidate: inviteUrl: https://aural-ai.com/invite/ Before insert, the API checks the organization’s monthly session time allowance. If exceeded: 403 PLAN_LIMIT_EXCEEDED Extra fields may include: limit: "session_hours", used_hours, max_hours, upgrade_url Example: curl -X POST "https://aural-ai.com/api/v1/interviews//candidates" \ -H "Authorization: Bearer dlv_xxx" \ -H "Content-Type: application/json" \ -d '[{"name":"Alex","email":"alex@example.com"}]' Response (200): { "data": [ { "id": "...", "name": "Alex", "email": "alex@example.com", "inviteToken": "...", "inviteUrl": "https://aural-ai.com/invite/...", "...": "..." } ] } Errors: 400 BAD_REQUEST — Invalid JSON, empty batch, >500 items, bad object shape 401 UNAUTHORIZED 403 FORBIDDEN 403 PLAN_LIMIT_EXCEEDED — Monthly session hours exhausted 404 NOT_FOUND 500 INTERNAL_ERROR --- ### GET /usage Return plan and quota snapshot for the API key user’s primary organization. Response (200): { "data": { "plan": "", "templates": { "used": , "limit": }, "session_hours": { "used": , "limit": }, "ai_tokens": { "used": , "limit": }, "seats": { "used": , "limit": } } } Notes: templates.used / templates.limit — lifetime-style template count vs plan cap (null limit means unlimited templates on the plan). session_hours.used — hours consumed in the current billing/calendar accounting period used by the product (aggregated from recorded session duration). session_hours.limit — included hours per period for the plan. ai_tokens — organization AI token quota and estimated usage against that quota. seats — organization member seats vs plan seat cap. Example: curl -s "https://aural-ai.com/api/v1/usage" \ -H "Authorization: Bearer dlv_xxx" Errors: 401 UNAUTHORIZED 500 INTERNAL_ERROR --- ## Plan Limits Aural ties API capabilities to the organization’s subscription. Limits are enforced server-side; clients should handle 403 PLAN_LIMIT_EXCEEDED gracefully and direct users to billing when upgrade_url is present. ### Templates (interviews) Each interview template counts against the organization’s template allowance (with “unlimited” represented as null in usage responses). Creating an interview via POST /interviews runs checkTemplateLimit; when the cap is reached, the API responds with 403 and code PLAN_LIMIT_EXCEEDED, including limit: "templates", current count, max (or null), and upgrade_url. Archiving or deleting behavior for quota reclamation follows product rules in the database; the API’s DELETE /interviews/{id} archives the row (isActive false). ### Session hours Included interview session time is allocated per billing period (calendar month style usage is tracked from session duration in the backend). Adding candidates via POST /interviews/{id}/candidates checks remaining monthly session time; if exhausted, you receive PLAN_LIMIT_EXCEEDED with limit: "session_hours", used_hours, max_hours, and upgrade_url. Listing or reading sessions does not consume session hours; consumption is tied to running/completing interview time according to backend metering. ### AI tokens Plans include an AI token pool for model usage. Token consumption during live interviews and analysis reduces the available balance. GET /usage exposes ai_tokens.used and ai_tokens.limit so integrations can surface remaining quota. Specific write endpoints may return PLAN_LIMIT_EXCEEDED for token-related operations elsewhere in the product; treat 403 + code as authoritative. ### 403 PLAN_LIMIT_EXCEEDED When any limit trips, the error object includes: "code": "PLAN_LIMIT_EXCEEDED", "message": "", "limit": "", ...additional numeric context fields..., "upgrade_url": "" Integrations should log the code and limit kind, not the full API key. ### Checking quota proactively Call GET /usage before bulk operations (batch candidate creation, automated template creation) to avoid partial failures. Values are approximate for rapidly changing systems; the enforcement on each mutating request remains the source of truth. --- ## Operational notes - Rate limits: follow fair use; heavy automation should backoff on 429/5xx if introduced in future. - Idempotency: POST create endpoints are not idempotent; use client-generated deduplication if needed. - Timestamps: ISO-8601 strings as returned by the database. - Self-hosted deployments must configure NEXT_PUBLIC_APP_URL so publish URLs and invite links point to the correct origin. --- ## Quick reference (methods and paths) POST /interviews GET /interviews GET /interviews/{id} PATCH /interviews/{id} DELETE /interviews/{id} POST /interviews/{id}/publish GET /interviews/{id}/questions POST /interviews/{id}/questions PATCH /questions/{id} DELETE /questions/{id} GET /interviews/{id}/sessions GET /sessions/{id} GET /interviews/{id}/candidates POST /interviews/{id}/candidates GET /usage End of reference.