Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.redbark.co/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks are in beta. The payload format and signing scheme may change.
When a sync runs for a webhook destination, Redbark sends one or more HTTP POSTs to your endpoint with a JSON payload. Transaction syncs produce transactions.synced events; brokerage syncs produce trades.synced events. Every request is signed with HMAC-SHA256.

Event types

typeWhen sentdata contents
transactions.syncedBank transaction sync completesTransaction objects in new and updated
trades.syncedBrokerage trade sync completesTrade objects in new (updated is reserved for future use and currently always empty)
The envelope shape is identical across event types — only the type field and the contents of data.new / data.updated change. Consumers that only care about one event type should filter on event.type and ignore the rest.

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 on transactions are integers in the smallest currency unit (e.g. cents). Envelope timestamps (created) are Unix epoch seconds. The local_date field on transactions is a YYYY-MM-DD string; transaction_date and post_date are full ISO 8601 timestamps passed through from the banking provider. 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": "e4a7f91b2c3d4e5f6a7b8c9d",
        "object": "transaction",
        "amount": -4550,
        "currency": "aud",
        "status": "posted",
        "description": "Woolworths Sydney",
        "direction": "debit",
        "class": "payment",
        "account_id": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890",
        "account_name": "Everyday Account",
        "account": "Everyday Account",
        "local_date": "2026-03-05",
        "transaction_date": "2026-03-05T10:30:00+11:00",
        "post_date": "2026-03-05T10:30:00+11:00",
        "merchant_name": "Woolworths",
        "category": "FOOD_AND_DRINK",
        "merchant_category_code": null,
        "reference": null,
        "extended_description": 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 — "transactions.synced" or "trades.synced". See Event types.
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
idstringOpaque transaction identifier from the banking provider
objectstringAlways "transaction"
amountintegerAmount in smallest currency unit (e.g. cents). Negative for debits.
currencystring | nullLowercase ISO 4217 currency code (e.g. "aud", "nzd")
statusstring"posted" by default. If the sync has includePendingTransactions enabled, pending transactions are also delivered with status: "pending".
descriptionstringTransaction description
directionstring"credit" or "debit"
classstring"payment", "transfer", "fee", "interest", or "other"
account_idstringUUID of the account. 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.
local_datestringTransaction date converted to the user’s timezone (YYYY-MM-DD). Based on the timezone in the user’s settings.
transaction_datestringRaw transaction timestamp from the bank (full ISO 8601). Use local_date for display.
post_datestring | nullFull ISO 8601 timestamp from the bank (not a date-only string — passed through verbatim from the provider’s posting_date_time)
merchant_namestring | nullMerchant name
categorystring | nullRaw CDR primary-category code (uppercase, e.g. FOOD_AND_DRINK). See REST categories for the full list.
merchant_category_codestring | nullMerchant category code (MCC)
referencestring | nullBank-provided transaction reference (e.g. BPAY reference, NPP end-to-end ID)
extended_descriptionstring | nullProvider-supplied extended description (NPP payload, payee/payer info) when available
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. Always {} today — the diff itself is not yet surfaced.
data.updated is populated when a sync detects mutations to previously-delivered transactions (for example, the banking provider’s stored values for a posted transaction change between syncs). The full current attributes are sent on every entry; you can compare against your own stored copy to derive the diff while previous_attributes remains reserved.

Trade fields

Trade events use the same envelope as transactions, but with type: "trades.synced" and Trade objects in data.new. Quantities, prices, and fees are decimal strings (not integers) to preserve precision for fractional shares. data.updated is reserved for future use and currently always empty.
{
  "id": "d8e7f6a5-b4c3-2109-8765-4321fedcba09",
  "object": "event",
  "type": "trades.synced",
  "api_version": "2026-03-15",
  "created": 1741243200,
  "data": {
    "new": [
      {
        "id": "trade_xyz789",
        "object": "trade",
        "symbol": "VAS",
        "name": "Vanguard Australian Shares Index ETF",
        "type": "buy",
        "quantity": "12",
        "price": "98.45",
        "currency": "AUD",
        "total_amount": "1181.40",
        "fees": "4.95",
        "trade_date": "2026-03-05T10:30:00+11:00",
        "settlement_date": "2026-03-07T00:00:00+11:00",
        "account": "Brokerage Cash",
        "description": "Market buy — limit 98.50"
      }
    ],
    "updated": []
  },
  "metadata": {
    "sync_run_id": "run_abc123",
    "new_count": 1,
    "updated_count": 0,
    "chunk": 1,
    "total_chunks": 1
  }
}
Each trade in data.new contains:
FieldTypeDescription
idstringUnique trade identifier from the brokerage provider
objectstringAlways "trade"
symbolstringTicker symbol (e.g. "VAS", "AAPL")
namestring | nullSecurity name where available
typestring"buy", "sell", "dividend", "split", "transfer", "fee", "interest", or "other"
quantitystringNumber of units (decimal string, supports fractional shares)
pricestringPrice per unit in currency (decimal string)
currencystringISO 4217 currency code (e.g. "AUD", "USD")
total_amountstringTotal trade value in currency (decimal string)
feesstring | nullFees charged on this trade, in currency
trade_datestringISO 8601 timestamp when the trade executed
settlement_datestring | nullISO 8601 timestamp when the trade settled
accountstringHuman-readable account name
descriptionstring | nullProvider-supplied description or note
Trade amounts are decimal strings, not integers. This is different from the transaction payload (which uses smallest-unit integers) because brokerage quantities can be fractional — parsing as a Decimal/BigDecimal on your side is recommended.

Metadata fields

FieldTypeDescription
sync_run_idstring | nullUUID of the sync run that produced this delivery. null when the delivery was replayed outside of a sync run.
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, consecutive-failure counter resets to zero.
429Rate limited. Retried with exponential backoff.
5xxServer error. Retried with exponential backoff.
Other 4xxClient error. Not retried — counted as a single failed delivery.
Timeout (no response within 30s)Not retried — the body has almost certainly been received by the endpoint, so a retry would risk duplicate delivery. Counted as a single failed delivery.

Retry schedule

Each delivery makes up to 3 attempts in total. Sleeps before each retry use exponential backoff with a base of 3:
AttemptSleep before
1none (immediate)
21 second
33 seconds
The per-attempt request timeout is 30 seconds.

Auto-disable on failed deliveries

A single failed delivery cycle (all 3 attempts for the same payload exhausted, or a non-retried 4xx, or a timeout) increments the destination’s consecutiveFailures counter. Once the counter reaches 1, the destination is automatically disabled — disabledAt is set and no further deliveries are attempted until the destination is manually re-enabled from the dashboard. The successful delivery of any subsequent payload resets the counter to zero. Auto-disable is intentionally aggressive: webhook endpoints are integrator-controlled, and continuing to retry against a broken endpoint causes more harm than good.

Deduplication

Entity IDs (transaction IDs and trade IDs) that have been successfully delivered are tracked per destination. Each successful delivery upserts the row, refreshing deliveredAt — the 90-day retention window therefore runs from the last successful delivery of that entity, not the first. If an entity is still inside its window when the next sync runs, it will not appear in data.new. Transactions and trades are deduplicated independently — a trade ID that happens to match a transaction ID won’t collide. Your endpoint should still handle potential duplicates gracefully (e.g. using <event.type>:<object.id> as an idempotency key) in case of network-level retries.