Webhooks
Syncupp POSTs a JSON body to your endpoint when something happens in your workspace. Every delivery is signed, so you can be sure it came from us and was not modified in transit.
No third-party automation platform is involved — your server talks to ours directly.
Setting one up
- In Syncupp, go to Settings → Integrations → Webhooks.
- Add your endpoint URL and tick the events you want.
- Copy the signing secret. It is shown once, when the webhook is created, and cannot be retrieved afterwards. If you lose it, delete the webhook and create it again.
- Press Send test. The result shows the exact status or error your endpoint returned, so you can confirm the whole path before waiting on a real event.
Your URL must be https and publicly reachable. Private and loopback addresses are rejected — if you are developing locally, use a tunnel such as ngrok.
What we send
A POST with a JSON body and these headers:
POST /your-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Syncupp-Webhooks/1.0
X-Syncupp-Event: task.completed
X-Syncupp-Signature: t=1756108800,v1=5f2b9c...
X-Syncupp-Delivery: 3fa85f64-5717-4562-b3fc-2c963f66afa6
{
"event": "task.completed",
"delivered_at": "2026-08-25T09:20:00.000Z",
"workspace_id": "6a58b043b0e0c8212a9ac324",
"data": { }
}- X-Syncupp-Event — the event name, also present in the body.
- X-Syncupp-Signature — see verifying.
- X-Syncupp-Delivery — a unique id for this attempt. Useful in your logs, and for spotting a repeat.
- data — the task, board or meeting the event is about.
Verifying the signature
The header looks like t=1756108800,v1=5f2b9c.... t is the Unix timestamp we signed at; v1 is an HMAC-SHA256, hex encoded.
To check it:
- Take the raw request body — the exact bytes, before any JSON parsing. Re-serialising a parsed object will not match.
- Build the string
{t}.{body}— the timestamp, a full stop, then the body. - HMAC-SHA256 it with your signing secret and hex encode the result.
- Compare with v1 using a constant-time comparison.
- Reject anything where t is more than a few minutes old — that is what stops a captured delivery being replayed later.
Node.js / Express
const crypto = require('crypto');
// The raw body is required — express.json() alone will not do.
app.post('/syncupp-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const header = req.get('X-Syncupp-Signature') || '';
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('='))
);
const expected = crypto
.createHmac('sha256', process.env.SYNCUPP_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body.toString('utf8')}`)
.digest('hex');
const ok =
parts.v1 &&
expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!ok) return res.status(400).send('bad signature');
// Reject anything older than five minutes.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300)
return res.status(400).send('stale');
const event = JSON.parse(req.body.toString('utf8'));
// ... do your work, then answer quickly (see below)
res.sendStatus(200);
}
);Python / Flask
import hmac, hashlib, time
from flask import request
@app.post("/syncupp-webhook")
def syncupp_webhook():
header = request.headers.get("X-Syncupp-Signature", "")
parts = dict(kv.split("=", 1) for kv in header.split(","))
body = request.get_data(as_text=True) # raw, not request.json
expected = hmac.new(
SYNCUPP_WEBHOOK_SECRET.encode(),
f"{parts['t']}.{body}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
return "bad signature", 400
if abs(time.time() - int(parts["t"])) > 300:
return "stale", 400
event = request.get_json()
# ... do your work
return "", 200If you skip this check, anyone who learns your URL can post events that look like ours. The secret is the only thing that distinguishes them.
Events
| Event | Sent when |
|---|---|
task.created | A task was created. |
task.updated | A task was edited — title, description, dates, assignees, priority. |
task.completed | A task moved into a completed column. Fires on the move itself, not on every later change. |
task.deleted | A task was deleted. |
board.created | A board was created. |
board.updated | A board was edited. |
meeting.created | A meeting was created. |
meeting.updated | A meeting was edited. |
A webhook receives only the events you ticked. One endpoint can take several, and one workspace can have up to 20 endpoints.
Delivery behaviour
- Answer within 8 seconds. We give up after that and record a timeout. Acknowledge first and do the slow work afterwards.
- Any 2xx means delivered. Anything else is recorded as a failure, with the status shown in Settings.
- Redirects are not followed. Register the final URL.
- There are no retries yet. A failed delivery is recorded, not re-sent. If an event matters, reconcile against your own data rather than relying on every delivery arriving.
- 20 consecutive failures pauses the endpoint. It is paused, not deleted — the last error is kept so you can see what went wrong, and you switch it back on in Settings.
- Handle repeats. Use X-Syncupp-Delivery, or make your handler safe to run twice.
If something is not arriving
- Check the webhook in Settings — the last delivery result and error are shown on the row.
- Press Send test. It reports the real response from your endpoint.
- Signature never matches — you are almost certainly hashing a re-serialised body. Use the raw bytes.
- “That address is not publicly routable” — the URL resolves to a private or loopback address. Use a tunnel for local development.
- Endpoint switched itself off — 20 failures in a row. Fix the cause, then re-enable it.
Something unclear or missing? Write to webmaster@syncupp.com — including what you were trying to build helps more than a bug report.