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.
Each webhook request includes these headers:
| Header | Description |
|---|
Content-Type | application/json |
User-Agent | Redbark-Webhook/1.0 |
X-Redbark-Signature | HMAC-SHA256 signature in the format sha256=<hex_digest> |
X-Redbark-Timestamp | Unix timestamp (seconds) when the signature was created |
X-Redbark-Delivery-Id | Unique UUID for this delivery attempt (matches id in the payload) |
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
| Field | Type | Description |
|---|
id | string | Unique event identifier (UUID) |
object | string | Always "event" |
type | string | Event type, currently always "transactions.synced" |
api_version | string | API version used to generate this payload |
created | integer | Unix timestamp (seconds) of when the event was created |
data | object | Contains new and updated transaction arrays |
metadata | object | Sync run context and chunking information |
Transaction fields
Each transaction in data.new contains:
| Field | Type | Description |
|---|
id | string | Unique transaction identifier |
object | string | Always "transaction" |
amount | integer | Amount in smallest currency unit (e.g. cents). Negative for debits. |
currency | string | null | Lowercase ISO 4217 currency code (e.g. "aud", "nzd") |
status | string | Always "posted" |
description | string | Transaction description |
direction | string | "credit" or "debit" |
class | string | "payment", "transfer", "fee", "interest", or "other" |
account_id | string | Stable account identifier (UUID). Use this to match transactions to accounts from the Accounts API. |
account_name | string | Human-readable account name (e.g. "Everyday Account") |
account | string | Deprecated. Same as account_name. Use account_id and account_name instead. |
transaction_date | string | Transaction date (YYYY-MM-DD) |
post_date | string | null | Date the transaction posted (YYYY-MM-DD) |
merchant_name | string | null | Merchant name |
category | string | null | Transaction category |
merchant_category_code | string | null | Merchant 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:
| Field | Type | Description |
|---|
previous_attributes | object | Reserved for future use. Currently an empty object {}. |
Updates are sent when the bank retroactively corrects a posted transaction’s amount or description.
| Field | Type | Description |
|---|
sync_run_id | string | null | UUID of the sync run that triggered this delivery |
new_count | integer | Number of new transactions in this payload |
updated_count | integer | Number of updated transactions in this payload |
chunk | integer | Current chunk number (1-indexed) |
total_chunks | integer | Total 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
- Extract the
X-Redbark-Timestamp and X-Redbark-Signature headers
- Concatenate the timestamp and raw request body with a period:
<timestamp>.<body>
- Compute the HMAC-SHA256 digest of this string using your signing secret
- Compare your computed signature with the one in the header (strip the
sha256= prefix)
- 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 code | Behaviour |
|---|
| 2xx | Success. Delivery is recorded, failure counter resets. |
| 429 | Rate limited. Redbark retries with backoff. |
| Other 4xx | Client error. Not retried. |
| 5xx | Server 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.