A2A Protocol
Webhooks
Receive real-time state-change notifications via signed HTTP POST webhooks.
When you provide a callbackUrl in your interview creation request, the system sends HMAC-SHA256 signed HTTP POST webhooks to that URL for every state change in the interview lifecycle.
Webhook Payload
{
"event": "interview.plan_generated",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"tenantId": "tenant-uuid",
"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 |
| 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. The signature is computed as HMAC-SHA256({timestamp}.{payload}, WEBHOOK_SECRET).
import { createHmac } from 'crypto';
function verifyWebhook(
rawBody: string,
signature: string,
timestamp: string,
secret: string
): boolean {
const message = `${timestamp}.${rawBody}`;
const expected = 'sha256=' +
createHmac('sha256', secret)
.update(message)
.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;
const ts = req.headers['x-webhook-timestamp'] as string;
if (!verifyWebhook(req.body.toString(), sig, ts, 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 });
});WEBHOOK_SECRET to a random string of at least 32 characters in production. Never use the default value.Events Reference
interview.info_needed
Fired when interview data is incomplete. HITL must provide missing fields before plan generation can begin.
{
"event": "interview.info_needed",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"state": "INFO_NEEDED",
"data": {
"missingFields": [
{
"field": "jobDescription",
"severity": "HIGH",
"reason": "Job description too brief (< 50 chars)",
"question": "Please provide a detailed job description"
}
],
"dataQuality": "POOR"
}
}interview.plan_generated
Fired when the AI-generated plan is ready for recruiter review.
{
"event": "interview.plan_generated",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"state": "PENDING",
"data": {
"planId": "plan-uuid"
}
}interview.approved
Fired when recruiter approves the plan. Contains the candidate interview link and an AI-generated LinkedIn InMail draft ready to send to the candidate.
{
"event": "interview.approved",
"runId": "agno-run-uuid",
"interviewId": "interview-uuid",
"state": "APPROVED",
"data": {
"approved": true,
"message": "Interview plan approved. Interview link sent to candidate.",
"interviewLink": "https://app.ai-interview.com/interview/join/abc123token",
"inmailDraft": {
"subject": "Senior Software Engineer Opportunity at Acme Corp — Interview Invitation",
"body": "Hi Jane,\n\nI came across your profile and was very impressed by your TypeScript and React expertise...\n\nPlease click the link below to begin your interview:\nhttps://app.ai-interview.com/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 skills. Placeholders are substituted before the webhook fires — the body is ready to send as-is. Only present when approved: true.
| 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 generated |
| interview.rejected | REJECTED | Plan rejected with reason |
| interview.modification_requested | GENERATING_PLAN | Recruiter requested plan changes |
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.