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
| Event | Fires when |
|---|---|
interview.info_needed | Required candidate/role data is missing |
interview.info_completed | Missing fields have been supplied |
interview.plan_generated | A plan is ready and awaiting approval |
interview.approved | The plan (or assessment) was approved |
interview.rejected | The plan or assessment was rejected |
interview.assessment_ready | The coverage-only assessment was approved and delivered |
interview.completed | The run reached its terminal completed state |
interview.cancelled | The run was cancelled |
interview.modification_requested | A plan revision was requested |
Envelope
Every event shares the same top-level envelope. All fields are snake_case.
| Field | Type | Description |
|---|---|---|
event | string | Event type — see catalog above |
run_id | string | Run identifier returned by the create endpoint |
interview_id | string | Interview database ID |
tenant_id | string | Tenant identifier — use to route in multi-tenant setups |
state | string | Workflow state at the time of this event |
timestamp | ISO-8601 | Event time — also echoed in X-Webhook-Timestamp header |
data | object | Event-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.
{
"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"
}
}{
"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"
}
}{
"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
}
}
}{
"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"
}
]
}
}{
"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"
}
}{
"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
| Header | Value |
|---|---|
X-Webhook-Signature | sha256=HMAC-SHA256(secret, "{timestamp}.{rawBody}") — timestamp prefix prevents replay attacks |
X-Webhook-Timestamp | ISO-8601 — same value embedded in the payload body; used as the prefix in the signed string |
X-Tenant-ID | The 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
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.