Webhooks
Two webhook surfaces: signed inbound (your sources → Switchyard) and outbound callbacks from ClickUp. Both use HMAC; the schemes are deliberately different so operators don't accidentally cross them up.
Inbound — sources → Switchyard
URL: POST /webhooks/inbound/{workspace_id}/{source_slug}
Signature header (Stripe/Inkwell-style):
X-Switchyard-Signature: t=<unix_seconds>,v1=<hex_sha256>
where v1 = hmac_sha256(source.inbound_secret, t + "." + raw_body)Replay tolerance is 5 minutes from t. The signed payload includes the timestamp, so a replay can't swap it in.
Signing recipe (Node)
import { createHmac } from 'node:crypto';
function sign(secret, body, t = Math.floor(Date.now() / 1000)) {
const v1 = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex');
return `t=${t},v1=${v1}`;
}
await fetch('https://api.switchyard.philiprehberger.com/webhooks/inbound/' + workspaceId + '/scopeforged-form', {
method: 'POST',
headers: {
'X-Switchyard-Signature': sign(SOURCE_SECRET, JSON.stringify(payload)),
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});Idempotency
Include an Idempotency-Key header (any unique string, ≤64 chars). A repeated key within 24 hours returns the original 202 response and does not enqueue a second ingest. After 24 hours the key expires and the next POST proceeds.
Error responses
401— signature missing, malformed, expired, or mismatched404— workspace or source slug not found409— source exists but is disabled
Outbound — ClickUp → Switchyard
URL: POST /webhooks/clickup/{workspace_id}
Subscribed on connect for taskStatusUpdated, taskUpdated, and taskDeleted events. ClickUp signs with X-Signature: <hex> where the hex is hmac_sha256(workspace.webhook_secret, raw_body).
On a valid signature, Switchyard dispatches MirrorClickUpStatusJob which looks up the local lead by clickup_task_id, translates the ClickUp status via status_mapping, and updates the lead row. Unmapped status names emit a mirroredevent for audit but don't change the local status column.