Integration API
Webhooks
Receive real-time state-change notifications via signed HTTP POST webhooks.
Teamcast sends HMAC-SHA256 signed HTTP POST webhooks for every state change in the interview lifecycle. Webhook endpoints can be configured at three levels — the system resolves the target URL using the first match:
| Priority | Level | Scope | Configured By |
|---|---|---|---|
| 1 (highest) | Per-interview callbackUrl | Single interview only | Passed in the A2A create request |
| 2 | Tenant webhook config | All interviews for one tenant | Tenant Admin or Platform Admin |
| 3 (fallback) | Platform webhook config | All tenants under the platform | SuperAdmin or Platform Admin |
tenantId field in each payload to route events internally.Event Subscription
Both tenant and platform webhook configs support an events array to subscribe to specific event types. When empty (default), all events are delivered.
// Example: subscribe to plan + approval events only
{
"callbackUrl": "https://your-system.com/webhook/teamcast",
"events": [
"interview.plan_generated",
"interview.approved",
"interview.rejected",
"interview.assessment_pending"
]
}Webhook Payload
{
"event": "interview.plan_generated",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"tenantId": "tenant-uuid",
"candidateRef": "li_app_a1b2c3d4",
"state": "PENDING",
"timestamp": "2024-01-15T10:45:00.000Z",
"data": {
"planId": "plan-uuid"
}
}| Field | Type | Description |
|---|---|---|
| event | string | Event type identifier |
| runId | string | Agno run identifier (from create response) |
| interviewId | string | Interview database ID |
| tenantId | string | Tenant identifier |
| candidateRef | string | null | Partner-side candidate identifier — returned as-is from the original request |
| state | string | Current workflow state after this event |
| timestamp | ISO string | Event occurrence time |
| data | object | Event-specific payload (varies by event type) |
HTTP Headers
| Header | Description |
|---|---|
| X-Webhook-Signature | HMAC-SHA256 signature: sha256=<hex> |
| X-Webhook-Timestamp | ISO timestamp of the event |
| X-Tenant-ID | Tenant ID for multi-tenant filtering |
| Content-Type | application/json |
Signature Verification
Always verify the X-Webhook-Signature header to prevent spoofing. Each webhook config (tenant or platform) has its own signing secret, auto-generated when you register your endpoint. The signature is computed as HMAC-SHA256(JSON.stringify(payload), YOUR_WEBHOOK_SECRET).
import { createHmac } from 'crypto';
function verifyWebhook(
rawBody: string,
signature: string,
secret: string
): boolean {
const expected = 'sha256=' +
createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return signature === expected;
}
// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-webhook-signature'] as string;
if (!verifyWebhook(req.body.toString(), sig, process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString());
// Handle payload...
res.json({ received: true });
});Events Reference
interview.info_needed
Fired when interview data is incomplete. Supply missing fields via PATCH .../info before plan generation can begin.
{
"event": "interview.info_needed",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"candidateRef": "li_app_a1b2c3d4",
"state": "INFO_NEEDED",
"data": {
"missingFields": [
{
"field": "qualifications",
"severity": "HIGH",
"reason": "No qualifications or skills provided",
"question": "Please provide at least one qualification statement or skill"
}
],
"dataQuality": "POOR"
}
}interview.plan_generated
Fired when the AI-generated plan is ready for recruiter review (or auto-approved when autoApprovePlans is enabled).
{
"event": "interview.plan_generated",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"candidateRef": "li_app_a1b2c3d4",
"state": "PENDING",
"data": {
"planId": "plan-uuid"
}
}interview.approved
Fired when the plan is approved (manually or via auto-approve). Contains the candidate interview link and an AI-generated LinkedIn InMail draft ready to forward to the candidate.
{
"event": "interview.approved",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"candidateRef": "li_app_a1b2c3d4",
"state": "APPROVED",
"data": {
"approved": true,
"message": "Interview plan approved. Interview link generated.",
"interviewLink": "{YOUR_APP_URL}/interview/join/abc123token",
"inmailDraft": {
"subject": "Senior Software Engineer Opportunity at Acme Corp — Interview Invitation",
"body": "Hi there,\n\nWe would love to invite you to complete an AI-powered interview for our Senior Software Engineer role at Acme Corp.\n\nPlease click the link below to begin:\n{YOUR_APP_URL}/interview/join/abc123token\n\nBest regards,\nThe Acme Corp Recruiting Team"
}
}
}The inmailDraft is generated by the Planner AI and personalised using the company name, role, and key qualifications. Forward the link to the candidate via your own notification channel. Only present when approved: true.
interview.assessment_pending
Fired when the Assessor Agent finishes generating the assessment. Contains the full structured result including qualification evaluation against must-have and nice-to-have criteria extracted from the qualifications.
{
"event": "interview.assessment_pending",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"candidateRef": "li_app_a1b2c3d4",
"state": "ASSESSMENT_PENDING",
"data": {
"assessmentId": "assessment-uuid",
"overallScore": 82,
"recommendation": "STRONG_HIRE",
"summary": "The candidate demonstrated strong TypeScript and distributed systems skills...",
"strengths": ["Strong system design thinking", "Clear communication"],
"weaknesses": ["Limited experience with distributed tracing"],
"mustHaveMet": 3,
"mustHaveTotal": 3,
"mustHaveEvaluations": [
{
"criterion": "5+ years TypeScript experience",
"category": "MUST_HAVE",
"met": true,
"confidence": 0.95,
"evidence": "Demonstrated advanced generics and strict mode patterns"
}
],
"niceToHaveEvaluations": [
{
"criterion": "GraphQL experience",
"category": "NICE_TO_HAVE",
"met": false,
"confidence": 0.8,
"evidence": "GraphQL was not discussed during the session"
}
]
}
}interview.assessment_completed
Fired after recruiter approves the assessment. Signals the hiring workflow is complete.
{
"event": "interview.assessment_completed",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"candidateRef": "li_app_a1b2c3d4",
"state": "ASSESSMENT_APPROVED",
"data": {
"finalVerdict": "HIRE",
"verdictBy": "li_recruiter_8821"
}
}| Event | State | Description |
|---|---|---|
| interview.info_needed | INFO_NEEDED | Missing critical data, HITL required |
| interview.info_completed | VALIDATING_SKILLS | HITL completed info, plan generation starting |
| interview.plan_generated | PENDING | Plan ready for recruiter review |
| interview.approved | APPROVED | Plan approved, candidate link + inmailDraft generated |
| interview.rejected | REJECTED | Plan rejected with reason |
| interview.modification_requested | GENERATING_PLAN | Recruiter requested plan changes |
| interview.assessment_pending | ASSESSMENT_PENDING | Assessment generated, awaiting recruiter verdict |
| interview.assessment_completed | ASSESSMENT_APPROVED | Assessment approved, hiring workflow complete |
Configuration & Management
Webhook endpoints can be created and managed through the Admin UI or REST API. For step-by-step UI walkthroughs and copy-pasteable curl examples see the dedicated configuration pages: Platform Admin → Webhook Configuration (platform-wide + per-tenant management) and Tenant Admin → Webhook Configuration (tenant self-serve). The endpoint summary below remains the canonical reference for the underlying API surface.
Platform Admin endpoints enforce strict isolation — a tenant must belong to the calling platform, otherwise the API returns 403.
Self-serve: /me/webhook-config (no :id)
OAuth tenant tokens, tenant Admin login JWTs, and API keys with X-Tenant-ID already carry a single tenant scope. Use /api/v1/me/webhook-config — the backend resolves the tenant from the authenticated identity and no path parameter is required or accepted. This is the recommended route for any tenant-scoped caller.
/me/* — they have no single tenant context. They use /tenants/:id/webhook-config instead.Access scoping for /tenants/:id/webhook-config
The :id-bearing route exists for callers that span multiple tenants. Tenant-scoped callers can also hit it, but the backend forces URL :id to equal the caller's own tenant — making it indistinguishable from /me/webhook-config. The backend enforces the following rules before any DB access:
| Caller type | Rule applied to URL :id |
|---|---|
| super-admin | Any tenant id is allowed |
| platform-admin | Tenant must belong to the caller's platform (else 403) |
| API key (X-API-Key + X-Tenant-ID) | URL :id must equal X-Tenant-ID (else 403) |
| OAuth tenant token (user or client_credentials with tenant_id claim) | URL :id must equal token tenant_id claim (else 403) |
| Internal user JWT (recruiter/admin login) | URL :id must equal user's tenantId (else 403) |
Practical effect: a tenant-scoped OAuth token cannot read or modify another tenant's webhook config even if it holds the tenant:read or tenant:update scope. Cross-tenant access is reserved for super-admin and (platform-scoped) platform-admin callers.
| Method | Endpoint | Purpose |
|---|---|---|
| GET / PUT / DELETE | /api/v1/me/webhook-config | Self-serve route for OAuth tenant tokens and tenant Admin login — no :id required |
| GET | /api/v1/platform-admin/webhook-config | Fetch platform-level webhook config |
| PUT | /api/v1/platform-admin/webhook-config | Create or update platform webhook (re-saving an existing config reactivates it) |
| PATCH | /api/v1/platform-admin/webhook-config/deactivate | Soft-pause platform webhook (isActive=false) — row preserved |
| PATCH | /api/v1/platform-admin/webhook-config/activate | Re-enable platform webhook (API/tooling only — not surfaced in the Platform Admin UI) |
| DELETE | /api/v1/platform-admin/webhook-config | Hard-remove platform webhook (API only — not surfaced in the Platform Admin UI) |
| GET | /api/v1/platform-admin/tenants/:tenantId/webhook-config | Fetch a tenant’s webhook config (platform-admin convenience route) |
| PUT | /api/v1/platform-admin/tenants/:tenantId/webhook-config | Create or update a tenant’s webhook (platform-admin convenience route) |
| DELETE | /api/v1/platform-admin/tenants/:tenantId/webhook-config | Remove a tenant’s webhook (platform-admin convenience route) |
| POST | /api/v1/platform-admin/tenants/:tenantId/webhook-test | Send a synthetic test webhook (debugging) |
| GET / PUT / DELETE | /api/v1/tenants/:id/webhook-config | Cross-tenant per-tenant route — primarily for Super Admin and Platform Admin; tenant callers should prefer /me/webhook-config |
Upsert payload
PUT /api/v1/me/webhook-config
{
"callbackUrl": "https://api.acme.com/webhooks/teamcast",
"secret": "optional-32+char-hmac-secret",
"events": [
"interview.plan_generated",
"interview.approved",
"interview.rejected"
],
"autoApprovePlans": false,
"candidatePiiRetentionDays": 90,
"assessmentRetentionDays": 365,
"recordingRetentionDays": 30
}Test trigger
Use the webhook-test endpoint to verify connectivity from Teamcast to the receiver. It resolves the effective callback URL (tenant config first, then platform fallback), generates a synthetic payload, signs it with the matching HMAC secret, and reports delivery status. Helpful when a tenant reports “webhooks not triggering” — the response tells you exactly which layer resolved and whether the receiver responded with 2xx. Platform Admin only.
POST /api/v1/platform-admin/tenants/{tenantId}/webhook-test
{ "event": "interview.plan_generated" }
// 200 OK
{
"delivered": true,
"callbackUrl": "https://api.acme.com/webhooks/teamcast",
"source": "tenant",
"event": "interview.plan_generated"
}secretHint (last 4 chars). Provide a new secret in an upsert call to rotate; omit it to keep the existing one.Dead Letter Queue (DLQ)
When the in-band retry schedule (Retry Policy below) is exhausted, the payload is written to a dead letter queue so operators can inspect and re-deliver it out-of-band. DLQ entries record the original payload, signature, callback URL, attempt count, last error, and a status field (PENDING, RETRYING, DELIVERED, EXHAUSTED).
| Method | Endpoint | Permission |
|---|---|---|
| GET | /api/v1/admin/webhook-dlq?tenantId=&status=&limit= | system:monitor |
| POST | /api/v1/admin/webhook-dlq/:id/retry | system:monitor |
Access scoping
The system:monitor permission is held by the tenant Admin role, so a permission check alone is not enough to keep tenants apart. Both endpoints enforce caller-aware scoping in addition to the permission:
| Caller | List behaviour | Retry behaviour |
|---|---|---|
| super-admin | tenantId query is honored; omit to list across all tenants | May retry any record |
| platform-admin | tenantId query is required and must belong to caller’s platform (else 403) | Record’s tenant must belong to caller’s platform (else 403) |
| API key (X-API-Key + X-Tenant-ID) | tenantId query is ignored; forced to X-Tenant-ID | Record must belong to X-Tenant-ID (else 403) |
| Tenant admin login JWT / OAuth tenant token | tenantId query is ignored; forced to caller’s tenantId | Record must belong to caller’s tenantId (else 403) |
system:monitor permission no longer lets a tenant peek at another tenant's DLQ — the URL/query tenantId is overridden by the caller's own tenant context for non-admin types.Retry Policy
If your endpoint returns a non-2xx response, the system retries with exponential backoff. Respond quickly (within 5 seconds) and process events asynchronously.
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 second |
| 3 | 2 seconds |
| 4 | 4 seconds |
| 5 | 8 seconds |
200 response immediately and process the event asynchronously using a queue. This prevents timeouts and duplicate deliveries.