Integration API (A2A)

Webhooks

Signed, retried HTTP POSTs delivered as an interview run progresses through its lifecycle.

Register a receiver once (see API Reference → Webhook configuration) and Teamcast pushes an event for every state transition instead of you polling interview.status.

Event catalog

EventFires when
interview.info_neededRequired candidate/role data is missing
interview.info_completedMissing fields have been supplied
interview.plan_generatedA plan is ready and awaiting approval
interview.approvedThe plan (or assessment) was approved
interview.rejectedThe plan or assessment was rejected
interview.assessment_readyThe coverage-only assessment was approved and delivered
interview.completedThe run reached its terminal completed state
interview.cancelledThe run was cancelled
interview.modification_requestedA plan revision was requested

Envelope

Every event shares the same top-level envelope. All fields are snake_case.

FieldTypeDescription
eventstringEvent type — see catalog above
run_idstringRun identifier returned by the create endpoint
interview_idstringInterview database ID
tenant_idstringTenant identifier — use to route in multi-tenant setups
statestringWorkflow state at the time of this event
timestampISO-8601Event time — also echoed in X-Webhook-Timestamp header
dataobjectEvent-specific payload — shapes documented below

Event payloads

The data field shape varies by event. All examples below show the complete data object only — wrap in the envelope above for the full POST body.

interview.plan_generated — state: PENDING
{
  "event": "interview.plan_generated",
  "run_id": "run_1735689600_a1f9c2",
  "interview_id": "int_7d2c...",
  "tenant_id": "tnt_8a21b6",
  "state": "PENDING",
  "timestamp": "2026-07-05T09:10:00.000Z",
  "data": {
    "plan_id": "pln_4e91...",
    "message": "plan ready for review"
  }
}
interview.approved — state: APPROVED
{
  "event": "interview.approved",
  "run_id": "run_1735689600_a1f9c2",
  "interview_id": "int_7d2c...",
  "tenant_id": "tnt_8a21b6",
  "state": "APPROVED",
  "timestamp": "2026-07-05T09:11:00.000Z",
  "data": {
    "approved": true,
    "interview_link": "https://app.teamcast.ai/interview/join/tok_abc123",
    "inmail_draft": "Hi Sarah,\n\nWe'd like to invite you to complete a short AI-powered interview for the Senior Engineer role at Acme Corp.\n\nClick here to begin: https://app.teamcast.ai/interview/join/tok_abc123\n\nBest,\nThe Acme Recruiting Team"
  }
}
interview.assessment_ready — state: ASSESSMENT_READY
{
  "event": "interview.assessment_ready",
  "run_id": "run_1735689600_a1f9c2",
  "interview_id": "int_7d2c...",
  "tenant_id": "tnt_8a21b6",
  "state": "ASSESSMENT_READY",
  "timestamp": "2026-07-05T09:12:03.000Z",
  "data": {
    "coverage_summary": {
      "overall_coverage": "partial",
      "gap_competencies": ["system design at scale"],
      "covered_count": 5,
      "total_count": 6
    },
    "highlights_count": 3,
    "lowlights_count": 1,
    "overall_coverage": "partial",
    "recording": {
      "chunks": [
        { "timestamp": 0, "url": "https://storage.googleapis.com/tc-recordings/..." }
      ],
      "audio": { "url": "https://storage.googleapis.com/tc-recordings/..." },
      "expiresAtMs": 1754390000000
    }
  }
}
interview.info_needed — state: INFO_NEEDED
{
  "event": "interview.info_needed",
  "run_id": "run_1735689600_a1f9c2",
  "interview_id": "int_7d2c...",
  "tenant_id": "tnt_8a21b6",
  "state": "INFO_NEEDED",
  "timestamp": "2026-07-05T09:09:00.000Z",
  "data": {
    "message": "Required fields are missing",
    "missing_fields": [
      {
        "field": "qualifications",
        "severity": "HIGH",
        "reason": "No qualifications or skills provided",
        "question": "Please provide at least one qualification or skill for this role"
      }
    ]
  }
}
interview.rejected — state: REJECTED
{
  "event": "interview.rejected",
  "run_id": "run_1735689600_a1f9c2",
  "interview_id": "int_7d2c...",
  "tenant_id": "tnt_8a21b6",
  "state": "REJECTED",
  "timestamp": "2026-07-05T09:11:30.000Z",
  "data": {
    "approved": false,
    "reason": "Plan questions do not align with the seniority level required"
  }
}
interview.cancelled — state: CANCELLED
{
  "event": "interview.cancelled",
  "run_id": "run_1735689600_a1f9c2",
  "interview_id": "int_7d2c...",
  "tenant_id": "tnt_8a21b6",
  "state": "CANCELLED",
  "timestamp": "2026-07-05T09:15:00.000Z",
  "data": {}
}

Signing & headers

HeaderValue
X-Webhook-Signaturesha256=HMAC-SHA256(secret, "{timestamp}.{rawBody}") — timestamp prefix prevents replay attacks
X-Webhook-TimestampISO-8601 — same value embedded in the payload body; used as the prefix in the signed string
X-Tenant-IDThe owning tenant — cross-check against your own records

The secret is either your /me/webhook-config secret, or the shared global secret when using a per-run callback_url override.

Verify a delivery

typescript
import { createHmac, timingSafeEqual } from 'node:crypto';

// The signed string is "{timestamp}.{rawBody}" — the timestamp prefix binds the
// signature to a freshness window so a captured delivery can't be replayed forever.
function verifyTeamcastWebhook(
  rawBody: string,
  timestamp: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const signed = `${timestamp}.${rawBody}`;
  const expected = 'sha256=' + createHmac('sha256', secret).update(signed, 'utf8').digest('hex');
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express example — read the RAW body (before JSON parsing) to hash it correctly.
app.post('/webhooks/teamcast', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyTeamcastWebhook(
    req.body.toString('utf8'),
    req.header('X-Webhook-Timestamp') ?? '',
    req.header('X-Webhook-Signature') ?? '',
    process.env.TEAMCAST_WEBHOOK_SECRET!,
  );
  if (!ok) return res.status(401).end();
  const payload = JSON.parse(req.body.toString('utf8'));
  // handle payload.event ...
  res.status(200).end();
});

Retries and dead-lettering

Delivery is retried up to 5 times with exponential backoff (1s, 2s, 4s, 8s) and a 10-second per-attempt timeout. After exhausting retries, the payload is parked in a dead-letter queue for admin inspection and manual retry — delivery is never silently dropped.

coverage_summary is the only assessment data ever sent — no score, no hire/no-hire recommendation, no emotion signal. Enforced by an egress guard on every outbound assessment payload, not just by convention.
Was this page helpful?