Gareth Wilson Gareth Wilson

Guide to Synctera Webhooks: Features and Best Practices

Published


Synctera webhooks notify your systems about banking-as-a-service activity: an account updates, a transaction posts, a customer changes. If you're building on Synctera's BaaS platform, webhooks are how you react to account and transaction events without polling.

This guide covers how Synctera webhooks work, the events you'll subscribe to, how to verify the Synctera-Signature, secret rotation, the replay window, and the best practices for production.

What are Synctera webhooks?

Synctera webhooks are HTTP POSTs delivered to endpoints you create, each signed with an HMAC-SHA256. Two headers matter: Synctera-Signature (the signature) and Request-Timestamp (POSIX seconds). The signed message is {Request-Timestamp}.{raw_body}.

The important detail: the signing secret is not your API key. You generate it via POST /v0/webhook_secrets and store it.

Synctera webhook features

FeatureDetails
ConfigurationPOST /v0/webhooks (url, enabled_events[], is_enabled, ...)
Signature headersSynctera-Signature, Request-Timestamp (POSIX seconds)
Signature schemeHMAC-SHA256 over {Request-Timestamp}.{raw_body}, hex
SecretGenerated via POST /v0/webhook_secrets (not the API key)
RotationRolling secret may put two delimited values in the header, check both
Replay windowReject if Request-Timestamp is more than 5 minutes off
Events<resource>.[<sub>.]<action>, with wildcards (CUSTOMER.*)
Delivery2xx within 5s; retries with backoff up to ~55h; 60-day retention
SDKOfficial Go client only

Common events

Synctera event names use <resource>.[<sub-resource>.]<action>:

EventFires when
ACCOUNT.UPDATEDAn account updates
TRANSACTIONS.POSTED.CREATEDA posted transaction is created
CUSTOMER.*Any customer event (wildcard; auto-subscribes to future events under CUSTOMER)

Wildcards like CUSTOMER.* auto-subscribe you to future events under that resource. Subscribe via enabled_events[] and consult Synctera's event reference for the full enum.

Setting up Synctera webhooks

Generate a signing secret with POST /v0/webhook_secrets (empty body) and store it, this is not your API key. Then create the webhook with POST /v0/webhooks, providing url, enabled_events[], is_enabled, and optional description/metadata. Test with POST /v0/webhooks/trigger.

Securing Synctera webhooks

To verify, build {Request-Timestamp}.{raw_body} (the timestamp, a literal dot, then the raw body), compute an HMAC-SHA256 with your webhook secret, hex-encode it, and compare against Synctera-Signature. Reject deliveries where Request-Timestamp is more than 5 minutes from now. During rotation, the header may carry two delimited values, accept a match against either.

const crypto = require("crypto");

// One or more current secrets (rotation can make two valid at once)
const SECRETS = (process.env.SYNCTERA_WEBHOOK_SECRETS || "").split(",");

function verify(rawBody, timestamp, signatureHeader) {
  // Reject stale deliveries (timestamp is POSIX seconds)
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const message = `${timestamp}.${rawBody.toString()}`;
  // The header may carry multiple delimited signatures during rotation
  const provided = (signatureHeader || "").split(",");

  return SECRETS.some((secret) => {
    const expected = crypto.createHmac("sha256", secret).update(message).digest("hex");
    return provided.some((sig) => {
      const a = Buffer.from(expected);
      const b = Buffer.from(sig.trim());
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    });
  });
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.headers["request-timestamp"];
  if (!verify(req.body, timestamp, req.headers["synctera-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge within 5s
  processQueue.add(JSON.parse(req.body)); // process asynchronously
});

The same check in Python:

import hashlib
import hmac
import os
import time

SECRETS = os.environ.get("SYNCTERA_WEBHOOK_SECRETS", "").split(",")


def verify(raw_body: bytes, timestamp: str, signature_header: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    message = (timestamp + ".").encode() + raw_body
    provided = (signature_header or "").split(",")
    for secret in SECRETS:
        expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
        if any(hmac.compare_digest(expected, s.strip()) for s in provided):
            return True
    return False

Synctera webhook limitations and pain points

The secret isn't your API key

The Problem: Verification uses a dedicated webhook secret from POST /v0/webhook_secrets, not your Synctera API key. Signing with the API key fails.

Why It Happens: Synctera issues a separate signing secret for webhooks.

Workarounds:

  • Generate and store the webhook secret; use it (not the API key) for verification.

How Hookdeck Can Help: Hookdeck verifies the Synctera-Signature at the edge, so the secret lives in one place.

Rolling secrets during rotation

The Problem: When you rotate, the signature header can carry two delimited values (old and new). Code that checks only one rejects valid deliveries signed with the other.

Why It Happens: Synctera overlaps secrets to rotate without dropping deliveries.

Workarounds:

  • Accept a match against either delimited signature (and either secret), as above.

How Hookdeck Can Help: Hookdeck handles the rotation case at the edge, so your app never juggles two secrets.

The 5-minute replay window

The Problem: Deliveries with a Request-Timestamp more than 5 minutes off must be rejected. A backlogged handler can see events age out.

Why It Happens: The freshness window is the scheme's replay protection.

Workarounds:

  • Acknowledge fast and process asynchronously so verification happens within the window; keep clocks in sync.

How Hookdeck Can Help: Hookdeck verifies once at the edge and durably queues, so a downstream backlog doesn't risk aging out.

The 5-second ack and long retries

The Problem: You must return 200 within 5 seconds, and failed deliveries retry with backoff up to ~55 hours, bringing duplicates.

Why It Happens: Synctera caps the ack time and retries persistently (events retained 60 days).

Workarounds:

  • Acknowledge fast, process off a queue, and dedupe on an event id.

How Hookdeck Can Help: Hookdeck acknowledges within the window, deduplicates, and retries to your downstream on your schedule. See our guide to webhook idempotency.

Best practices

Verify over Request-Timestamp.body with the webhook secret

Build {timestamp}.{body}, HMAC-SHA256 with the secret from /v0/webhook_secrets, hex, compare in constant time, and reject stale timestamps.

Handle rotation

Accept a match against either delimited signature during rotation.

Acknowledge within 5 seconds, process asynchronously

Return 200 quickly and defer work to a queue. See why to process webhooks asynchronously.

Subscribe with wildcards and dedupe

Use enabled_events[] (wildcards like CUSTOMER.* catch future events), and dedupe on an event id.

Make Synctera webhooks production-ready

Hookdeck verifies Synctera-Signature, handles rotation, deduplicates, and durably queues every event

Conclusion

Synctera webhooks verify with a Synctera-Signature HMAC over {Request-Timestamp}.{raw_body}, keyed with a dedicated secret from /v0/webhook_secrets (not your API key), with a 5-minute replay window and rolling secrets during rotation. Verify against the raw body, handle both secrets/signatures during rotation, acknowledge within 5 seconds, and dedupe.

Hookdeck verifies the signature, handles rotation, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

Get started with Hookdeck for free and handle Synctera webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.