Webhooks
Marca can POST to your own endpoint when work changes in a project: a task is created, edited or moved, someone comments, items are exported, or a crawl publishes pages. Use it to sync another tracker, notify a channel, or kick off an agent.
Set up an endpoint
In your project, open Integrations → Webhooks → Add webhook, enter an https:// URL and pick the events. Marca shows the endpoint's signing secret once — store it on your server. You can also manage endpoints through the API with a key that has webhooks:manage (see the API reference).
Send test queues a ping so you can check your endpoint before real events arrive.
Events
| Event | When |
|---|---|
task.created | A task or subtask is created |
task.updated | A task's instruction, assignee or priority changes |
task.status_changed | A task moves to another status |
task.comment_added | Someone comments on a task |
comment.created | A reviewer pins a comment on a page |
export.created | Items are exported (one event per export) |
manifest.published | A crawl publishes a new set of pages |
Every delivery is a JSON body like this:
{
"id": "0a1b…",
"type": "task.status_changed",
"createdAt": "2026-10-08T15:04:05.000Z",
"project": { "token": "proj_…" },
"actor": { "email": "[email protected]", "via": "claude-code" },
"data": {
"task": { "id": "TASK-2026-10-08-001", "status": "done", "route": "/pricing", "instruction": "…", "url": "https://marca.dev/dashboard/…" },
"change": { "kind": "task.status_changed", "from": "in_review", "to": "done" }
}
}
actor.via names the agent or API key that acted for the person, when one did. The id is the same across retries of one delivery, so use it to ignore duplicates.
Verify the signature
Each request carries:
X-Marca-Event— the event typeX-Marca-Delivery— a unique id for this deliveryX-Marca-Signature: t=<unix seconds>,v1=<hex>— HMAC-SHA256 of"<t>.<raw body>", keyed with your signing secret
Check it against the raw body before trusting anything, and reject old timestamps so a captured request can't be replayed. With the SDK:
import { parseWebhook } from '@marca/sdk';
app.post('/hooks/marca', express.raw({ type: 'application/json' }), async (req, res) => {
const event = await parseWebhook(process.env.MARCA_WEBHOOK_SECRET, req.body.toString('utf8'), req.get('x-marca-signature'));
// event.type, event.data …
res.sendStatus(204);
});
Without the SDK:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, rawBody, header) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
Delivery and retries
- Respond with any
2xxwithin 10 seconds. The response body is ignored. - Anything else — an error status, a timeout, a connection failure — is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours, then marked failed.
- Each endpoint's recent deliveries, with status codes and errors, are listed under the endpoint in the Integrations tab. Redeliver sends one again with a full retry budget.
- Events are usually sent within a few seconds, in a consistent order per endpoint, but don't rely on strict ordering across retries. A change saved in a long-running operation is sent once that operation finishes, so it can arrive after changes made later.
- Disabling an endpoint stops new deliveries; when you re-enable it, it resumes from that moment rather than replaying what it missed.
Security
- Public https only. Endpoints on private, loopback, link-local or cloud-metadata addresses are refused, and so are plain
http://URLs. - Checked on every attempt. The destination is resolved and checked again before each delivery, so a hostname that later points inside a private network is blocked — that delivery is marked
blockedand not retried. Redirects are never followed. - Rotate the secret from the endpoint's menu if it leaks; the old one stops working immediately.