Webhook Event Structure
Webhook Event Structure
All webhook events follow a consistent structure, making it easy to parse and handle events in your application.
Event Payload
Every webhook request contains a JSON payload with the following structure:
{
"event": "CONVERSATION_STARTED",
"eventId": "abc123-def456-ghi789",
"eventTimestamp": "2024-01-15T10:30:00.000Z",
"data": {
"recordId": "conv_abc123",
"organizationId": "org_xyz789"
// Additional event-specific data
}
}Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | The event type (e.g., CONVERSATION_STARTED) |
eventId | string | Unique identifier for this event |
eventTimestamp | string | ISO 8601 timestamp when the event occurred |
data | object | Event-specific data payload |
data.recordId | string | ID of the primary resource (conversation, candidate, etc.) |
data.organizationId | string | Your organization ID |
HTTP Headers
Each webhook request includes the following headers:
| Header | Description |
|---|---|
Content-Type | application/json |
x-signature | HMAC-SHA256 signature for verification (see Authentication) |
Example: Handling a Webhook
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/popp', (req, res) => {
const { event, eventId, eventTimestamp, data } = req.body;
console.log(`Received event: ${event}`);
console.log(`Event ID: ${eventId}`);
console.log(`Timestamp: ${eventTimestamp}`);
console.log(`Record ID: ${data.recordId}`);
console.log(`Organization ID: ${data.organizationId}`);
// Handle the event based on type
switch (event) {
case 'CONVERSATION_STARTED':
handleConversationStarted(data);
break;
case 'CONVERSATION_COMPLETED':
handleConversationCompleted(data);
break;
// ... handle other events
}
// Always respond with 200 to acknowledge receipt
res.status(200).send('OK');
});Document Events
DOCUMENT_UPLOADED
DOCUMENT_UPLOADEDFires in real time whenever a candidate uploads a document during a conversation — you no longer have to wait for the conversation to complete to retrieve it. The event fires for every document a candidate sends (it is not limited to documents we successfully recognise).
In addition to the standard fields, data includes document metadata:
| Field | Type | Description |
|---|---|---|
data.recordId | string | The conversation ID the document was uploaded to |
data.attachmentId | string | ID of the stored attachment |
data.fileName | string | Original file name |
data.contentType | string | MIME type (e.g. application/pdf) |
data.classifiedDocumentType | string | What we classified the document as (e.g. passport), if recognised |
data.classifiedDocumentTypes | string[] | All candidate classifications, if more than one matched |
Checking document validity: this event is a notification that a document arrived; it does not include a pass/fail validity verdict. To decide whether a document is valid before ingesting it (e.g. to avoid pulling expired documents into your pipeline), call the processDocument endpoint with the document reference — it returns a structured verdict (passedValidation, validationErrors). Validity is computed on demand so it always reflects your current validation configuration.
Best Practices
- Respond quickly: Return a 200 response as soon as possible, then process the event asynchronously
- Handle duplicates: Events may be delivered more than once; use
eventIdfor idempotency - Verify signatures: Always verify the
x-signatureheader before processing - Log events: Store incoming events for debugging and audit purposes
Next Steps
- Conversation Events - Events for conversation lifecycle
- Analysis Events - Events for candidate analysis
- Scheduling Events - Events for calendar and meetings