# 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.

```javascript
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:

```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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Subscribe with wildcards and dedupe

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

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Synctera webhooks reliably in minutes.