Gareth Wilson Gareth Wilson

Guide to Taskrabbit Webhooks: Features and Best Practices

Published


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

FeatureDetails
AvailabilityPartner API, set up during onboarding
Delivery platformSvix
ConfigurationSvix Partner Dashboard
Event fieldBody event
Eventsproject.completed, project.canceled, project.rescheduled
SignatureSvix scheme, with a secret unique to each endpoint
RetriesExponential backoff on a non-2xx response or timeout
Manual replaySvix Partner Dashboard

Common events

Every payload has event, project_uuid and timestamp, plus agreement_external_reference if you gave the project your own reference when you created it.

EventWhen it firesExtra fields
project.completedThe Tasker has completed the project's workNone
project.canceledThe project has been cancelledcancellation_reason
project.rescheduledA task in the project has a new start timenew_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

  1. Open the Svix Partner Dashboard you were given during onboarding.
  2. Add your HTTPS endpoint and subscribe it to the events you need.
  3. Copy the endpoint's signing secret and store it server-side, for example as TASKRABBIT_WEBHOOK_SECRET.
  4. 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_time with the start time you have stored, and apply the change only if it matches.
  • Don't order events by timestamp across 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.