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 verificationendpoint = client.create_webhook_endpoint({
"url": "https://your-app.com/webhooks/firsthand",
"events": ["submission.approved", "job.completed"],
})Event Types
| Event | Description |
|---|---|
submission.approved | A submission scored 3+ stars and was auto-approved |
submission.rejected | A submission scored below 3 stars and was auto-rejected |
job.completed | All requested files have been collected |
job.cancelled | Job 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', 200Best Practices
- Always verify signatures — Never skip verification in production
- Return 200 quickly — Process events asynchronously to avoid timeouts
- Handle duplicates — Use the event
idfor idempotency - Monitor failures — Check the webhook events list for delivery issues
- Rotate secrets — Rotate webhook secrets periodically via the API