Guide to Taskrabbit Webhooks: Features and Best Practices
Taskrabbit webhooks tell partners when something happens to a project they booked through Taskrabbit's Partner API: the Tasker finishes the work, the project is cancelled, or it moves to a new start time. If you sell assembly, mounting or delivery services through Taskrabbit, webhooks keep your order status in step without polling the API.
This guide covers Taskrabbit's three webhook events, how deliveries are signed, retries and replay, and the best practices for running a Taskrabbit receiver in production.
What are Taskrabbit webhooks?
Taskrabbit delivers webhooks through Svix. During partner onboarding you get access to a Svix Partner Dashboard, where you add your endpoint URL, choose the events to subscribe to, view delivery logs and replay events.
Each delivery is a JSON POST signed by Svix with a secret unique to your endpoint.
Taskrabbit webhook features
| Feature | Details |
|---|---|
| Availability | Partner API, set up during onboarding |
| Delivery platform | Svix |
| Configuration | Svix Partner Dashboard |
| Event field | Body event |
| Events | project.completed, project.canceled, project.rescheduled |
| Signature | Svix scheme, with a secret unique to each endpoint |
| Retries | Exponential backoff on a non-2xx response or timeout |
| Manual replay | Svix Partner Dashboard |
Common events
Every payload has event, project_uuid and timestamp, plus agreement_ if you gave the project your own reference when you created it.
| Event | When it fires | Extra fields |
|---|---|---|
project.completed | The Tasker has completed the project's work | None |
project.canceled | The project has been cancelled | cancellation_reason |
project.rescheduled | A task in the project has a new start time | new_start_time, previous_start_time |
cancellation_reason is one of order_canceled (cancelled by the partner or client), delivery_delayed, changed_mind (the client changed their mind) or canceled_by_cs (cancelled by Taskrabbit Customer Support).
Here is Taskrabbit's project.canceled example:
{
"event": "project.canceled",
"project_uuid": "550e8400-e29b-41d4-a716-446655440000",
"cancellation_reason": "order_canceled",
"timestamp": "2025-01-22T10:00:00Z",
"agreement_external_reference": "REF-12345"
}
Event names use the American spelling canceled, so match on that exactly.
Setting up Taskrabbit webhooks
- Open the Svix Partner Dashboard you were given during onboarding.
- Add your HTTPS endpoint and subscribe it to the events you need.
- Copy the endpoint's signing secret and store it server-side, for example as
TASKRABBIT_.WEBHOOK_ SECRET - Check the dashboard's delivery log once events start arriving.
Securing Taskrabbit webhooks
Taskrabbit says Svix signs every payload and that unverified webhooks should be discarded, and points to Svix's libraries. It doesn't restate the scheme or name the headers. Svix's defaults are svix-id, svix-timestamp and svix-signature, with a whsec_ secret: the signature is a base64 HMAC-SHA256 over {id}.{timestamp}.{raw body}, keyed with the base64-decoded part of the secret after whsec_, and sent as v1,<signature>.
const crypto = require('crypto');
const express = require('express');
function verifyTaskrabbitSignature(rawBody, headers, secret) {
const msg_id = headers['svix-id'];
const msg_timestamp = headers['svix-timestamp'];
const signature_header = headers['svix-signature'];
if (!msg_id || !msg_timestamp || !signature_header) return false;
if (typeof secret !== 'string' || !secret.startsWith('whsec_')) return false;
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const signed_content = Buffer.concat([Buffer.from(`${msg_id}.${msg_timestamp}.`, 'utf8'), rawBody]);
const expected = Buffer.from(
crypto.createHmac('sha256', key).update(signed_content).digest('base64'),
'utf8'
);
return signature_header.split(' ').some((entry) => {
const [version, signature] = entry.split(',');
if (version !== 'v1' || !signature) return false;
const received = Buffer.from(signature, 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post('/webhooks/taskrabbit', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.TASKRABBIT_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyTaskrabbitSignature(req.body, req.headers, secret)) {
return res.sendStatus(400); // Taskrabbit: discard unverified webhooks
}
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on the svix-id header, queue the event, then respond.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
import os
def verify_taskrabbit_signature(raw_body: bytes, headers, secret: str) -> bool:
msg_id = headers.get("svix-id")
msg_timestamp = headers.get("svix-timestamp")
signature_header = headers.get("svix-signature")
if not (msg_id and msg_timestamp and signature_header):
return False
if not secret or not secret.startswith("whsec_"):
return False
key = base64.b64decode(secret[len("whsec_"):])
signed_content = f"{msg_id}.{msg_timestamp}.".encode("utf-8") + raw_body
expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())
for entry in signature_header.split(" "):
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
return True
return False
# In FastAPI: raw_body = await request.body()
# verify_taskrabbit_signature(raw_body, request.headers, os.environ["TASKRABBIT_WEBHOOK_SECRET"])
The Svix libraries do the same and also check the timestamp.
Make Taskrabbit webhooks production-ready. Hookdeck Event Gateway verifies Taskrabbit's Svix signature, queues every completion, cancellation and reschedule, and lets you replay any of them after a failure.
Hookdeck's Taskrabbit source type verifies Svix signatures with the secret you enter and accepts either svix- or webhook- header names. Verification is optional on the source, so leave it off if you didn't set a secret. Hookdeck does not apply a timestamp window.
Taskrabbit webhook limitations and pain points
No event ID in the body
The Problem: Retries and replays produce duplicate updates, and there's no obvious field to deduplicate on.
Why It Happens: The payload has project_uuid and timestamp but no event ID. A project can be rescheduled several times, so project_uuid and event together aren't unique either.
Workarounds:
- Deduplicate on the Svix message ID header, which identifies the message across retries.
- Make handlers idempotent, as Taskrabbit recommends: setting a status to "completed" twice should have the same result as once.
How Hookdeck Can Help: Hookdeck's deduplication can key on a header or on a combination of body fields, so repeats are dropped before they reach your service.
timestamp means different things per event
The Problem: Sorting events by timestamp puts reschedules in the wrong place.
Why It Happens: For project.completed and project.canceled, timestamp is when the event was processed. For project.rescheduled, Taskrabbit sets it to new_start_time, which can be in the future.
Workarounds:
- For reschedules, compare
previous_start_timewith the start time you have stored, and apply the change only if it matches. - Don't order events by
timestampacross event types.
How Hookdeck Can Help: Hookdeck records when each request arrived, so you have a receive time for every event, including reschedules.
No published retry schedule
The Problem: You can't tell how long Taskrabbit will keep retrying during an outage.
Why It Happens: Taskrabbit says Svix retries with exponential backoff on a non-2xx response or timeout, but doesn't give the schedule or the timeout.
Workarounds:
- Respond quickly and process in the background.
- After an outage, replay failed messages from the Partner Dashboard.
How Hookdeck Can Help: Hookdeck accepts deliveries while your service is down and holds them for replay, so recovery doesn't depend on Svix's retry window.
Best practices
Verify against the raw body
Use express.raw() in Express, Buffer.from(await request.arrayBuffer()) in a Next.js route handler, or await request.body() in FastAPI, and parse only after verification.
Store agreement_ external_ reference
Pass your own order reference when you create a project. It comes back on every event, so you can find the order without a lookup on project_uuid.
Route on event and handle cancellation reasons
Switch on event, and for cancellations act on cancellation_reason: a delivery_delayed cancellation may need a different customer message from changed_mind.
Make handlers idempotent
See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Return a 2xx as soon as the event is stored. See why to process webhooks asynchronously.
Conclusion
Taskrabbit sends three project events through Svix: completed, cancelled and rescheduled. Verify each delivery with your endpoint's secret and discard anything that fails, deduplicate on the Svix message ID because the body has no event ID, and treat timestamp on reschedules as the new start time.
Hookdeck Event Gateway verifies Taskrabbit's signatures and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Taskrabbit webhooks reliably in minutes.