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.

MethodPathRequired stateDescription
POST/integration/interviewsSubmit an interview request
GET/integration/interviews/:run_idAnyGet current status
GET/integration/interviews/:run_id/plan≥ PENDINGGet the generated plan
PATCH/integration/interviews/:run_id/infoINFO_NEEDEDSupply missing data
POST/integration/interviews/:run_id/plan/approvePENDINGApprove the plan
POST/integration/interviews/:run_id/plan/rejectPENDINGReject the plan (terminal)
PATCH/integration/interviews/:run_id/plan/revisePENDINGRequest targeted revisions
POST/integration/interviews/:run_id/inviteAPPROVED / INVITEDMint a candidate join link
POST/integration/interviews/:run_id/assessment/approveASSESSMENT_READYApprove the assessment
POST/integration/interviews/:run_id/assessment/rejectASSESSMENT_READYReject the assessment (terminal)
GET/integration/interviews/:run_id/assessmentASSESSMENT_READY+Fetch full assessment result
GET/integration/interviews/:run_id/transcriptCOMPLETED+Get interview transcript
GET/integration/rankingsCandidate rankings by coverage tier
DELETE/integration/interviews/:run_idNon-terminalCancel an interview
DELETE/integration/interviews/:run_id/candidate-dataAnyPurge 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)

http
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.

http
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

FieldTypeRequiredDescription
external_idstringYesYour ATS application/job ID — enables idempotent retries (max 200 chars)
candidate_refstringOne of *Opaque partner-side candidate ID — preferred, avoids sending PII
candidate_namestringOne of *Full name — only when no candidate_ref is available
candidate_emailstringOne of *Email — required if OTP delivery is needed
positionstringYesJob title being interviewed for (max 200 chars)
levelenumYesENTRY · MID · SENIOR · LEAD · EXEC
qualificationsstring[]One of **Natural-language recruiter qualification statements (max 100 items)
skillsstring[]One of **Explicit skill names — when qualifications is not available
job_descriptionstringOne of **Full role description (max 20 000 chars)
company_namestringNoCompany name used in Maya's opening greeting
company_descriptionstringNoBrief company overview (max 4 000 chars)
candidate_resumestringNoResume text — helps Maya generate targeted questions (max 60 000 chars)
candidate_profile_urlstringNoPublic profile URL (must be valid HTTPS)
duration_minutesintegerNoTarget interview length in minutes (15–120, default 45)
callback_urlstringNoPer-run webhook override — must be HTTPS; SSRF-checked on delivery
* Identity: provide at least one of 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)

bash
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
  }'
json
{
  "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.

Response 200
{
  "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.

Response 200
{
  "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.

bash
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

bash
# 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." }'
Approve response 200
{
  "run_id":       "run_1720000000_a1b2",
  "interview_id": "int_01j2kpq...",
  "state":        "APPROVED",
  "message":      "plan approved"
}
Next step after approval: the candidate join link is NOT included in the approve response. Call 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.

bash
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"
  }'
Response 200
{
  "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"
}
One-time only. 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.

bash
# 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." }'
Approve response 200
{
  "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.

Response 200
{
  "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"
  }
}
Coverage only. The per-competency 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.

Response 200
{
  "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

ParameterTypeDescription
positionstringCase-insensitive substring filter on job title
levelenumENTRY · MID · SENIOR · LEAD · EXEC
bash
curl "https://devapi.teamcast.ai/api/v2/integration/rankings?position=Senior+Engineer&level=SENIOR" \
  -H "X-API-Key: ..." -H "X-Tenant-ID: ..."
Response 200 — array
[{
  "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"
}]
The 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

bash
# 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" }'
Purge response 200
{
  "purged":    true,
  "purged_at": "2026-07-14T10:00:00.000Z"
}

Error responses

HTTP statusWhen
400Body fails Zod validation — message lists failing fields. Unknown keys also rejected.
401Missing or invalid X-API-Key / X-Tenant-ID / Bearer token
403Feature disabled for this tenant, or permission not held
404run_id not found within this tenant
409Action not permitted in the current state (e.g. approve when already REJECTED)
422Not implemented endpoint — do not depend on it
500Unexpected server error
Error body
{
  "statusCode": 400,
  "message":    "level: Invalid enum value. Expected 'ENTRY' | 'MID' | 'SENIOR' | 'LEAD' | 'EXEC'"
}
Was this page helpful?