Receive real-time notifications when tasks change state. HumanRail delivers webhook events to your configured endpoint with HMAC-SHA256 signatures for verification.
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.
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.
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:
{
"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"
}
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 |
All events follow the same envelope format:
{
"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"
}
}
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:
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.
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:
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"],
)
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:
import { verifyWebhookSignature } from '@humanrail/sdk';
const isValid = verifyWebhookSignature({
payload: req.body,
signature: req.headers['x-humanrail-signature'],
secret: process.env.HUMANRAIL_WEBHOOK_SECRET,
});
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:
isValid := humanrail.VerifyWebhookSignature(body, sigHeader, webhookSecret)
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.
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.
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.
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);
});
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:
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...
}
Webhook endpoints must use HTTPS in production. HTTP endpoints are only
allowed for localhost during development with sandbox keys.
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.
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.