Guide to Synctera Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | POST /v0/webhooks (url, enabled_events[], is_enabled, ...) |
| Signature headers | Synctera-Signature, Request-Timestamp (POSIX seconds) |
| Signature scheme | HMAC-SHA256 over {Request-Timestamp}.{raw_body}, hex |
| Secret | Generated via POST /v0/webhook_secrets (not the API key) |
| Rotation | Rolling secret may put two delimited values in the header, check both |
| Replay window | Reject if Request-Timestamp is more than 5 minutes off |
| Events | <resource>.[<sub>.]<action>, with wildcards (CUSTOMER.*) |
| Delivery | 2xx within 5s; retries with backoff up to ~55h; 60-day retention |
| SDK | Official Go client only |
Common events
Synctera event names use <resource>.[<sub-resource>.]<action>:
| Event | Fires when |
|---|---|
ACCOUNT.UPDATED | An account updates |
TRANSACTIONS.POSTED.CREATED | A 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.