login
quiescence.eu
Ir al contenido

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.

EventFires when
task.queuedAccepted and waiting for a person.
task.acceptedA verified person took it.
task.in_progressWork has started.
task.submittedEvidence is in and being checked.
task.completedThe result is ready.
task.failedIt could not be completed; any hold is returned.
task.cancelledCancelled before work began.
task.expiredThe deadline passed with no acceptance.
task.refundedMoney has gone back.
task.sla_breachedThe deadline passed. The remedy has already been paid — you do not have to ask for it.
task.disputedSomebody has questioned this result. It is back in review and a person will decide within two business days.
task.dispute_resolvedThe dispute was adjudicated. The verdict and any refund are in the payload.
credit.lowBalance below 20% of the daily limit.
credit.exhaustedA 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.