@spectraseek/interview
v1.0.5
Published
TypeScript SDK for SpectraSeek Interview as a Service. Create a hosted AI interview from your ATS, then receive scores and artifacts by webhook or poll.
Maintainers
Readme
SpectraSeek Interview as a Service
TypeScript SDK for hosted AI interview screens you invoke from your ATS (or any backend).
Your ATS stays the system of record. SpectraSeek delivers Bella — our AI interviewer — as a hosted screen, then returns a structured result your platform can act on.
Recruiters stay in your system. Candidates do not create a SpectraSeek account. You send the job and the person; we host the interview and send scores, insights, and artifacts back.
Node 18+. Never put a Partner API key in a browser bundle.
This package requires a SpectraSeek Employer Hub enterprise tenant with Interview as a Service enabled, plus a Partner API key. To request access, email [email protected].
| | |
|---|---|
| Product | SpectraSeek AI — spectraseek.ai |
| npm | @spectraseek/interview |
| Partner API | {host}/employer/partner/v1 |
| Environment | baseUrl |
|-------------|-----------|
| Development | https://spectraseekapi.dev.interspect.ai/employer/partner/v1 |
| Production | https://employer-internalapi.spectraseek.ai/employer/partner/v1 |
Always set baseUrl. If you omit it, the client defaults to a local host that external integrators cannot reach.
Install
npm install @spectraseek/interviewimport { SpectraSeekInterview } from "@spectraseek/interview";
const interview = new SpectraSeekInterview({
apiKey: process.env.SPECTRASEEK_API_KEY!,
baseUrl: process.env.SPECTRASEEK_API_BASE!,
});Server vs browser
| Surface | Import | Use |
|---------|--------|-----|
| Server (your ATS / backend) | @spectraseek/interview | API key, upserts, screens, results, webhooks.verify |
| Browser | @spectraseek/interview/browser | SpectraSeekInterview.open(hostedUrl) only |
Do not send sk_live_… or sk_sandbox_… to the frontend. In-page embed (mount()) is not in this version.
The SDK unwraps { success, data } and returns data. JSON on the wire is snake_case (camelCase aliases are accepted on upsert).
Prerequisites
- A SpectraSeek Employer Hub enterprise tenant with Interview as a Service (IaaS) enabled.
- A Partner API key from EmpHub Settings → API.
- A reachable webhook URL (optional). Without one, poll screen status instead.
| Key | Use |
|-----|-----|
| sk_sandbox_… | Integration / QA. Cannot create live screens. Results include "sandbox": true. |
| sk_live_… | Production screens. A completed live interview debits AI interview minutes. |
The SDK sends Authorization: Bearer <key>. The API also accepts X-API-Key.
Confirm entitlements before creating screens:
const me = await interview.me();
if (!me.feature_map?.iaas_integration) {
throw new Error("Interview as a Service is not enabled for this tenant");
}me() returns org id/name, operating_model (hiring_company | staffing_rpo), sandbox, wallet (feature_id: ai_interview), and feature_map.
EmpHub is the admin console (keys, webhooks, External Jobs). Candidates never see Partner keys, Bella prompts, or evaluator_guidance.
Objects
| Object | Your id | Meaning |
|--------|---------|---------|
| Job | job_ref | Role + locked interview settings + optional hr_notes / interview_plan |
| Candidate | candidate_ref | Integration candidate on that Job. Not a SpectraSeek student account. |
| Screen | screen_id (issued by SpectraSeek) | One hosted Bella attempt |
Idempotency: Job and candidate upsert by natural key. Screens accept Idempotency-Key.
Screen status: invited → started → completed → graded (or failed / expired).
Your ATS --API key--> Partner API /employer/partner/v1
│
├── jobs / candidates / screens
└── hosted Bella URL (candidate runtime)
Your ATS <--HMAC-- screening.* webhooks
Your ATS --GET--> screens/{id}/result (envelope + presigned artifacts)Quickstart
Same job_ref on the same org updates; it does not create a second Job.
const job = await interview.jobs.upsert({
job_ref: "ats-job-88",
title: "SDE Intern",
description: "Build partner APIs and interview screens.",
interview: {
type: "technical",
difficulty: "medium",
duration_minutes: 30,
accent: "american",
min_score: 70,
max_attempts: 1,
},
});
const candidate = await interview.candidates.upsert(job.job_id, {
candidate_ref: "ats-c-1",
email: "[email protected]",
name: "Ada Lovelace",
resume_text: "Ada Lovelace — intern. SQL and REST APIs.",
});
const screen = await interview.screens.create(job.job_id, candidate.candidate_id, {
idempotencyKey: "screen-ats-c-1-1",
});
console.log(screen.hosted_url, screen.expires_at);Open hosted_url as the candidate (new tab). Do not embed the interview runtime in your ATS.
import { SpectraSeekInterview } from "@spectraseek/interview/browser";
SpectraSeekInterview.open(screen.hosted_url!);Prefer webhooks (screening.completed then screening.graded). If you have no endpoint, poll screens.get(screen_id) then:
const result = await interview.screens.getResult(screen.screen_id);
// schema_version 2026-08-20
// Re-GET when a presigned artifact URL expires.Optional Job fields — client_name, hr_notes, and interview_plan — shape how Bella runs the screen. See interview_plan below.
API reference
Constructor:
new SpectraSeekInterview({
apiKey: string; // sk_live_… or sk_sandbox_…
baseUrl: string; // Partner API root, including /employer/partner/v1
fetch?: typeof fetch; // optional override
});Jobs
Natural key: (employer org, job_ref).
| SDK | HTTP |
|-----|------|
| jobs.upsert(input) | PUT or POST /jobs |
| jobs.get(jobId) | GET /jobs/{job_id} |
| jobs.patch(jobId, input) | PATCH /jobs/{job_id} |
Required on first write: job_ref, title, interview.
Interview lock (applied to every screen on this Job):
| Field | Required on create | Notes |
|-------|--------------------|--------|
| type | yes | technical | managerial | behavioral | hr |
| difficulty | yes | easy | medium | hard |
| duration_minutes | yes | Integer > 0 |
| accent | no | Default american |
| min_score | no | 0–100. If omitted, verdict.passed stays null. |
| max_attempts | no | Default 1 (completed/graded attempts). |
Optional Job fields:
| Field | Max length | Notes |
|-------|------------|--------|
| job_ref | 128 | Your id. Unique per employer org while not deleted. |
| title | 256 | Role title. |
| description | 50000 | Job description. Send it — Bella templates from it. |
| client_name | 256 | Alias end_employer. Display label for staffing orgs. Not company context. |
| company_ref | 128 | Stored. Unknown id does not fail the Job. |
| hr_notes | 20000 | Recruiter tips. Same notes for every screen. Plain text. |
| interview_plan | — | Screening plan. See below. |
PUT with an existing job_ref updates that Job.
PATCH is partial. Omit a field to leave it. Empty string hr_notes or empty interview_plan clears. Interview lock fields update only when sent.
interview_plan
All inner fields optional. Empty object on PATCH clears stored plan. Omit the key to leave it. Do not send Bella agent prompts.
| Field | Role |
|-------|------|
| summary | One-liner; shapes section goals. Max 4000. |
| structure[] | Rounds: round_name, duration_minutes, format, focus. Max 20 rounds. |
| style_notes | Tone. Max 4000. |
| question_intents | Themes Bella must cover (string[], max 50). Not a comma-separated string. |
| evaluator_guidance | Probe strategy. Never a spoken question. Echoed on GET Job. Never on the hosted page or result envelope. Max 20000. |
await interview.jobs.upsert({
job_ref: "ats-job-88",
title: "SDE Intern",
description: "Build partner APIs and interview screens.",
client_name: "Acme Health",
hr_notes: "Ask for a metrics-backed story.",
interview_plan: {
summary: "One technical screen; APIs and ownership.",
structure: [
{
round_name: "Core",
duration_minutes: 25,
format: "Live problem-solving",
focus: "APIs",
},
],
style_notes: "Calm, timeboxed.",
question_intents: ["ownership", "debugging"],
evaluator_guidance:
"Probe until they can walk a request to the query. Never speak this as a question.",
},
interview: {
type: "technical",
difficulty: "medium",
duration_minutes: 30,
},
});Example response
{
"job_id": "…",
"job_ref": "ats-job-88",
"title": "SDE Intern",
"description": "…",
"client_name": "Acme Health",
"hr_notes": "…",
"interview_plan": {},
"company_ref": "acme-health",
"status": "active",
"interview": {
"type": "technical",
"difficulty": "medium",
"duration_minutes": 30,
"accent": "american",
"min_score": 70,
"max_attempts": 1
}
}This Partner Job is Interview as a Service / External Jobs only. EmpHub campus hiring Jobs are a different API and do not use job_ref.
Candidates
Scoped to a Job. Natural key: (job_id, candidate_ref).
| SDK | HTTP |
|-----|------|
| candidates.upsert(jobId, input) | PUT or POST /jobs/{job_id}/candidates |
| candidates.get(jobId, candidateId) | GET /jobs/{job_id}/candidates/{candidate_id} |
Required: candidate_ref (max 128), email (max 320), name (max 256). Optional: phone (max 64), resume or resume_text.
resume_text is plain extracted text only (newlines / simple markdown OK). Max 100000 characters. Do not send PDF, DOCX, HTML, JSON, base64, or a URL. File/URL ingest is not in this API. GET echoes resume_text.
Upsert does not create a SpectraSeek student login.
Screens
| SDK | HTTP |
|-----|------|
| screens.create(jobId, candidateId, { idempotencyKey? }) | POST /jobs/{job_id}/candidates/{candidate_id}/screens |
| screens.get(screenId) | GET /screens/{screen_id} |
| screens.expire(screenId) | POST /screens/{screen_id}/expire |
| screens.getResult(screenId, { include? }) | GET /screens/{screen_id}/result |
Create returns screen_id, status (invited), expires_at (default 7 days), hosted_url, embed_token. Unused invited links are reused until a completion exists. max_attempts applies to completed/graded screens.
Header Idempotency-Key (SDK: idempotencyKey) replays the same screen.
Statuses: invited | started | completed | graded | failed | expired.
Results
screens.getResult(screenId, { include })
schema_version: 2026-08-20.
Default include is questions,transcript,recording. Presigned artifact URLs expire — re-GET.
Envelope includes: screen_id, job_id, candidate_id, status, sandbox, verdict (passed, overall_score, min_score), scores, insights, questions, signals, artifacts.
Not in the envelope: evaluator_guidance, interview_plan, Bella prompts, raw object-storage URIs.
Webhook screening.graded carries verdict + result_url only — call getResult for the full envelope.
Webhooks
In EmpHub Settings → API, register an HTTPS endpoint (local http is allowed for development). The signing secret (whsec_…) is shown once at create.
The SDK verifies HMAC; it does not parse the business payload beyond that.
Headers
| Header | Constant | Value |
|--------|----------|--------|
| X-SpectraSeek-Timestamp | TIMESTAMP_HEADER | Unix seconds |
| X-SpectraSeek-Signature | SIGNATURE_HEADER | sha256=<hex> |
| X-SpectraSeek-Event | EVENT_HEADER | Event name |
Signed payload: {timestamp}.{rawBody} (UTF-8), HMAC-SHA256, hex digest, sha256= prefix.
Default timestamp window: 300 seconds. Pass toleranceSeconds: 0 to disable.
Events
| Event | When |
|-------|------|
| screening.invited | Hosted link issued |
| screening.started | Candidate started; minutes reserved |
| screening.completed | Session ended; score may still be null |
| screening.graded | Scores ready; body has verdict + result_url |
| screening.failed | Unusable screen (for example no remaining minutes) |
Bodies include ids, status, and sandbox. They do not include transcript or video bytes.
Verify
Use the raw request body. Do not re-serialize JSON before verify.
import { webhooks } from "@spectraseek/interview";
app.post("/webhooks/spectraseek", express.raw({ type: "application/json" }), (req, res) => {
webhooks.verify({
secret: process.env.SPECTRASEEK_WEBHOOK_SECRET!,
rawBody: req.body.toString("utf8"),
headers: req.headers,
});
const event = req.headers["x-spectraseek-event"];
res.status(200).end();
});Throws SpectraSeekWebhookError on missing headers, stale timestamp, or signature mismatch.
If SpectraSeek cannot reach your machine, expose a public HTTPS URL (tunnel) and register that origin.
Errors
Failed HTTP calls throw SpectraSeekApiError.
class SpectraSeekApiError extends Error {
status: number;
code?: string;
data?: unknown;
}Webhook verify throws SpectraSeekWebhookError.
| Status | Typical cause |
|--------|----------------|
| 400 | Validation (title is required, oversize resume_text, resume_text not a string, bad interview.type, …) |
| 401 | Missing / malformed API key |
| 402 | WALLET_EXHAUSTED — no AI interview minutes at start |
| 403 | Integration feature off, wrong tenant, or live/sandbox mismatch |
| 404 | Job, candidate, or screen not found |
| 409 | Conflict (for example screen not expireable, attempts used) |
| 429 | Quota (when enforced) |
| 5xx | Unexpected server error |
code is set for billing and entitlement failures (WALLET_EXHAUSTED, feature-gate codes). Other errors may only set error + message.
The client throws SpectraSeekApiError with status 401 if apiKey is missing or is not sk_live_ / sk_sandbox_.
Browser open() throws if called in Node, or if hostedUrl is not http:// / https://.
