Skip to main content
Webhooks are in beta. The payload format and signing scheme may change.
When a sync runs for a webhook destination, Redbark sends an HTTP POST to your endpoint with a JSON payload containing new and updated transactions. Every request is signed with HMAC-SHA256.

Request headers

Each webhook request includes these headers:
HeaderDescription
Content-Typeapplication/json
User-AgentRedbark-Webhook/1.0
X-Redbark-SignatureHMAC-SHA256 signature in the format sha256=<hex_digest>
X-Redbark-TimestampUnix timestamp (seconds) when the signature was created
X-Redbark-Delivery-IdUnique UUID for this delivery attempt (matches id in the payload)

Payload format

All field names use snake_case. Monetary amounts are integers in the smallest currency unit (e.g. cents). Timestamps are Unix epoch seconds. Dates are YYYY-MM-DD strings. Missing optional fields are null, never empty strings.
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "object": "event",
  "type": "transactions.synced",
  "api_version": "2026-03-15",
  "created": 1741243200,
  "data": {
    "new": [
      {
        "id": "txn_abc123",
        "object": "transaction",
        "amount": -4550,
        "currency": "aud",
        "status": "posted",
        "description": "Woolworths Sydney",
        "direction": "debit",
        "class": "payment",
        "account_id": "d4e5f6a7-b8c9-0123-4567-890abcdef012",
        "account_name": "Everyday Account",
        "account": "Everyday Account",
        "transaction_date": "2026-03-05",
        "post_date": "2026-03-05",
        "merchant_name": "Woolworths",
        "category": "Groceries",
        "merchant_category_code": null
      }
    ],
    "updated": []
  },
  "metadata": {
    "sync_run_id": "run_abc123",
    "new_count": 1,
    "updated_count": 0,
    "chunk": 1,
    "total_chunks": 1
  }
}

Top-level fields

FieldTypeDescription
idstringUnique event identifier (UUID)
objectstringAlways "event"
typestringEvent type, currently always "transactions.synced"
api_versionstringAPI version used to generate this payload
createdintegerUnix timestamp (seconds) of when the event was created
dataobjectContains new and updated transaction arrays
metadataobjectSync run context and chunking information

Transaction fields

Each transaction in data.new contains:
FieldTypeDescription
idstringUnique transaction identifier
objectstringAlways "transaction"
amountintegerAmount in smallest currency unit (e.g. cents). Negative for debits.
currencystring | nullLowercase ISO 4217 currency code (e.g. "aud", "nzd")
statusstringAlways "posted"
descriptionstringTransaction description
directionstring"credit" or "debit"
classstring"payment", "transfer", "fee", "interest", or "other"
account_idstringStable account identifier (UUID). Use this to match transactions to accounts from the Accounts API.
account_namestringHuman-readable account name (e.g. "Everyday Account")
accountstringDeprecated. Same as account_name. Use account_id and account_name instead.
transaction_datestringTransaction date (YYYY-MM-DD)
post_datestring | nullDate the transaction posted (YYYY-MM-DD)
merchant_namestring | nullMerchant name
categorystring | nullTransaction category
merchant_category_codestring | nullMerchant category code (MCC)
Amounts are integers in the smallest currency unit. For example, $45.50 AUD is 4550. Zero-decimal currencies like JPY use the full amount (e.g. ¥500 is 500).

Updated transaction fields

Each entry in data.updated contains all the same fields above, plus:
FieldTypeDescription
previous_attributesobjectReserved for future use. Currently an empty object {}.
Updates are sent when the bank retroactively corrects a posted transaction’s amount or description.

Metadata fields

FieldTypeDescription
sync_run_idstring | nullUUID of the sync run that triggered this delivery
new_countintegerNumber of new transactions in this payload
updated_countintegerNumber of updated transactions in this payload
chunkintegerCurrent chunk number (1-indexed)
total_chunksintegerTotal number of chunks for this sync

Chunking

When a sync produces more than 500 transactions, the payload is split into multiple requests. Each chunk is delivered sequentially as a separate POST. Use metadata.chunk and metadata.total_chunks to reassemble if needed.

Verifying signatures

Every webhook request is signed with HMAC-SHA256 using your signing secret. Verify the signature on every request to confirm it came from Redbark and has not been tampered with.

Verification steps

  1. Extract the X-Redbark-Timestamp and X-Redbark-Signature headers
  2. Concatenate the timestamp and raw request body with a period: <timestamp>.<body>
  3. Compute the HMAC-SHA256 digest of this string using your signing secret
  4. Compare your computed signature with the one in the header (strip the sha256= prefix)
  5. Check that the timestamp is within 5 minutes of the current time to prevent replay attacks

Example: Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(req, signingSecret) {
  const signature = req.headers["x-redbark-signature"];
  const timestamp = req.headers["x-redbark-timestamp"];
  const body = req.rawBody; // raw request body as string

  // Check timestamp is within 5 minutes
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) {
    throw new Error("Timestamp too old, possible replay attack");
  }

  // Compute expected signature
  const hmac = createHmac("sha256", signingSecret);
  hmac.update(`${timestamp}.${body}`);
  const expected = `sha256=${hmac.digest("hex")}`;

  // Constant-time comparison
  if (
    signature.length !== expected.length ||
    !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  ) {
    throw new Error("Invalid signature");
  }

  return JSON.parse(body);
}

Example: Python

import hashlib
import hmac
import time

def verify_webhook(headers, body, signing_secret):
    signature = headers["X-Redbark-Signature"]
    timestamp = headers["X-Redbark-Timestamp"]

    # Check timestamp is within 5 minutes
    age = abs(time.time() - int(timestamp))
    if age > 300:
        raise ValueError("Timestamp too old, possible replay attack")

    # Compute expected signature
    signed_content = f"{timestamp}.{body}".encode()
    expected = "sha256=" + hmac.new(
        signing_secret.encode(), signed_content, hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid signature")

    return json.loads(body)

Expected response

Your endpoint must return a 2xx status code (e.g. 200 OK) to acknowledge receipt. The response body is ignored.
Status codeBehaviour
2xxSuccess. Delivery is recorded, failure counter resets.
429Rate limited. Redbark retries with backoff.
Other 4xxClient error. Not retried.
5xxServer error. Retried with backoff.
See Delivery and retries for the full retry schedule.

Deduplication

Transaction IDs that have been successfully delivered are tracked for 90 days. If a transaction was already delivered, it will not appear in data.new on subsequent syncs. Your endpoint should still handle potential duplicates gracefully (e.g. using the transaction id as an idempotency key) in case of network-level retries.