Webhooks
Webhooks allow you to receive real-time HTTP notifications when events occur in your organization. This is more efficient than polling the API for status updates.
Configuration
Configure your webhook URL in your organization settings. You'll receive a webhook secret for verifying request authenticity.
Webhook Events
| Event | Description |
|---|---|
model.created | A new model was created |
model.status.updated | Model status changed (e.g., pending → active) |
model.photos_ready | Headshots have been generated and are ready |
model.favorite_selected | User selected their favorite photo |
model.deleted | Model was deleted |
Payload Structure
All webhooks follow the same payload structure:
{
"event": "model.status.updated",
"objectType": "model",
"object": {
"_id": "507f1f77bcf86cd799439011",
"uid": "anon_abc123",
"title": "John Doe",
"trigger": "male",
"status": "active",
"isDevelopment": false,
"finishedAt": "2024-01-15T11:45:00.000Z",
"deletedAt": null,
"inviteExpiresAt": "2024-02-14T10:30:00.000Z",
"createdAt": "2024-01-15T10:30:00.000Z",
"organization": "org_abc123def456"
}
}
Object Fields
| Field | Type | Description |
|---|---|---|
_id | string | Model ID |
uid | string | User identifier |
title | string | Display name |
trigger | string | Selected gender (male, female) |
status | string | Current model status |
isDevelopment | boolean | Whether in test mode |
finishedAt | datetime | When processing completed |
deletedAt | datetime | When model was deleted |
inviteExpiresAt | datetime | When invite expires |
createdAt | datetime | When model was created |
organization | string | Organization ID |
Event Details
model.created
Triggered when a new model is created via API or invite signup.
{
"event": "model.created",
"objectType": "model",
"object": {
"_id": "507f1f77bcf86cd799439011",
"status": "onboarding",
...
}
}
model.status.updated
Triggered when a model's status changes. Use this to track progress through the pipeline.
{
"event": "model.status.updated",
"objectType": "model",
"object": {
"_id": "507f1f77bcf86cd799439011",
"status": "generatingHeadshots",
...
}
}
Status Flow:
onboarding → waiting → pending → generatingHeadshots → active
model.photos_ready
Triggered when headshots are generated and available for download. This is typically the event you want to listen for to fetch the final photos.
{
"event": "model.photos_ready",
"objectType": "model",
"object": {
"_id": "507f1f77bcf86cd799439011",
"status": "active",
"finishedAt": "2024-01-15T11:45:00.000Z",
...
}
}
model.favorite_selected
Triggered when a user selects their favorite photo.
{
"event": "model.favorite_selected",
"objectType": "model",
"object": {
"_id": "507f1f77bcf86cd799439011",
...
}
}
model.deleted
Triggered when a model is deleted via API or admin action.
{
"event": "model.deleted",
"objectType": "model",
"object": {
"_id": "507f1f77bcf86cd799439011",
"status": "deleted",
"deletedAt": "2024-01-16T09:00:00.000Z",
...
}
}
Verifying Webhooks
Webhooks are signed using HMAC-SHA256. Verify the signature to ensure requests are from HeadshotPro.
Signature Header
X-HeadshotPro-Signature: sha256=abc123...
Verification Example
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return `sha256=${expectedSignature}` === signature;
}
// Express middleware
app.post('/webhooks/headshotpro', express.json(), (req, res) => {
const signature = req.headers['x-headshotpro-signature'];
if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const { event, object } = req.body;
console.log(`Received ${event} for model ${object._id}`);
res.status(200).send('OK');
});
Delivery Behavior
- Delay: Webhooks are sent with a 5-second delay
- Retries: Failed deliveries are retried up to 3 times
- Timeout: Your endpoint must respond within 10 seconds
- Expected Response: HTTP 200 status code
Handling Webhooks
1. Respond Quickly
Process webhooks asynchronously and return 200 immediately:
app.post('/webhooks/headshotpro', async (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
processWebhook(req.body).catch(console.error);
});
async function processWebhook({ event, object }) {
switch (event) {
case 'model.photos_ready':
await handlePhotosReady(object);
break;
case 'model.favorite_selected':
await handleFavoriteSelected(object);
break;
}
}
2. Handle Duplicate Events
Webhooks may be delivered more than once. Make your handlers idempotent:
const processedEvents = new Set();
async function handleWebhook(payload) {
const eventKey = `${payload.event}:${payload.object._id}:${payload.object.status}`;
if (processedEvents.has(eventKey)) {
console.log('Duplicate event, skipping');
return;
}
processedEvents.add(eventKey);
// Process event...
}
3. Log Events
Maintain a log of received webhooks for debugging:
app.post('/webhooks/headshotpro', async (req, res) => {
await db.webhookLogs.insert({
receivedAt: new Date(),
event: req.body.event,
objectId: req.body.object._id,
payload: req.body
});
res.status(200).send('OK');
});
Complete Integration Example
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/webhooks/headshotpro', express.json(), async (req, res) => {
// 1. Verify signature
const signature = req.headers['x-headshotpro-signature'];
const expected = `sha256=${crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex')}`;
if (signature !== expected) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// 2. Respond quickly
res.status(200).send('OK');
// 3. Process asynchronously
const { event, object } = req.body;
try {
switch (event) {
case 'model.created':
console.log(`New model created: ${object._id}`);
break;
case 'model.photos_ready':
console.log(`Photos ready for: ${object._id}`);
// Fetch favorite photo and update user profile
const photo = await fetchFavoritePhoto(object._id);
if (photo) {
await updateUserProfilePhoto(object.uid, photo.urls.main);
}
break;
case 'model.favorite_selected':
console.log(`Favorite selected for: ${object._id}`);
// Sync favorite photo
const favorite = await fetchFavoritePhoto(object._id);
await syncToExternalSystem(object.uid, favorite);
break;
case 'model.deleted':
console.log(`Model deleted: ${object._id}`);
// Clean up local references
await removeUserProfilePhoto(object.uid);
break;
default:
console.log(`Unhandled event: ${event}`);
}
} catch (error) {
console.error('Webhook processing error:', error);
// Don't throw - we already responded 200
}
});
Testing Webhooks
During development, use tools like ngrok to expose your local server:
ngrok http 3000
Then configure the ngrok URL as your webhook endpoint in HeadshotPro settings.