Skip to content
SynupAPI
Get an API key

Synup Webhooks

Synup can notify your systems in near-real-time when things happen in your account — a listing gets connected, a review campaign is sent, a social post publishes, an AI post idea is generated, and more. When an event occurs, Synup sends an HTTP POST with a JSON body to a URL you configure. Every delivery is cryptographically signed so you can verify it genuinely came from Synup.

This guide covers how to set webhooks up on your side, how signing and endpoint verification work (and why), and the shape of every delivery. The exact payload of each event is documented under the Webhooks section.

Quick start

  1. Stand up an HTTPS endpoint on your server that accepts POST requests (publicly reachable, valid TLS certificate).
  2. In Synup, go to Settings → Notifications → Configure Webhooks and set your Webhooks URL.
  3. Click Generate to create your Signing secret. Copy it and store it securely on your server — you’ll use it to (a) verify incoming signatures and (b) answer the verification challenge.
  4. Implement two things in your endpoint: a signature check on every request, and a challenge response for the one-time verification handshake.
  5. Click Verify endpoint in Synup. Synup sends a signed challenge; your endpoint proves it holds the secret. On success the status turns verified and deliveries begin.
  6. That’s it — you’ll receive every event for the account. There is no per-event subscription: if webhooks are enabled and your endpoint is verified, all events deliver.

Delivery is gated on four things: the webhooks feature is enabled on your account, a Webhooks URL is set, a signing secret has been generated, and your endpoint has passed verification. If any is missing, events are not delivered.

Configuring your endpoint

Your Webhooks URL must be:

  • HTTPS — plain HTTP is rejected.
  • Publicly resolvable — the host must resolve to a public IP. Internal/private addresses (loopback, RFC-1918, link-local, cloud metadata IPs, etc.) are refused.
  • Fast — respond within 10 seconds. Do the minimum synchronously (verify the signature, enqueue the payload) and process asynchronously. A slow endpoint is treated as a failed delivery.
  • Idempotent-friendly — you may occasionally receive the same event more than once. De-duplicate on the IDs inside data (they are stable).

You configure a single Webhooks URL per account; all event types are delivered to it.

The signing secret

A per-account secret string you generate in Settings → Notifications. It is the shared key between Synup and your server. Because your endpoint is a public URL, the signing secret lets you prove a request actually came from Synup and was not tampered with in transit. The secret itself is never transmitted — every delivery instead carries an HMAC computed with it.

Every webhook request includes this header:

X-Synup-Signature: sha256=<base64( HMAC-SHA256(secret, raw_request_body) )>

To verify, on your side:

  1. Read the raw request body bytes exactly as received — do not re-serialize the parsed JSON (key order / whitespace differences break the HMAC).
  2. Compute HMAC-SHA256(your_secret, raw_body), Base64-encode it, and prefix with sha256=.
  3. Compare against the X-Synup-Signature header using a constant-time comparison.
  4. If they don’t match, reject the request (return 401) and do not process it.

Rotating the secret. You can Regenerate the secret at any time. When you do, the old secret immediately stops validating and your endpoint’s verified status is reset — update the secret on your server and re-verify before deliveries resume. (Changing the Webhooks URL also resets verification.)

Endpoint verification

A one-time (repeatable) challenge–response handshake, triggered by the Verify endpoint button, that confirms your endpoint is reachable and actually holds the signing secret — before Synup starts sending real events. Verification is the final gate: deliveries stay off until it passes.

When you click Verify endpoint, Synup sends a signed POST with body:

{ "event": "endpoint.verification", "nonce": "<random hex string>", "timestamp": "<ISO-8601 UTC>" }

Your endpoint must recognize the special event endpoint.verification, compute HMAC-SHA256(secret, nonce) as a lowercase hex string, and reply HTTP 200 with that hex string as the body (a quoted/JSON-wrapped hex string is also accepted). Synup compares your reply to its own computation; on a match the endpoint is marked verified and deliveries turn on.

