Webhooks

Receive real-time notifications when tasks change state. HumanRail delivers webhook events to your configured endpoint with HMAC-SHA256 signatures for verification.

What are webhooks?

Webhooks are HTTP callbacks that HumanRail sends to your server when task state changes occur. Instead of polling the API repeatedly, you register a URL and HumanRail pushes events to you as they happen — typically within milliseconds of the state change.

Each webhook delivery is a POST request with a JSON body containing the event type, a timestamp, and the full task object at the time of the event.

Register a webhook

Via the Dashboard

Navigate to Settings → Webhooks in your customer dashboard. Click Add Endpoint, enter your URL, and select which events you want to receive. A signing secret will be generated automatically.

Via the API

HTTP
POST https://api.humanrail.dev/v1/webhooks
Authorization: Bearer ek_live_...
Content-Type: application/json

{
  "url": "https://myapp.com/webhooks/humanrail",
  "events": [
    "task.verified",
    "task.failed",
    "task.expired"
  ],
  "description": "Production webhook"
}

The response includes your signing secret. Store it securely — it is only shown once:

JSON Response
{
  "id": "wh_01abc123...",
  "url": "https://myapp.com/webhooks/humanrail",
  "events": ["task.verified", "task.failed", "task.expired"],
  "secret": "whsec_a1b2c3d4e5f6...",
  "created_at": "2026-03-01T12:00:00Z"
}

Event types

HumanRail sends the following webhook events. Each event payload contains the full task object at the time the event was emitted.

Event Description When it fires
task.created A new task has been created and is queued for routing Immediately after POST /tasks
task.assigned A worker has been assigned to the task When the routing engine matches a worker (typically < 50ms)
task.submitted The worker has submitted a response (not yet verified) When the worker completes the task form
task.verified The response has passed verification. This is the primary event you should listen to. After the 6-stage verification pipeline passes
task.completed The task is fully complete (verified + worker paid) After payment settlement
task.failed The task failed verification or encountered an unrecoverable error When verification rejects the response after all retries
task.cancelled The task was cancelled by the caller When you call POST /tasks/{id}/cancel
task.expired The task exceeded its SLA deadline without a verified result When the SLA timer expires

Example payload

All events follow the same envelope format:

JSON
{
  "id": "evt_01abc123...",
  "type": "task.verified",
  "created_at": "2026-03-01T14:30:00.000Z",
  "data": {
    "task_id": "tsk_01xyz789...",
    "org_id": "org_01def456...",
    "type": "content_review",
    "status": "verified",
    "output": {
      "verdict": "genuine",
      "notes": "Content appears authentic. Writing style is consistent."
    },
    "verification": {
      "score": 0.95,
      "stages_passed": 6,
      "stages_total": 6
    },
    "worker_id": "wkr_01ghi012...",
    "payout_sats": 500,
    "sla_seconds": 300,
    "created_at": "2026-03-01T14:25:00.000Z",
    "completed_at": "2026-03-01T14:28:42.000Z"
  }
}

Verify signatures

Every webhook delivery includes an X-Humanrail-Signature header containing an HMAC-SHA256 signature of the request body, computed using your webhook signing secret. Always verify this signature before processing the event to prevent spoofing attacks.

The signature is computed as:

Pseudocode
signature = HMAC-SHA256(webhook_secret, timestamp + "." + raw_body)
header    = "t=" + timestamp + ",v1=" + hex(signature)

The X-Humanrail-Signature header has the format t=1709312400,v1=5257a86.... Parse the timestamp and signature, then compute the expected signature and compare using a constant-time comparison.

Python
import hmac, hashlib, time

