Integration API (A2A)
REST endpoint reference
All 15 integration endpoints under /api/v2/integration — create interviews, manage the HITL workflow, and retrieve structured assessment results.
| Method | Path | Required state | Description |
|---|---|---|---|
POST | /integration/interviews | — | Submit an interview request |
GET | /integration/interviews/:run_id | Any | Get current status |
GET | /integration/interviews/:run_id/plan | ≥ PENDING | Get the generated plan |
PATCH | /integration/interviews/:run_id/info | INFO_NEEDED | Supply missing data |
POST | /integration/interviews/:run_id/plan/approve | PENDING | Approve the plan |
POST | /integration/interviews/:run_id/plan/reject | PENDING | Reject the plan (terminal) |
PATCH | /integration/interviews/:run_id/plan/revise | PENDING | Request targeted revisions |
POST | /integration/interviews/:run_id/invite | APPROVED / INVITED | Mint a candidate join link |
POST | /integration/interviews/:run_id/assessment/approve | ASSESSMENT_READY | Approve the assessment |
POST | /integration/interviews/:run_id/assessment/reject | ASSESSMENT_READY | Reject the assessment (terminal) |
GET | /integration/interviews/:run_id/assessment | ASSESSMENT_READY+ | Fetch full assessment result |
GET | /integration/interviews/:run_id/transcript | COMPLETED+ | Get interview transcript |
GET | /integration/rankings | — | Candidate rankings by coverage tier |
DELETE | /integration/interviews/:run_id | Non-terminal | Cancel an interview |
DELETE | /integration/interviews/:run_id/candidate-data | Any | Purge candidate PII (GDPR) |
Authentication
All endpoints require authentication scoped to your tenant. Two methods are supported — pick whichever fits your stack.
Option A — API key (recommended)
X-API-Key: tc_live_xxxxxxxxxxxx
X-Tenant-ID: ten_01j2kpq...Option B — OAuth 2.1 Bearer
Obtain a short-lived token via the client_credentials grant at POST /api/v2/oauth/token, then pass it as a Bearer header. Tenant scope is baked into the token — no X-Tenant-ID header needed. See OAuth for the full flow.
Authorization: Bearer eyJhbGciOiJFUzI1NiJ9...Create interview
POST /api/v2/integration/interviews
Submit a candidate for an AI-led interview. Returns immediately — plan readiness arrives via interview.plan_generated webhook. Duplicate external_id within a tenant returns the existing run (200, not 409).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
external_id | string | Yes | Your ATS application/job ID — enables idempotent retries (max 200 chars) |
candidate_ref | string | One of * | Opaque partner-side candidate ID — preferred, avoids sending PII |
candidate_name | string | One of * | Full name — only when no candidate_ref is available |
candidate_email | string | One of * | Email — required if OTP delivery is needed |
position | string | Yes | Job title being interviewed for (max 200 chars) |
level | enum | Yes | ENTRY · MID · SENIOR · LEAD · EXEC |
qualifications | string[] | One of ** | Natural-language recruiter qualification statements (max 100 items) |
skills | string[] | One of ** | Explicit skill names — when qualifications is not available |
job_description | string | One of ** | Full role description (max 20 000 chars) |
company_name | string | No | Company name used in Maya's opening greeting |
company_description | string | No | Brief company overview (max 4 000 chars) |
candidate_resume | string | No | Resume text — helps Maya generate targeted questions (max 60 000 chars) |
candidate_profile_url | string | No | Public profile URL (must be valid HTTPS) |
duration_minutes | integer | No | Target interview length in minutes (15–120, default 45) |
callback_url | string | No | Per-run webhook override — must be HTTPS; SSRF-checked on delivery |
candidate_ref, candidate_name, or candidate_email. candidate_ref is preferred — it identifies the candidate without transmitting PII.** Qualifications: provide at least one of
qualifications (non-empty), skills (non-empty), or job_description. If the data is insufficient the interview enters INFO_NEEDED — a webhook lists exactly what is missing.Example — qualification-first (recommended)
curl -X POST https://devapi.teamcast.ai/api/v2/integration/interviews \
-H "X-API-Key: tc_live_xxxx" -H "X-Tenant-ID: ten_01j2..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "ats-app-a1b2c3d4",
"candidate_ref": "li_app_a1b2c3d4",
"position": "Senior Software Engineer",
"level": "SENIOR",
"qualifications": [
"5+ years production TypeScript experience",
"Demonstrated ability to design distributed systems at scale",
"Strong async/concurrency patterns knowledge"
],
"company_name": "Acme Corp",
"duration_minutes": 45
}'{
"run_id": "run_1720000000_a1b2",
"interview_id": "int_01j2kpq...",
"state": "GENERATING_PLAN",
"message": "plan generation started"
}Get status
GET /api/v2/integration/interviews/:run_id
Returns the current state of a run. When the run reaches ASSESSMENT_READY or COMPLETED, the response includes a coverage_summary.
{
"run_id": "run_1720000000_a1b2",
"interview_id": "int_01j2kpq...",
"state": "PENDING",
"awaiting_human": true,
"coverage_summary": null // populated when state = ASSESSMENT_READY or COMPLETED
}Get plan
GET /api/v2/integration/interviews/:run_id/plan
Returns the generated interview plan once the run has advanced past GENERATING_PLAN. Returns 404 if the plan is not yet available.
{
"run_id": "run_1720000000_a1b2",
"interview_id": "int_01j2kpq...",
"payload": { /* full plan object with competencies and question topics */ }
}Supply missing info
PATCH /api/v2/integration/interviews/:run_id/info
Answer the missing fields listed in the interview.info_needed webhook when the run is in INFO_NEEDED state. Triggers plan regeneration and returns immediately.
curl -X PATCH https://devapi.teamcast.ai/api/v2/integration/interviews/run_.../info \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{
"fields": {
"job_description": "Full role description text...",
"skills": "TypeScript, AWS, System Design"
}
}'fields is a Record<string, string> — a flat object where each key is the missing field name from missing_fields[].field. Webhooks fired: interview.info_completed immediately, then interview.plan_generated or interview.info_needed when regeneration settles.Approve, reject, or revise plan
# Approve — advances to APPROVED
curl -X POST https://devapi.teamcast.ai/api/v2/integration/interviews/run_.../plan/approve \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{ "comment": "Looks good — solid competency coverage" }'
# Reject — terminal REJECTED state
curl -X POST https://devapi.teamcast.ai/api/v2/integration/interviews/run_.../plan/reject \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{ "reason": "Role requirements changed" }'
# Revise — returns to GENERATING_PLAN then to PENDING when ready
curl -X PATCH https://devapi.teamcast.ai/api/v2/integration/interviews/run_.../plan/revise \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{ "comments": "Focus more on system design. Reduce basic syntax questions." }'{
"run_id": "run_1720000000_a1b2",
"interview_id": "int_01j2kpq...",
"state": "APPROVED",
"message": "plan approved"
}POST .../invite after approval to mint it. Webhook interview.approved fires on approval.Issue candidate invite
POST /api/v2/integration/interviews/:run_id/invite
Mint a one-time join link and OTP for the candidate. Only callable when state is APPROVED or INVITED — returns 409 otherwise.
curl -X POST https://devapi.teamcast.ai/api/v2/integration/interviews/run_.../invite \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{
"ttl_seconds": 259200,
"base_url": "https://app.teamcast.ai"
}'{
"run_id": "run_1720000000_a1b2",
"interview_id": "int_01j2kpq...",
"joinLink": "https://app.teamcast.ai/interview/join/eyJhbGc...",
"otp": "847291",
"expiresAt": "2026-07-17T10:00:00.000Z"
}joinLink and otp are never persisted after this response — capture and deliver them immediately. The default TTL is 72 hours (ttl_seconds: 259200).Field naming:
joinLink and expiresAt are camelCase — an intentional deviation from the snake_case convention used throughout the rest of the API. Parse these field names accordingly.Approve or reject assessment
After interview.assessment_ready fires, review the coverage summary and call one of these two endpoints. The run must be in ASSESSMENT_READY state.
# Approve — advances to COMPLETED
curl -X POST https://devapi.teamcast.ai/api/v2/integration/interviews/run_.../assessment/approve \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{ "comment": "Coverage looks solid. Approving." }'
# Reject — terminal REJECTED state
curl -X POST https://devapi.teamcast.ai/api/v2/integration/interviews/run_.../assessment/reject \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{ "reason": "Assessment quality insufficient — significant gaps remain." }'{
"run_id": "run_1720000000_a1b2",
"interview_id": "int_01j2kpq...",
"state": "COMPLETED",
"message": "assessment approved",
"coverage_summary": {
"overall_coverage": "sufficient",
"gap_competencies": ["distributed-systems"],
"covered_count": 5,
"total_count": 6
}
}Get assessment
GET /api/v2/integration/interviews/:run_id/assessment
Pull the full assessment result once the run reaches ASSESSMENT_READY or COMPLETED. Use this as a polling alternative to the interview.assessment_ready webhook. Returns 404 if scoring has not yet completed.
{
"run_id": "run_1720000000_a1b2",
"interview_id": "int_01j2kpq...",
"overall_coverage": "sufficient",
"payload": {
"coverage_summary": {
"overall_coverage": "sufficient",
"gap_competencies": ["leadership"],
"competencies_well_evidenced": 2,
"competencies_with_gaps": 1,
"hitl_flags": ["unmet_essential:leadership"]
},
"competencies": [
{
"competency_id": "analytical",
"score": 0.35,
"role_importance": 1.2,
"requirement_type": "essential",
"sub_dimension_scores": { "problem_diagnosis": 0.47, "decision_quality": 0.24 },
"sub_dimension_weights": { "problem_diagnosis": 0.5, "decision_quality": 0.5 }
}
// ... one per competency
],
"sub_dimension_details": [
{
"competency_id": "analytical",
"sub_dimension_id": "problem_diagnosis",
"score": 0.47,
"rubric": 1.0,
"observed_signals": 3,
"coverage_factor": 1.0
}
],
"traces": [
{
"signal_id": "diagnostic_reasoning",
"competency_id": "analytical",
"sub_dimension_id": "problem_diagnosis",
"strength": 0.8,
"merged_duplicates": 0,
"refs": [
{
"utterance_id": "u6",
"evidence_type_id": "direct_quote",
"strength": 0.8,
"quote": "So we first take all the loggers and track out...",
"measures": { "quality": 0.8, "confidence": 0.888, "contribution": 0.639 /* ...full block */ }
}
]
}
// ... one per scored signal (dark signals have strength 0 and no refs)
],
"narrative": {
"highlights_text": "Strongest evidence in problem diagnosis...",
"lowlights_text": "Leadership was thinly surfaced...",
"method": "deterministic"
},
"highlights": { "episodes": [], "gaps": [], "lowlights": [] },
// Additive 13-stage projection for the recruiter Detailed view — present on recently-scored
// payloads, omitted on legacy ones. Full shape: /docs/measurement/deliverable
"stage_trace": { "stages": [ /* 13 */ ], "segmentation": [], "evidence_units": [], "era_cot": null },
"ontology_version": "4.0.0",
"signal_weights_version": "1.3.0"
}
}score is an internal 0–1 evidence weight, not a rating — there are no hire/no-hire labels and no emotion inference fields anywhere in the payload. The decision is overall_coverage (sufficient / partial / insufficient) plus its supporting gap analysis. See Coverage & the deliverable for field-by-field detail and the full stage_trace schema.Get transcript
GET /api/v2/integration/interviews/:run_id/transcript
Returns the full interview transcript once the run has completed. Each entry in transcript carries the speaker, turn text, and a timestamp offset from the interview start.
{
"run_id": "run_1720000000_a1b2",
"interview_id": "int_01j2kpq...",
"transcript": [], // array of { speaker, text, offset_ms }
"proctor_events": [] // proctoring signal events
}Rankings
GET /api/v2/integration/rankings
Returns completed interviews ranked by coverage tier (sufficient → partial → insufficient), then ascending gap count. Only interviews with an approved assessment are included.
Query parameters
| Parameter | Type | Description |
|---|---|---|
position | string | Case-insensitive substring filter on job title |
level | enum | ENTRY · MID · SENIOR · LEAD · EXEC |
curl "https://devapi.teamcast.ai/api/v2/integration/rankings?position=Senior+Engineer&level=SENIOR" \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..."[{
"rank": 1,
"interview_id": "int_01j2kpq...",
"run_id": "run_1720000000_a1b2",
"candidate_name": "Alex Rivera",
"candidate_email": null,
"position": "Senior Software Engineer",
"level": "senior",
"overall_coverage": "sufficient",
"gap_count": 1,
"created_at": "2026-07-14T09:00:00.000Z"
}]level field in each ranking result is lowercase (entry, mid, senior, lead, exec) — unlike the ?level= query parameter which expects uppercase. Normalize with .toUpperCase() before comparing against a SeniorityLevel enum.Cancel and GDPR purge
# Cancel a still-in-flight run → terminal CANCELLED state
curl -X DELETE https://devapi.teamcast.ai/api/v2/integration/interviews/run_... \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{ "reason": "Position filled before interview completed" }'
# Purge candidate PII — recording, transcript, resume, name, email all nulled
# The assessment and audit trail are retained. Irreversible.
curl -X DELETE https://devapi.teamcast.ai/api/v2/integration/interviews/run_.../candidate-data \
-H "X-API-Key: ..." -H "X-Tenant-ID: ..." \
-H "Content-Type: application/json" \
-d '{ "reason": "Retention window elapsed" }'{
"purged": true,
"purged_at": "2026-07-14T10:00:00.000Z"
}Error responses
| HTTP status | When |
|---|---|
| 400 | Body fails Zod validation — message lists failing fields. Unknown keys also rejected. |
| 401 | Missing or invalid X-API-Key / X-Tenant-ID / Bearer token |
| 403 | Feature disabled for this tenant, or permission not held |
| 404 | run_id not found within this tenant |
| 409 | Action not permitted in the current state (e.g. approve when already REJECTED) |
| 422 | Not implemented endpoint — do not depend on it |
| 500 | Unexpected server error |
{
"statusCode": 400,
"message": "level: Invalid enum value. Expected 'ENTRY' | 'MID' | 'SENIOR' | 'LEAD' | 'EXEC'"
}