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:

PriorityLevelScopeConfigured By
1 (highest)Per-interview callbackUrlSingle interview onlyPassed in the A2A create request
2Tenant webhook configAll interviews for one tenantTenant Admin or Platform Admin
3 (fallback)Platform webhook configAll tenants under the platformSuperAdmin or Platform Admin
Most integrations use platform-level config. Register one endpoint for your platform and receive events for all tenants. Use the 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.

json
// 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

json
{
  "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"
  }
}
FieldTypeDescription
eventstringEvent type identifier
runIdstringAgno run identifier (from create response)
interviewIdstringInterview database ID
tenantIdstringTenant identifier
candidateRefstring | nullPartner-side candidate identifier — returned as-is from the original request
statestringCurrent workflow state after this event
timestampISO stringEvent occurrence time
dataobjectEvent-specific payload (varies by event type)

HTTP Headers

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature: sha256=<hex>
X-Webhook-TimestampISO timestamp of the event
X-Tenant-IDTenant ID for multi-tenant filtering
Content-Typeapplication/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).

typescript
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 });
});
Your signing secret is auto-generated when you register your webhook endpoint and returned once in the API response. Each tenant or platform has its own isolated secret — store it securely in an environment variable. It is never sent in webhook requests.

Events Reference

interview.info_needed

Fired when interview data is incomplete. Supply missing fields via PATCH .../info before plan generation can begin.

json
{
  "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).

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

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

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

json
{
  "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"
  }
}
EventStateDescription
interview.info_neededINFO_NEEDEDMissing critical data, HITL required
interview.info_completedVALIDATING_SKILLSHITL completed info, plan generation starting
interview.plan_generatedPENDINGPlan ready for recruiter review
interview.approvedAPPROVEDPlan approved, candidate link + inmailDraft generated
interview.rejectedREJECTEDPlan rejected with reason
interview.modification_requestedGENERATING_PLANRecruiter requested plan changes
interview.assessment_pendingASSESSMENT_PENDINGAssessment generated, awaiting recruiter verdict
interview.assessment_completedASSESSMENT_APPROVEDAssessment 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.

Cross-tenant principals (super-admin, platform-admin) cannot use /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 typeRule applied to URL :id
super-adminAny tenant id is allowed
platform-adminTenant 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.

MethodEndpointPurpose
GET / PUT / DELETE/api/v1/me/webhook-configSelf-serve route for OAuth tenant tokens and tenant Admin login — no :id required
GET/api/v1/platform-admin/webhook-configFetch platform-level webhook config
PUT/api/v1/platform-admin/webhook-configCreate or update platform webhook (re-saving an existing config reactivates it)
PATCH/api/v1/platform-admin/webhook-config/deactivateSoft-pause platform webhook (isActive=false) — row preserved
PATCH/api/v1/platform-admin/webhook-config/activateRe-enable platform webhook (API/tooling only — not surfaced in the Platform Admin UI)
DELETE/api/v1/platform-admin/webhook-configHard-remove platform webhook (API only — not surfaced in the Platform Admin UI)
GET/api/v1/platform-admin/tenants/:tenantId/webhook-configFetch a tenant’s webhook config (platform-admin convenience route)
PUT/api/v1/platform-admin/tenants/:tenantId/webhook-configCreate or update a tenant’s webhook (platform-admin convenience route)
DELETE/api/v1/platform-admin/tenants/:tenantId/webhook-configRemove a tenant’s webhook (platform-admin convenience route)
POST/api/v1/platform-admin/tenants/:tenantId/webhook-testSend a synthetic test webhook (debugging)
GET / PUT / DELETE/api/v1/tenants/:id/webhook-configCross-tenant per-tenant route — primarily for Super Admin and Platform Admin; tenant callers should prefer /me/webhook-config

Upsert payload

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

json
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"
}
Secrets are never returned in plaintext after creation — responses include only 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).

MethodEndpointPermission
GET/api/v1/admin/webhook-dlq?tenantId=&status=&limit=system:monitor
POST/api/v1/admin/webhook-dlq/:id/retrysystem: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:

CallerList behaviourRetry behaviour
super-admintenantId query is honored; omit to list across all tenantsMay retry any record
platform-admintenantId 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-IDRecord must belong to X-Tenant-ID (else 403)
Tenant admin login JWT / OAuth tenant tokentenantId query is ignored; forced to caller’s tenantIdRecord must belong to caller’s tenantId (else 403)
Holding the Admin role's 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.

AttemptDelay
1 (initial)Immediate
21 second
32 seconds
44 seconds
58 seconds
Return a 200 response immediately and process the event asynchronously using a queue. This prevents timeouts and duplicate deliveries.
Was this page helpful?