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

EventDescription
model.createdA new model was created
model.status.updatedModel status changed (e.g., pending → active)
model.photos_readyHeadshots have been generated and are ready
model.favorite_selectedUser selected their favorite photo
model.deletedModel 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

FieldTypeDescription
_idstringModel ID
uidstringUser identifier
titlestringDisplay name
triggerstringSelected gender (male, female)
statusstringCurrent model status
isDevelopmentbooleanWhether in test mode
finishedAtdatetimeWhen processing completed
deletedAtdatetimeWhen model was deleted
inviteExpiresAtdatetimeWhen invite expires
createdAtdatetimeWhen model was created
organizationstringOrganization 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.