Integration

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.

1

Add your endpoint URL

Must be HTTPS and publicly accessible

2

Select events to receive

Choose which event types trigger webhooks

3

Copy your signing secret

Use this to verify webhook signatures

Event Types

EventDescription
task.createdA new task was submitted for review
task.reviewedA task has been reviewed (includes all reviews for two-step workflows)

Payload Format

All webhooks include these headers and a JSON body:

Headers

HeaderDescription
X-Datashift-SignatureHMAC-SHA256 signature of the payload
X-Datashift-TimestampUnix timestamp when the webhook was sent
X-Datashift-Event-IdUnique identifier for this delivery

Example: task.created

json
{
  "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

json
{
  "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

FieldDescription
resultArray of selected option keys (e.g. ["approved"])
dataReview-type-specific structured output (see below)
feedbackOptional text feedback from the reviewer
reviewerReviewer name and type ("human" or "ai")
created_atISO 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 typedataExample
ApprovalEmpty{}
ClassificationEmpty{}
LabelingEmpty{}
ScoringNumeric score{ "score": 4 }
AugmentationOriginal and augmented content{ "augmented_content": "...", "original_content": "...", "modified": true }

Reviews submitted from Slack, Discord, or Teams also include source metadata in data:

json
{
  "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.

1

Extract the timestamp and signature from headers

2

Create the signed payload: timestamp.body

3

Compute HMAC-SHA256 with your webhook secret

4

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:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 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.