Webhooks
Receive HTTP callbacks when reviews are completed.
Webhooks notify your application when events occur in Datashift. Use them with the REST API to receive asynchronous notifications when reviews complete.
When to use webhooks
- • Your service submits tasks but doesn't maintain persistent connections
- • You need to trigger downstream actions when reviews complete
- • You're integrating with workflow tools that support HTTP callbacks
Setup
Configure webhooks in Settings → Webhooks.
Add your endpoint URL
Must be HTTPS and publicly accessible
Select events to receive
Choose which event types trigger webhooks
Copy your signing secret
Use this to verify webhook signatures
Event Types
| Event | Description |
|---|---|
| task.created | A new task was submitted for review |
| task.reviewed | A task has been reviewed (includes all reviews for two-step workflows) |
Payload Format
All webhooks include these headers and a JSON body:
Headers
| Header | Description |
|---|---|
| X-Datashift-Signature | HMAC-SHA256 signature of the payload |
| X-Datashift-Timestamp | Unix timestamp when the webhook was sent |
| X-Datashift-Event-Id | Unique identifier for this delivery |
Example: task.created
{
"event": "task.created",
"timestamp": "2026-02-26T10:30:00Z",
"data": {
"task": {
"id": "task_xyz789",
"external_id": "order-12345",
"state": "pending",
"summary": "Refund request for $500",
"data": { "amount": 500, "reason": "Defective item" },
"metadata": { "source": "support-agent" },
"sla_deadline": "2026-02-26T11:30:00Z",
"reviewed_at": null,
"created_at": "2026-02-26T10:30:00Z"
},
"queue": {
"key": "refund-approval",
"name": "Refund Approvals",
"review_type": "approval"
}
}
}Example: task.reviewed
{
"event": "task.reviewed",
"timestamp": "2026-02-26T10:35:00Z",
"data": {
"task": {
"id": "task_xyz789",
"external_id": "order-12345",
"state": "reviewed",
"summary": "Refund request for $500",
"data": { "amount": 500, "reason": "Defective item" },
"metadata": { "source": "support-agent" },
"sla_deadline": "2026-02-26T11:30:00Z",
"reviewed_at": "2026-02-26T10:35:00Z",
"created_at": "2026-02-26T10:30:00Z"
},
"queue": {
"key": "refund-approval",
"name": "Refund Approvals",
"review_type": "approval"
},
"reviews": [
{
"result": ["approved"],
"data": {},
"feedback": "Looks good",
"reviewer": { "name": "Jane Smith", "type": "human" },
"created_at": "2026-02-26T10:35:00Z"
}
]
}
}Review fields
| Field | Description |
|---|---|
| result | Array of selected option keys (e.g. ["approved"]) |
| data | Review-type-specific structured output (see below) |
| feedback | Optional text feedback from the reviewer |
| reviewer | Reviewer name and type ("human" or "ai") |
| created_at | ISO 8601 timestamp of when the review was submitted |
The data field
The reviews[].data field contains structured output that varies by review type. For most review types it's an empty object — the primary result lives in result.
| Review type | data | Example |
|---|---|---|
| Approval | Empty | {} |
| Classification | Empty | {} |
| Labeling | Empty | {} |
| Scoring | Numeric score | { "score": 4 } |
| Augmentation | Original and augmented content | { "augmented_content": "...", "original_content": "...", "modified": true } |
Reviews submitted from Slack, Discord, or Teams also include source metadata in data:
{
"result": ["approved"],
"data": {
"source": "slack",
"slack_user_id": "U0123ABC",
"slack_channel_id": "C0456DEF",
"submitted_via": "modal"
},
"feedback": null,
"reviewer": { "name": "Jane Smith", "type": "human" },
"created_at": "2026-02-26T10:35:00Z"
}Signature Verification
Always verify webhook signatures to ensure requests come from Datashift.
Extract the timestamp and signature from headers
Create the signed payload: timestamp.body
Compute HMAC-SHA256 with your webhook secret
Compare with the signature header (constant-time comparison)
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-datashift-signature'];
const timestamp = req.headers['x-datashift-timestamp'];
// Verify signature
const payload = `${timestamp}.${req.body}`;
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (signature !== `sha256=${expected}`) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
switch (event.event) {
case 'task.reviewed':
console.log('Task reviewed:', event.data.task.id);
console.log('Result:', event.data.reviews[0].result);
break;
case 'task.created':
console.log('Task created:', event.data.task.id);
console.log('Queue:', event.data.queue.key);
break;
}
res.status(200).send('OK');
});Retry Policy
If your endpoint returns a non-2xx status or doesn't respond within 30 seconds, we'll retry:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
After 4 failed attempts, the webhook is marked as failed. You can manually retry from the webhook logs.
Best Practices
Return 200 quickly
Acknowledge receipt immediately, then process asynchronously. Long-running handlers will timeout.
Handle duplicates
Use the event ID to deduplicate. The same event may be delivered multiple times due to retries.
Verify signatures
Always verify the signature before processing. This prevents attackers from spoofing webhooks.
Check timestamps
Reject webhooks with timestamps older than 5 minutes to prevent replay attacks.