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

json
{
  "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"
  }
}
FieldTypeDescription
eventstringEvent type identifier
runIdstringAgno run identifier (from create response)
interviewIdstringInterview database ID
tenantIdstringTenant identifier
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. The signature is computed as HMAC-SHA256({timestamp}.{payload}, WEBHOOK_SECRET).

typescript
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 });
});
Set 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.

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

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

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

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 generated
interview.rejectedREJECTEDPlan rejected with reason
interview.modification_requestedGENERATING_PLANRecruiter 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.

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?