GuidesWebhook Handling

Webhook Handling

Receive real-time notifications when submissions are scored and jobs are completed.

Setup

Register a webhook endpoint:

const endpoint = await client.createWebhookEndpoint({
  url: 'https://your-app.com/webhooks/firsthand',
  events: ['submission.approved', 'job.completed'],
});
// Save endpoint.secret for signature verification
endpoint = client.create_webhook_endpoint({
    "url": "https://your-app.com/webhooks/firsthand",
    "events": ["submission.approved", "job.completed"],
})

Event Types

EventDescription
submission.approvedA submission scored 3+ stars and was auto-approved
submission.rejectedA submission scored below 3 stars and was auto-rejected
job.completedAll requested files have been collected
job.cancelledJob was cancelled by the buyer

Payload Format

{
  "id": "evt_01JQ...",
  "type": "submission.approved",
  "created_at": "2026-03-15T10:05:00Z",
  "data": {
    "object": "submission",
    "id": "sub_01JQ...",
    "job_id": "job_01JQ...",
    "status": "approved",
    "ai_star_rating": 4,
    "ai_reasoning": "Clear, well-lit photo that matches the job description. Subject is centered and in focus.",
    "file_url": "https://files.firsthandapi.com/...",
    "content_type": "image/jpeg",
    "submitted_at": "2026-03-15T10:04:55Z"
  }
}

Signature Verification

Always verify webhook signatures to ensure authenticity.

import { verifyWebhookSignature } from '@firsthand/sdk';
 
app.post('/webhooks/firsthand', (req, res) => {
  try {
    verifyWebhookSignature(
      req.rawBody,
      req.headers['x-firsthand-signature'],
      process.env.WEBHOOK_SECRET,
    );
  } catch (e) {
    return res.status(400).send('Invalid signature');
  }
 
  const event = req.body;
  switch (event.type) {
    case 'submission.approved':
      handleSubmissionApproved(event.data);
      break;
    case 'job.completed':
      handleJobCompleted(event.data);
      break;
  }
 
  res.status(200).send('OK');
});
from firsthand import verify_webhook_signature
 
@app.route('/webhooks/firsthand', methods=['POST'])
def handle_webhook():
    try:
        verify_webhook_signature(
            request.data,
            request.headers['X-FirstHand-Signature'],
            WEBHOOK_SECRET,
        )
    except ValueError:
        return 'Invalid signature', 400
 
    event = request.json
    if event['type'] == 'submission.approved':
        handle_submission_approved(event['data'])
    elif event['type'] == 'job.completed':
        handle_job_completed(event['data'])
 
    return 'OK', 200

Best Practices

  1. Always verify signatures — Never skip verification in production
  2. Return 200 quickly — Process events asynchronously to avoid timeouts
  3. Handle duplicates — Use the event id for idempotency
  4. Monitor failures — Check the webhook events list for delivery issues
  5. Rotate secrets — Rotate webhook secrets periodically via the API