{"openapi":"3.0.3","info":{"title":"Orythara API","version":"1.0.0","description":"Run AI-conducted interviews from your ATS. Create an interview, then send the candidate the returned `interviewUrl` **exactly as-is** (do not build the URL yourself). Store `id` + `interviewUrl`, then pull back transcript, AI scoring, recording, and integrity. Authenticate with a tenant API key: `Authorization: Bearer sk_live_...`.\n\n**Completion webhook:** we POST `{ event: \"interview.completed\", data, createdAt }` to your `webhookUrl`. `data` includes an `integrity` summary. If a signing secret is configured for your tenant, each request carries an `X-Interview-Signature` header (hex HMAC-SHA256 of the raw body) — verify it before trusting the payload. Delivery is single-attempt today (no retries), so poll `GET /interviews/{id}` as a fallback.\n\n**Proctoring/integrity** is log-and-flag only: it never changes `overallScore` or rejects a candidate."},"servers":[{"url":"https://interview.orythara.ai/api/v1"}],"tags":[{"name":"Interviews","description":"Create and manage AI interview sessions. Orythara calls these endpoints from `/oryt/talent-pool/{candidateId}?tab=video-interviews`."},{"name":"Results","description":"Read transcript, scoring, recording, and usage data after an interview is active or completed."}],"security":[{"bearerAuth":[]}],"paths":{"/interviews":{"post":{"tags":["Interviews"],"summary":"Create an interview","description":"Creates an interview and returns a candidate `interviewUrl`. In Orythara, call this when the recruiter clicks Send Interview from the candidate detail Video Interviews tab. Set `candidate.externalId` to the Orythara candidate id, set `job.externalId` to the job order/project/requisition id when available, then store the returned `id`, `token`, `status`, and `interviewUrl` against the Orythara video-interview row. Either supply your own `questions` (questionMode \"provided\") or let the AI generate them from the job + candidate profile (questionMode \"generate\").","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInterviewRequest"},"examples":{"generate":{"summary":"Orythara talent-pool candidate, AI-generated questions","value":{"job":{"externalId":"REQ-123","title":"Senior Backend Engineer","details":"Go, distributed systems, 5+ yrs."},"candidate":{"externalId":"ca01000a-0000-0000-0000-00000000000a","name":"Fatou Diallo","email":"fatou@example.com","profile":"Talent-pool profile, CV summary, skills, experience, and notes from Orythara."},"questionMode":"generate","numQuestions":5,"deliveryMode":"avatar","webhookUrl":"https://orythara.example.com/api/video-interviews/webhooks/ai-interviewer"}},"provided":{"summary":"Your own questions, audio only","value":{"job":{"title":"Sales Development Rep"},"candidate":{"name":"Sam Rivera"},"questionMode":"provided","questions":["Walk me through a deal you closed.","How do you handle objections?"],"deliveryMode":"audio"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Interview"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"}}},"get":{"tags":["Interviews"],"summary":"List interviews","description":"Use this for reconciliation views or background sync. Orythara can filter by candidate.externalId client-side after reading each interview, or use its local `video_interviews` table as the primary candidate-scoped list.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending","active","completed","canceled"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of interviews","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Interview"}},"nextCursor":{"type":"string","nullable":true}}}}}}}}},"/interviews/{id}":{"get":{"tags":["Interviews"],"summary":"Get an interview","description":"Fetch the provider-side state for an interview that Orythara has already linked to a talent-pool candidate.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Interview"}}}},"404":{"$ref":"#/components/responses/Error"}}},"patch":{"tags":["Interviews"],"summary":"Cancel an interview","description":"Cancel an interview when the recruiter cancels or replaces a queued/invited session from Orythara.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["canceled"]}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Interview"}}}}}}},"/interviews/{id}/transcript":{"get":{"tags":["Results"],"summary":"Get the transcript","description":"Pull the candidate/interviewer turns into Orythara after completion so the transcript can appear on the Video Interviews detail page.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"interviewId":{"type":"string"},"status":{"type":"string"},"turns":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["interviewer","candidate"]},"content":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}}}}}}}},"/interviews/{id}/report":{"get":{"tags":["Results"],"summary":"Get the AI scoring report","description":"200 when ready; 202 while the interview is in progress or scoring is pending. Map `overallScore`, `recommendation`, `strengths`, and `concerns` back to the Orythara video interview detail and candidate score surfaces.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Report ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Report"}}}},"202":{"description":"Not ready yet"}}},"post":{"tags":["Results"],"summary":"Force (re)generate the report","description":"Administrative repair endpoint when a completed interview did not produce a report automatically.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Report generated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Report"}}}}}}},"/interviews/{id}/recording":{"get":{"tags":["Results"],"summary":"Download the candidate recording","description":"Download the recording (video+audio for avatar interviews, audio-only for voice). Only available when the interview's recording.status is \"ready\"; otherwise returns 404 no_recording.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Media file","content":{"video/webm":{"schema":{"type":"string","format":"binary"}},"audio/webm":{"schema":{"type":"string","format":"binary"}}}},"404":{"$ref":"#/components/responses/Error"}}}},"/usage":{"get":{"tags":["Results"],"summary":"Usage & billing summary","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Usage"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"sk_live_..."}},"responses":{"Error":{"description":"Error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}}}}},"schemas":{"CreateInterviewRequest":{"type":"object","required":["job"],"properties":{"job":{"type":"object","required":["title"],"properties":{"externalId":{"type":"string","description":"Your ATS job/requisition id (interviews reuse the same job role)."},"title":{"type":"string"},"description":{"type":"string"},"details":{"type":"string","description":"Full job description; used for AI question generation + scoring. Send this whenever you have it: with no job details there is nothing role-specific for question generation to work from, so questions drift toward the candidate profile instead of the job order. Supplying it on a request that reuses an existing externalId also refreshes the stored job role."},"maxDurationMin":{"type":"integer","minimum":1,"maximum":240,"nullable":true,"description":"Soft cap on interview length, in minutes. Null (or omitted) means no limit. The candidate sees a live countdown against this, and ORY wraps up rather than opening a new question once it is reached — it never interrupts an answer. Stored on the job role, so it applies to every interview for this externalId."},"answerRetries":{"type":"integer","minimum":0,"maximum":5,"description":"How many times a candidate may re-answer a question, per question. Default 0. Stored on the job role."}}},"candidate":{"type":"object","properties":{"externalId":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"profile":{"description":"Resume/profile text or JSON; used to tailor AI questions.","oneOf":[{"type":"string"},{"type":"object"}]}}},"questionMode":{"type":"string","enum":["generate","provided"],"default":"provided"},"questions":{"type":"array","items":{"oneOf":[{"type":"string"},{"type":"object","properties":{"text":{"type":"string"},"competency":{"type":"string"}}}]}},"numQuestions":{"type":"integer","default":5,"description":"Used when questionMode = generate."},"deliveryMode":{"type":"string","enum":["avatar","audio"],"description":"Controlled by your tenant provisioning, not the request. Tenants set to \"avatar\" or \"audio\" are locked to that mode and this field is ignored; sending the other mode returns 403. Only tenants provisioned for \"both\" may choose here (default avatar)."},"webhookUrl":{"type":"string","description":"Overrides the tenant default for this interview."}}},"Interview":{"type":"object","properties":{"id":{"type":"string"},"token":{"type":"string"},"interviewUrl":{"type":"string"},"status":{"type":"string","enum":["pending","active","completed","canceled"],"description":"SESSION LIFECYCLE only. \"completed\" means the session ended and cannot be resumed — including one the candidate stopped after 2 of 5 questions. Do not label a UI from this field alone; use `outcome.label`."},"completionState":{"type":"string","enum":["complete","partial","ended_early"],"nullable":true,"description":"Whether the candidate was actually ASSESSED. \"complete\" = every configured question was asked; \"partial\" = it finished without reaching them all (e.g. the duration cap); \"ended_early\" = the candidate stopped it. Null on interviews finalized before this was recorded, which are treated as complete. Anything other than \"complete\" makes the report withhold a hiring recommendation."},"outcome":{"type":"object","description":"The status as a human should read it, derived from `status` + `completionState`. Use this for lists so an interview cannot read \"Completed\" next to a report that says otherwise.","properties":{"state":{"type":"string","enum":["completed","incomplete","active","pending","canceled"]},"label":{"type":"string","example":"Incomplete"},"fullyAssessed":{"type":"boolean","description":"False whenever the report withholds a hiring recommendation."}}},"deliveryMode":{"type":"string","enum":["avatar","audio"]},"questionMode":{"type":"string"},"job":{"type":"object"},"candidate":{"type":"object"},"questions":{"type":"array","items":{"type":"object"}},"scoring":{"type":"object","nullable":true},"integrity":{"$ref":"#/components/schemas/IntegritySummary"},"recording":{"type":"object","description":"Recording state. Download via /interviews/{id}/recording only when status is \"ready\".","properties":{"status":{"type":"string","enum":["none","uploading","ready","failed"]},"available":{"type":"boolean"},"bytes":{"type":"integer"}}},"usage":{"type":"object"},"timestamps":{"type":"object"}}},"Report":{"type":"object","properties":{"interviewId":{"type":"string"},"ready":{"type":"boolean"},"status":{"type":"string","description":"Session lifecycle — see Interview.status."},"completionState":{"type":"string","enum":["complete","partial","ended_early"],"nullable":true,"description":"Why the recommendation may be withheld. Carried here so this response cannot contradict itself: it previously returned status \"completed\" alongside recommendation \"incomplete\" with nothing to explain the difference."},"outcome":{"type":"object","description":"Same shape as Interview.outcome."},"overallScore":{"type":"number","nullable":true,"description":"Null when too little of the interview was covered to score it honestly."},"recommendation":{"type":"string","enum":["strong_yes","yes","maybe","no","incomplete"],"description":"\"incomplete\" means no hiring recommendation is being made because the candidate was not fully assessed; check `completionState` for why."},"displayRecommendation":{"type":"string","nullable":true,"description":"The human-readable recommendation for display, with \" — High Integrity Risk, Manual Review Required\" appended when `integrity.level` is \"high_risk\". `recommendation` itself is NEVER changed by proctoring — this field exists so a UI or ATS reading only the headline verdict cannot show a clean \"Strong Yes\" for an interview proctoring flagged as likely cheating."},"integrityWarning":{"type":"boolean","description":"True when `integrity.level` is \"high_risk\" — the same condition `displayRecommendation` reflects, exposed as a boolean for consumers that branch on it instead of parsing text."},"candidateAnswerMode":{"type":"object","description":"How the candidate actually answered — spoken vs typed — as opposed to the interview-wide voiceMode/deliveryMode, which is the INTERVIEWER's channel and is fixed regardless of how the candidate responded.","properties":{"mode":{"type":"string","enum":["voice","text","mixed","unknown"]},"label":{"type":"string"}}},"report":{"type":"object","properties":{"overallScore":{"type":"number"},"recommendation":{"type":"string"},"summary":{"type":"string"},"strengths":{"type":"array","items":{"type":"string"}},"concerns":{"type":"array","items":{"type":"string"}},"competencies":{"type":"array","items":{"type":"object"}},"perQuestion":{"type":"array","items":{"type":"object"}}}},"integrity":{"$ref":"#/components/schemas/Integrity"}}},"Integrity":{"type":"object","nullable":true,"description":"Proctoring integrity result. Log-and-flag only — does not change overallScore.","properties":{"score":{"type":"integer","description":"0-100; higher = fewer red flags."},"level":{"type":"string","enum":["clear","review","high_risk"]},"flags":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"e.g. paste, tab_hidden, multiple_faces, answer_latency_anomaly"},"count":{"type":"integer"},"severity":{"type":"string","enum":["info","warn","critical"]},"label":{"type":"string"}}}}}},"IntegritySummary":{"type":"object","nullable":true,"description":"Compact integrity summary on the interview object; full flags come from the report endpoint.","properties":{"score":{"type":"integer"}}},"Usage":{"type":"object","properties":{"currency":{"type":"string"},"totalBilledUsd":{"type":"number"},"totalRawCostUsd":{"type":"number"},"interviews":{"type":"object"},"breakdown":{"type":"array","items":{"type":"object"}}}}}}}