Verification is automatically reset to unverified when you change the Webhooks URL or regenerate the secret, or when a verification attempt fails (including when a previously-working endpoint later stops resolving or responding). Deliveries pause until you click Verify endpoint again.

Delivery mechanics — the payload envelope

Every event (except the verification challenge) is delivered with the same top-level shape:

{
  "event": "connection.location_connected",
  "timestamp": "2026-07-15T14:32:10Z",
  "account_id": 11073,
  "location_id": "279381",
  "data": { }
}
Field Type Notes
event string The dotted event name (e.g. campaign.sent).
timestamp ISO-8601 string When the event was emitted (UTC).
account_id integer The Synup account the event belongs to.
location_id string | null Numeric string for location-scoped events; null for brand-scoped events (social posts, social connections, social ideas). For brand-scoped events the brand is identified by social_profile_id inside data.
data object Event-specific fields — documented per event under Webhooks.
agency_account_id integer Only present when the account is managed by a parent agency account.
scope string Only present for AI Post Idea events"local" or "social".

Synup records each delivery attempt (attempt count, last attempt time, HTTP response code). Return a 2xx to acknowledge receipt; any non-2xx (or a timeout) is treated as a failed attempt. Optional fields are omitted from data when empty rather than sent as null — always code defensively and treat missing optional keys and null the same way.

Code samples

Node.js (Express) — capture the raw body so the HMAC matches exactly:

const crypto = require('crypto');
const express = require('express');
const app = express();

const SECRET = process.env.SYNUP_WEBHOOK_SECRET;

// IMPORTANT: keep the raw body for signature verification.
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

function isValidSignature(rawBody, header) {
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('base64');
  const a = Buffer.from(header || '');
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/synup/webhooks', (req, res) => {
  if (!isValidSignature(req.rawBody, req.get('X-Synup-Signature'))) {
    return res.status(401).send('bad signature');
  }

  const body = req.body;

  // Verification handshake: reply with hex HMAC(secret, nonce).
  if (body.event === 'endpoint.verification') {
    const answer = crypto.createHmac('sha256', SECRET).update(body.nonce).digest('hex');
    return res.status(200).send(answer);
  }

  // Real event — acknowledge fast, process asynchronously.
  enqueue(body);              // your own async handoff
  return res.status(200).json({ received: true });
});

Python (Flask):

import os, hmac, hashlib, base64
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["SYNUP_WEBHOOK_SECRET"].encode()

def valid_signature(raw_body: bytes, header: str) -> bool:
    expected = "sha256=" + base64.b64encode(
        hmac.new(SECRET, raw_body, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(header or "", expected)

@app.post("/synup/webhooks")
def webhooks():
    raw = request.get_data()  # raw bytes, exactly as received
    if not valid_signature(raw, request.headers.get("X-Synup-Signature")):
        abort(401)

    body = request.get_json(silent=True) or {}

    # Verification handshake.
    if body.get("event") == "endpoint.verification":
        answer = hmac.new(SECRET, body["nonce"].encode(), hashlib.sha256).hexdigest()
        return answer, 200

    enqueue(body)             # your own async handoff
    return {"received": True}, 200

Operational notes & FAQ

  • One URL, all events. There is no per-event subscription. If webhooks are enabled and your endpoint is verified, you receive every event type.
  • Always verify the signature. Treat any request that fails signature verification as hostile — return 401 and ignore it.
  • Use the raw body for the HMAC. Re-serializing parsed JSON changes bytes and breaks verification.
  • Acknowledge fast (2xx), process later. You have ~10 seconds. Enqueue and return; do slow work off the request path.
  • Expect occasional duplicates. De-duplicate on the stable IDs inside data.
  • Ordering is not guaranteed. Don’t assume events arrive in the exact order they occurred.
  • Rotating the secret or changing the URL resets verification — update your server and click Verify endpoint again to resume deliveries.
  • location_id is null for brand-scoped events (social posts, social connections, social ideas). Use social_profile_id inside data to identify the brand.
  • agency_account_id appears only when the account is managed by a parent agency.