> ## Documentation Index
> Fetch the complete documentation index at: https://docs.redbark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# REST API

> Programmatic access to your bank accounts, balances, transactions, and connections

<Info>
  The REST API is in **beta**. Endpoints, request formats, and response shapes may change. Pin your integration to a specific API version once versioning is available.
</Info>

The Redbark REST API provides programmatic access to accounts, account details (rates and fees), balances, transactions, connections, and brokerage data. Use it to build integrations, dashboards, or workflows on top of your banking and investment data.

## Base URL

```
https://api.redbark.com
```

## Authentication

All API requests require a Bearer token. Create an API key in the [dashboard](https://app.redbark.com/settings) and include it in the `Authorization` header:

```
Authorization: Bearer YOUR_API_KEY
```

<Info>
  API access requires an active **Developer** or **Professional** plan. Trial subscriptions (`status: trialing`), legacy accounts within their 14-day grace period, and grandfathered accounts also retain API access. You can revoke keys from the dashboard at any time.
</Info>

<Tip>
  Want to connect an AI agent instead? See the [MCP Server](/api-reference/mcp) documentation.
</Tip>

<Tip>
  Want to test your parsing code before creating a trial account? See [Sample Responses](/api-reference/sample-responses) for a coherent set of example JSON payloads covering every endpoint.
</Tip>

## Rate limits

Limits are tiered by endpoint cost and applied per API key. Every non-first-party request also passes through a pre-auth gate before any DB lookup: **60 per minute**, bucketed by `(client IP, first 16 chars of the supplied bearer token)`. This gate applies to all requests, not just failed ones, so probes with different partial keys from the same IP each get their own bucket, while a single key+IP combination is throttled before any DB lookup.

| Tier  | Endpoints                                           | Limit       |
| ----- | --------------------------------------------------- | ----------- |
| Cheap | `/v1/connections`, `/v1/accounts`, `/v1/categories` | 60 / minute |
| Mid   | `/v1/balances`, `/v1/account-details`               | 30 / minute |
| Heavy | `/v1/transactions`, `/v1/holdings`, `/v1/trades`    | 30 / minute |

In addition, **heavy** endpoints have a per-key concurrency cap of **4 in-flight requests**. Calls that arrive while four heavy requests are already in progress on the same key return `429`. Issue heavy requests sequentially, or fan them out across multiple keys, to stay under the cap.

First-party sync clients identified by user agent (`redbark-sure-sync`, `redbark-actual-sync`) use a separate **300 / minute** pre-auth bucket keyed only by IP. This is not relevant for third-party integrations but explains why two distinct pre-auth limits exist.

Every response includes rate limit headers:

| Header                  | Description                                                                                                                                 |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed per window                                                                                                         |
| `X-RateLimit-Remaining` | Requests remaining in the current window                                                                                                    |
| `X-RateLimit-Reset`     | Epoch seconds when the current bucket resets. Emitted on every response so well-behaved clients can pace themselves before hitting `429`.   |
| `Retry-After`           | Seconds until the next request is allowed. Set on `429` responses, and on `503` responses when the per-connection upstream breaker is open. |

### Truncation and deprecation headers

Some endpoints set additional response headers to signal operational state:

| Header                                                                         | When                                                                                                            | Meaning                                                                                                                                                                                                   |
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-Redbark-Truncated: true`                                                    | `/v1/transactions` only, when the \~5,000-row per-request ceiling is reached before the result set is exhausted | The service stops accumulating rows at the ceiling. When set, `pagination.hasMore` is `true`; continue paginating with `offset + limit`. This header is never sent by `/v1/trades` or any other endpoint. |
| `Deprecation: true` + `Sunset: <date>` + `Link: <docs-url>; rel="deprecation"` | A request uses a parameter shape that has been removed                                                          | Set on `/v1/transactions` calls that omit `accountId`. That form was removed on 2026-06-30 and now returns `410 Gone`.                                                                                    |

Separately, `/v1/transactions` fetches each account with a per-request page cap of `ceil((offset + limit) / 1000) + 2` upstream pages. In an unusually dense date window this cap can stop paging before the window is exhausted **without** setting `X-Redbark-Truncated`, and the page can come back with `hasMore: false`. If you need every row in a very dense window, narrow the date range and page through smaller windows.

## Error format

All errors return a JSON envelope:

```json theme={null}
{
  "error": {
    "message": "Connection not found or does not belong to this user"
  }
}
```

A `400` validation error includes a `details` array. Date-validation errors raised by the service layer carry their own `message`; schema-layer validation failures use the generic `"Invalid request parameters"` message:

```json theme={null}
{
  "error": {
    "message": "Invalid date parameters",
    "details": [
      "from: Must be YYYY-MM-DD or RFC3339 with timezone (e.g. 2026-05-09T23:35:19Z or 2026-05-09T23:35:19+10:00)"
    ]
  }
}
```

```json theme={null}
{
  "error": {
    "message": "Invalid request parameters",
    "details": [
      "limit: Number must be greater than or equal to 1"
    ]
  }
}
```

| Field     | Type       | Description                                        |
| --------- | ---------- | -------------------------------------------------- |
| `message` | `string`   | Error description. Always present.                 |
| `code`    | `string`   | Machine-readable error code. Omitted when not set. |
| `details` | `string[]` | Validation details. Omitted when empty.            |

Stable `code` values currently emitted:

| Code                          | Status | When                                                                                                                                                                                                                                 |
| ----------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `invalid_params`              | `400`  | Service-layer input validation failed, e.g. `accountIds` is empty after trimming (`accountIds=,,`). Schema-shape failures (bad query/body) return the same `400` and `"Invalid request parameters"` message but carry **no** `code`. |
| `invalid_date_range`          | `400`  | `from` is after `to`                                                                                                                                                                                                                 |
| `from_too_old`                | `400`  | `from` is more than \~7 years ago                                                                                                                                                                                                    |
| `wrong_endpoint_for_category` | `400`  | Transactions requested for a brokerage connection (banking and documents connections are allowed), or trades/holdings requested for a non-brokerage connection                                                                       |
| `upstream_bad_request`        | `400`  | Banking provider rejected the request with a 4xx (date format, missing scope, etc.); the upstream detail is appended to `message`                                                                                                    |
| `body_too_large`              | `413`  | Request body exceeded the 16 KiB limit                                                                                                                                                                                               |
| `professional_required`       | `403`  | Brokerage endpoint called by a Developer-plan customer                                                                                                                                                                               |
| `insufficient_scope`          | `403`  | **MCP server only** (never emitted by the REST API): OAuth access token lacks the required scope (e.g. `mcp:read`)                                                                                                                   |
| `not_found`                   | `404`  | Unknown path or method-not-allowed on a known path                                                                                                                                                                                   |
| `connection_not_found`        | `404`  | Connection ID does not exist or does not belong to the caller                                                                                                                                                                        |
| `account_not_found`           | `404`  | Account ID does not exist or does not belong to the connection                                                                                                                                                                       |
| `client_disconnected`         | `499`  | **MCP server only** (the REST API passes no abort signal, so it cannot emit this): client aborted the request before the response was sent                                                                                           |
| `upstream_breaker_open`       | `503`  | Per-connection upstream circuit breaker is open; `Retry-After` indicates cooldown                                                                                                                                                    |

### HTTP status codes

| Status | Meaning                                                                                                                                                                          |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | Success                                                                                                                                                                          |
| `400`  | Invalid parameters                                                                                                                                                               |
| `401`  | Missing or invalid API key                                                                                                                                                       |
| `403`  | Plan does not include API access (`code: plan_upgrade_required`; requires the Developer or Professional plan), or `professional_required` for brokerage endpoints                |
| `404`  | Resource not found, or method not allowed on a known path (Hono collapses both to 404)                                                                                           |
| `405`  | MCP only: `GET` or `DELETE` on `/mcp`. The MCP transport is Streamable HTTP POST-only.                                                                                           |
| `413`  | Request body exceeded the 16 KiB limit (`code: body_too_large`)                                                                                                                  |
| `429`  | Rate limited (per-IP, per-key, or per-key concurrency cap)                                                                                                                       |
| `499`  | MCP server only: client disconnected before the response was sent (`code: client_disconnected`). The REST API passes no abort signal and does not emit this.                     |
| `500`  | Internal server error                                                                                                                                                            |
| `501`  | Endpoint not implemented for the connection's provider (e.g. balances on a provider that doesn't expose them)                                                                    |
| `503`  | Banking/brokerage provider temporarily unavailable. Retry later. Carries `Retry-After` when the per-connection upstream circuit breaker is open (`code: upstream_breaker_open`). |

## Pagination

List endpoints support `limit` and `offset` query parameters:

| Parameter | Type      | Default            | Description                                                                     |
| --------- | --------- | ------------------ | ------------------------------------------------------------------------------- |
| `limit`   | `integer` | Varies by endpoint | Maximum number of items to return. Passed as a query string (e.g. `?limit=50`). |
| `offset`  | `integer` | `0`                | Number of items to skip. Passed as a query string.                              |

Paginated responses include a `pagination` object:

```json theme={null}
{
  "data": [...],
  "pagination": {
    "total": 142,
    "limit": 50,
    "offset": 0,
    "hasMore": true
  }
}
```

| Field     | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                      |
| --------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `total`   | `number \| null` | Exact count of matching rows when known. `null` when the count is unknown — currently emitted by `/v1/trades` whenever `hasMore` is `true` (the count is only known once every page has been fetched), and may also be `null` on other paginated endpoints when an internal row ceiling is reached without exhausting the set. Use `hasMore` for next-page detection, not arithmetic on `total`. |
| `limit`   | `number`         | Effective page size (may be clamped from the requested value).                                                                                                                                                                                                                                                                                                                                   |
| `offset`  | `number`         | Offset that produced this page.                                                                                                                                                                                                                                                                                                                                                                  |
| `hasMore` | `boolean`        | `true` when at least one more page is available.                                                                                                                                                                                                                                                                                                                                                 |

## Caching

**Banking endpoints** may serve responses from a short-lived, per-connection server-side cache. Within the freshness window below, repeat requests can return the same data; once the window passes, the next request fetches fresh data from the banking provider.

| Endpoint              | Maximum data age |
| --------------------- | ---------------- |
| `/v1/account-details` | 15 minutes       |
| `/v1/balances`        | 30 minutes       |
| `/v1/transactions`    | 60 minutes       |

`/v1/accounts` and `/v1/connections` are served live from Redbark's own records (no provider fetch, no cache). Brokerage endpoints (`/v1/holdings`, `/v1/trades`) are not cached; every request fetches live from the brokerage provider. `/v1/categories` is a static list, not per-connection data.

A few behaviours to note:

* Cached data is purged immediately when a consent is revoked or a connection is deleted.
* You can opt out entirely in the dashboard under **Settings → Performance cache**. With the cache disabled, nothing is stored for your account and every request goes straight to the provider.
* Responses carry no cache-status header. Treat any banking response as potentially up to the maximum age above.
* During a banking-provider outage, requests may return `503` with a `Retry-After` header while the circuit breaker is open. See [Upstream provider errors](#upstream-provider-errors).

## Date validation

`from` and `to` accept either `YYYY-MM-DD` (a bare date) or RFC 3339 with an explicit timezone — `Z` for UTC or `±HH:MM` for an offset. Naive datetimes like `2026-05-09T23:35:19` (no `Z`, no offset) are rejected with a `400` carrying:

```
from: Must be YYYY-MM-DD or RFC3339 with timezone (e.g. 2026-05-09T23:35:19Z or 2026-05-09T23:35:19+10:00)
```

This is enforced locally rather than at the upstream provider so callers see a clean `400` instead of a misleading `503`.

## Upstream provider errors

When the banking provider rejects a request with a 4xx (validation issue we don't catch locally), the response is surfaced as a `400` with `code: upstream_bad_request` and `message: "Upstream rejected request: <provider detail>"`. This avoids collapsing caller-input errors into the generic `503 "provider temporarily unavailable"`.

A per-`(provider, userId, connectionId)` circuit breaker protects `/v1/transactions`. After **3 upstream failures within a 60-second rolling window**, the breaker opens for **60 seconds** and subsequent requests for that connection fail fast with a `503`, `code: upstream_breaker_open`, and a `Retry-After` header. The breaker is in-process per worker isolate, so one tripped isolate does not stop others from attempting the upstream.

## OpenAPI specification

A machine-readable OpenAPI 3.1 spec is available at:

```
https://api.redbark.com/openapi.json
```

Interactive documentation is available at [`/doc`](https://api.redbark.com/doc) via Swagger UI.

## Quick start

<Steps>
  <Step title="Get your API key">
    Go to [Settings > API Keys](https://app.redbark.com/settings) in the dashboard and create a new key. Copy the key. It is only shown once.
  </Step>

  <Step title="List your connections">
    ```bash theme={null}
    curl -H "Authorization: Bearer YOUR_API_KEY" \
      https://api.redbark.com/v1/connections
    ```
  </Step>

  <Step title="List accounts">
    ```bash theme={null}
    curl -H "Authorization: Bearer YOUR_API_KEY" \
      https://api.redbark.com/v1/accounts
    ```
  </Step>

  <Step title="Get balances">
    Use account IDs (UUIDs) from step 3:

    ```bash theme={null}
    curl -H "Authorization: Bearer YOUR_API_KEY" \
      "https://api.redbark.com/v1/balances?accountIds=a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890,b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901"
    ```
  </Step>

  <Step title="Fetch transactions">
    Use a `connectionId` (from step 2) and an `accountId` (from step 3). `accountId` is required: the connectionId-only form was removed on 2026-06-30 and now returns `410 account_id_required`.

    ```bash theme={null}
    curl -H "Authorization: Bearer YOUR_API_KEY" \
      "https://api.redbark.com/v1/transactions?connectionId=b7c4a1e2-8d3f-4e9a-9c5b-1f2a3e4d5c6b&accountId=a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890"
    ```
  </Step>
</Steps>

## Brokerage endpoints

The [Holdings](/api-reference/holdings) and [Trades](/api-reference/trades) endpoints provide access to investment portfolio data from brokerage connections. These endpoints require a **Professional plan** subscription.
