Webhooks
Register an endpoint and we POST every state change to it instead of making you ask. Each delivery is signed with HMAC-SHA256 over the raw body, delivery is at least once, and sandbox tasks fire exactly the same events — so you can build and test a receiver without spending anything.
A task takes hours. Register an endpoint in the console and we POST every state change to it instead of making you ask. Sandbox tasks fire the same events on the same signature, so you can build and test a receiver without spending anything.
| Event | Fires when |
|---|---|
task.queued | Accepted and waiting for a person. |
task.accepted | A verified person took it. |
task.in_progress | Work has started. |
task.submitted | Evidence is in and being checked. |
task.completed | The result is ready. |
task.failed | It could not be completed; any hold is returned. |
task.cancelled | Cancelled before work began. |
task.expired | The deadline passed with no acceptance. |
task.refunded | Money has gone back. |
task.sla_breached | The deadline passed. The remedy has already been paid — you do not have to ask for it. |
task.disputed | Somebody has questioned this result. It is back in review and a person will decide within two business days. |
task.dispute_resolved | The dispute was adjudicated. The verdict and any refund are in the payload. |
credit.low | Balance below 20% of the daily limit. |
credit.exhausted | A task was refused for want of credit. |
Verify the signature
Each delivery carries HO-Signature: t=<unix>,v1=<hex>, where the HMAC is SHA-256 over t + "." + the raw request body with your endpoint secret. Reject anything with a timestamp older than five minutes, and compare in constant time. During a secret rotation two v1 values are sent — accept the delivery if either matches, which is what makes rotating a secret something other than an outage.
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split('=', 1) for p in header.split(','))
ts = int(parts['t'])
if abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(secret.encode(),
f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
# every v1 in the header — there are two during a rotation
return any(hmac.compare_digest(expected, v)
for k, v in (p.split('=', 1) for p in header.split(','))
if k == 'v1')
Delivery is at least once
Dedupe on the event id. We retry six times over six hours — immediately, then after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours — and then mark the delivery dead. Return 2xx quickly and do your work afterwards: a slow endpoint does not block anyone else, but it does eat its own retry budget.
Your endpoint must be public HTTPS. We resolve the hostname, refuse every private, loopback, link-local and CGNAT address, pin the connection to the address we validated, and refuse redirects — re-checked on every delivery, not just at registration.