def verify_signature(payload: bytes, header: str, secret: str) -> bool:
    # Parse the signature header
    parts = {k: v for k, v in (p.split("=", 1) for p in header.split(","))}
    timestamp = parts["t"]
    signature = parts["v1"]

    # Reject if timestamp is more than 5 minutes old
    if abs(time.time() - int(timestamp)) > 300:
        return False

    # Compute expected signature
    signed_content = f"{timestamp}.{payload.decode()}"
    expected = hmac.new(
        secret.encode(),
        signed_content.encode(),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

Or use the built-in SDK helper:

Python
from humanrail import verify_webhook_signature

is_valid = verify_webhook_signature(
    payload=request_body,
    signature=request.headers["x-humanrail-signature"],
    secret=os.environ["HUMANRAIL_WEBHOOK_SECRET"],
)
TypeScript
import crypto from 'crypto';

function verifySignature(
  payload: Buffer,
  header: string,
  secret: string,
): boolean {
  // Parse the signature header
  const parts = Object.fromEntries(
    header.split(',').map(p => p.split('=', 2) as [string, string])
  );
  const timestamp = parts.t;
  const signature = parts.v1;

  // Reject if timestamp is more than 5 minutes old
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false;
  }

  // Compute expected signature
  const signedContent = `${timestamp}.${payload.toString()}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedContent)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

Or use the built-in SDK helper:

TypeScript
import { verifyWebhookSignature } from '@humanrail/sdk';

const isValid = verifyWebhookSignature({
  payload: req.body,
  signature: req.headers['x-humanrail-signature'],
  secret: process.env.HUMANRAIL_WEBHOOK_SECRET,
});
Go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "math"
    "strconv"
    "strings"
    "time"
)

func VerifySignature(payload []byte, header, secret string) bool {
    // Parse header: "t=...,v1=..."
    parts := make(map[string]string)
    for _, p := range strings.Split(header, ",") {
        kv := strings.SplitN(p, "=", 2)
        parts[kv[0]] = kv[1]
    }

    // Reject old timestamps
    ts, _ := strconv.ParseInt(parts["t"], 10, 64)
    if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
        return false
    }

    // Compute expected signature
    signedContent := fmt.Sprintf("%s.%s", parts["t"], string(payload))
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(signedContent))
    expected := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(expected), []byte(parts["v1"]))
}

Or use the built-in SDK helper:

Go
isValid := humanrail.VerifyWebhookSignature(body, sigHeader, webhookSecret)

Retry policy

If your endpoint returns a non-2xx status code, times out, or is unreachable, HumanRail will retry delivery with exponential backoff:

Attempt Delay after failure Cumulative time
1st retry 1 minute 1 minute
2nd retry 5 minutes 6 minutes
3rd retry 30 minutes 36 minutes
4th retry 2 hours ~2.5 hours
5th retry (final) 24 hours ~26.5 hours

After 5 failed retries, the delivery is marked as failed. Failed deliveries are visible in your dashboard under Webhooks → Delivery History. You can manually retry any failed delivery from the dashboard.

Important

If your endpoint consistently fails (more than 95% failure rate over 24 hours), HumanRail will automatically disable the webhook and notify you via email. Re-enable it from the dashboard once the issue is resolved.

Best practices

Respond quickly

Your webhook endpoint has a 5-second timeout. Return a 200 status immediately, then process the event asynchronously. If you need to perform slow operations (database writes, API calls), enqueue the work and respond right away.

TypeScript (Express)
app.post('/webhooks/humanrail', (req, res) => {
  // Verify signature first (fast)
  if (!verifySignature(req)) return res.sendStatus(401);

  // Respond immediately
  res.sendStatus(200);

  // Process asynchronously
  processEvent(req.body).catch(console.error);
});

Handle duplicates with idempotency

Webhook deliveries are at-least-once. In rare cases (network issues, timeouts where your server processed the event but didn't respond in time), you may receive the same event more than once. Use the event id field to deduplicate:

TypeScript
const processed = new Set<string>(); // Use Redis/DB in production

function handleEvent(event: WebhookEvent) {
  if (processed.has(event.id)) return; // Already handled
  processed.add(event.id);

  // Process the event...
}

Use HTTPS

Webhook endpoints must use HTTPS in production. HTTP endpoints are only allowed for localhost during development with sandbox keys.

Monitor delivery health

Check the Webhooks → Delivery History page in your dashboard to monitor delivery success rates. Set up alerts for failed deliveries so you can respond quickly to endpoint outages.

Testing tip

Use POST /v1/webhooks/test to send a test event to your endpoint. This is useful for verifying your signature verification logic and endpoint connectivity without creating a real task.

Next steps

Quickstart

Send your first task in 5 minutes with code examples in all 3 languages.

📖

API Reference

Full endpoint documentation with request and response schemas.

📦

SDKs

TypeScript, Python, and Go client libraries.