Sortd delivers events to your URL with a Stripe-style signature, retries on failure, and keeps a replayable delivery log.
Needs a key with webhooks.manage.
Register the URL to receive events at and the event types you want. The response includes the subscription id (wh_…) and a signing secret (whsec_…) — shown once; store it to verify deliveries.
curl -X POST "https://api.sortd.com/v2/webhooks/subscriptions" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://your-app.example.com/hooks/sortd",
"events": ["task.created", "task.completed"]
}'
# -> { "data": { "id": "wh_…", "target_url": "…", "events": ["…"], "secret": "whsec_…" }, … }
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "sk_live_…");
var content = new StringContent("""
{ "target_url": "https://your-app.example.com/hooks/sortd", "events": ["task.created"] }
""", Encoding.UTF8, "application/json");
var res = await client.PostAsync("https://api.sortd.com/v2/webhooks/subscriptions", content);
Each delivery carries an x-sortd-signature header of the form t=<unix>,v1=<hex>. The signed payload is <t>.<raw request body>, HMAC-SHA256 with your subscription secret. Recompute it over the raw body, compare in constant time, and reject if the timestamp is outside your tolerance (defends against replay).
import crypto from "crypto";
function verifySortdSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // stale
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Mount with the RAW body (not parsed JSON) so the bytes match what was signed:
app.post("/hooks/sortd", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifySortdSignature(req.body.toString("utf8"), req.get("x-sortd-signature"), "whsec_…");
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body.toString("utf8"));
// … handle event … then ack quickly:
res.status(200).end();
});
import hmac, hashlib, time
from flask import request, abort
def verify_sortd_signature(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", 0))
if not t or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
@app.post("/hooks/sortd")
def hook():
if not verify_sortd_signature(request.get_data(), request.headers.get("x-sortd-signature", ""), "whsec_…"):
abort(400)
# … handle request.get_json() … then return 200 quickly
return "", 200
Return a 2xx quickly to acknowledge. Non-2xx (or a timeout) is retried with backoff.
POST /webhooks/subscriptions/{id}/test sends a ping so you can confirm your endpoint + verification work.GET /webhooks/subscriptions/{id}/deliveries shows recent attempts, status and response.POST /webhooks/deliveries/{deliveryId}/replay re-sends a past delivery (handy after a fix).POST /webhooks/subscriptions/{id}/rotate-secret issues a new whsec_…; update your verifier when you rotate.A webhook needs a server you host — Sortd delivers to your HTTPS endpoint. A chat assistant (Claude, ChatGPT, a scheduled assistant task) can't be that endpoint: it's a client with no public URL. If you want an assistant to react to Sortd events, either have it poll on a schedule through the MCP connector, or receive the webhook in an agent runner or workflow platform — n8n, Make, Pipedream, Zapier, or a service you host — that acts on it or puts it where the assistant will look (platform patterns). More in Connect your AI.
See every endpoint in the reference.