## Overview

Get familliar with Talli API fundamentals, including how the API is organised, how authentication works, and how we handle errors, pagination, rate limiting and versioning.

Talli API is built on **REST** principles, lets you programmatically manage **Distributions to beneficiaries** - this includes creating distributions, sending payout instructions, querying balances and bank accounts and pulling reports

It is a machine-to-machine (M2M) API. Your app calls Talli directly over HTTPS with no browser or end-user in the loop.

### Conventions

- **Transport**: HTTPS only, Plain HTTP requests are rejected.
- **Encoding**: All requests and responses use 'UTF-8'.
- **Content type**: 'application/json' for request and response bodies, unless an endpoint explicitly accepts or returns another type (e.g. report endponts accept 'text/csv' to stream a CSV file)
- **Timestamps**: ISO 8601 in UTC
- **ID's**: Resource identifiers are UUID strings
- **Monetary Format**: Amounts are returned as decimal numbers in the currency's major unit (e.g '12.50' for USD $12.50), as of this moment, only USD is supported


### Base URL

| Environment | Base URL |
|  --- | --- |
| Production | `https://api.talli.ai` |
| Sandbox | Provided by your Talli representative |


All requests are made against the base URL plus the versioned-prefixed path of the API surface that you're using.

### API surfaces

Talli exposes a single public API surface for customer-facing integrations:

| Surface | Base path |
|  --- | --- |
| **Distribution Manager (v3)** | `/v3/distribution-manager/` |


## Authentication

Talli API uses the **OAuth 2.0 Client Credentials Flow** for authentication. This process involves exchanging your Client ID and Client Secret for a temporary bearer access token. Authentication is identical across all Talli API surfaces

### Credentials

Your Talli representative will provide you with the following:

| Credential | Description | Example (for illustration only) |
|  --- | --- | --- |
| `Token URL` | The endpoint for requesting access tokens. | `https://[your-tenant].us.auth0.com/oauth/token` |
| `Client ID` | Your application's unique public identifier. | `aBcDeFgHiJkLmNoPqRsTuVwXyZ` |
| `Client Secret` | Your application's secret key. **Treat this like a password.** | `123-very-secret-key-456` |
| `Audience` | The unique identifier for the Talli API. | `https://api.talli.ai` |


Keep secrets out of source control. Store them in a dedicated secret management service (e.x. AWS Secrets Manager, Vault, or Google Secret Manager) and inject them into your application at runtime.

You can create a second credential, rename one, rotate its secret or delete it yourself: see [Credentials](/docs/get-started/credentials).

### Requesting an access token

To get your token, make a `POST` request to the `Token URL`.

**Example:**

cURL
```bash
curl --request POST \
  --url https://<your-tenant>.us.auth0.com/oauth/token \
  --header 'content-type: application/json' \
  --data '{
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "audience": "https://api.talli.ai",
    "grant_type": "client_credentials"
  }'
```

Python
```python
import requests

response = requests.post(
    "https://<your-tenant>.us.auth0.com/oauth/token",
    json={
        "client_id": "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET",
        "audience": "https://api.talli.ai",
        "grant_type": "client_credentials",
    },
    timeout=10,
)
response.raise_for_status()
token = response.json()["access_token"]
```

Node
```javascript
const response = await fetch(
  "https://<your-tenant>.us.auth0.com/oauth/token",
  {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      client_id: "YOUR_CLIENT_ID",
      client_secret: "YOUR_CLIENT_SECRET",
      audience: "https://api.talli.ai",
      grant_type: "client_credentials",
    }),
  }
);
const { access_token: token } = await response.json();
```

**Response:**

If your credentials are correct, you will receive a JSON response containing your access token.

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIs...",
  "expires_in": 86400,
  "token_type": "Bearer"
}
```

**Important: Handling Your Access Token**

* **Store it securely:** The `access_token` is the key to accessing the API. Store it in a secure but temporary location (e.g., in-memory cache).
* **It's a Bearer Token:** You will use this token in the `Authorization` header of all subsequent API requests.
* **It Expires:** The `expires_in` field indicates the token's lifetime in seconds (86400 seconds = 24 hours). Your application should reuse this token until it is close to expiring, at which point you should request a new one. **Please do not request a new token for every API call.**


## Error Handling

Talli API uses standard HTTP response codes to indicate the success or failure of an API request.

**HTTP Status code summary**

| Code | Meaning |
|  --- | --- |
| `200 OK` | Request succeeded. |
| `201 Created` | A 'POST' that creates the resource. The response body contains the created resource |
| `202 Accepted` | The request was validated and queued for asynchronous processing. |
| `204 No Content` | A successful write that returns no body, typically a delete or an update without a response payload. |
| `400 Bad Request` | The request is malformed, fails validation, or violates a business rule (e.g. invalid paging, missing required field, or a payout instruction status that does not allow the operation) |
| `401 Unauthorized` | Your access token is missing, expired, or wasn't issued by Talli. Re-authenticate at the token endpoint. |
| `403 Forbidden` | Token is valid but lacks the permission required for this endpoint, or a business rule forbids this caller the operation, for example `NOT_FUNDING_REQUEST_ADDRESSEE`. |
| `404 Not Found` | The resource does not exist, **or** it exists but belongs to a different organization. |
| `409 Conflict` | Request conflicts with the current state of your account (e.g. the distribution's bank account does not hold enough unallocated funds, `INSUFFICIENT_DISTRIBUTION_BALANCE`). |
| `429 Too Many Requests` | Rate limit exceeded - see [Rate Limits](#rate-limits). |
| `500 Internal Server Error` | Something went wrong on our side. The response body includes a `requestId` - share it with support and we can trace the request. |
| `502 Bad Gateway` | A service that Talli depends on failed while processing the request. Retry the request. |


Error responses are returned as ProblemDetails objects, the format defined by [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807). Every error body includes type, title, status, detail, requestId and traceId. Most error bodies also include instance. Send the requestId to support when you report an error.

```json
{
    "type": "Not Found",
    "title": "Not Found",
    "status": 404,
    "detail": "Payout for payout instruction id 1a2b3c4d-e5f6-7890-1234-567890abcdef was not found.",
    "instance": "GET /v3/distribution-manager/payout-instructions/1a2b3c4d-e5f6-7890-1234-567890abcdef",
    "requestId": "0HN7Q9V2D2L0J:00000004",
    "traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}
```

| Field | Description |
|  --- | --- |
| `type` | Stable identifier for the error class - a short label such as `Not Found` on a validated request, or an RFC 9110 section link such as `https://tools.ietf.org/html/rfc9110#section-15.6.1` on an unexpected error. |
| `title` | Short, human-readable summary. |
| `status` | HTTP status code. |
| `detail` | Readable explanation of *this* occurrence. |
| `instance` | The method and path of the request that failed. |
| `errorCode` | Stable, machine-readable code for the error class, e.g. `INTERNAL_ERROR`, `PAYLOAD_TOO_LARGE`, `INSUFFICIENT_DISTRIBUTION_BALANCE`, `EMAIL_SEND_QUOTA_EXCEEDED`. Present on unexpected errors and on business-rule `403` and `409` responses; prefer it over `type` when branching in code. |
| `requestId` | Correlates to a single request in Talli's logs. **Include in support tickets.** |
| `traceId` | Distributed-trace identifier across our services. |


A `500` never carries the underlying error text - the body is always the fixed shape below, and the
`requestId` is what lets support find the failing request:

```json
{
    "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
    "title": "An error occurred",
    "status": 500,
    "detail": "An unexpected error occurred. Quote requestId when contacting Talli support.",
    "errorCode": "INTERNAL_ERROR",
    "requestId": "0HN7Q9V2D2L0J:00000004",
    "traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}
```

**Validation errors**

For 400 validation failures from invalid request bodies/query strings, the response follows RFC 7807's validation extension and includes per-field error messages under errors:

```json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
    "bankAccountId": [
    "Bank account is still Pending — wait for activation before promoting the draft."
    ],
    "description": [
    "The 'description' field must not exceed 512 characters."
    ]
},
"requestId": "0HN7Q9V2D2L0J:00000004"
}
```

**Reporting errors to support**

Capture and include the requestId and traceId when contacting api-support@talli.ai, it lets us jump directly to the failing request in our logs.

## Pagination

Talli API list methods let you retrieve collections of resources, e.g. such as listing distributions, payout instructions or bank accounts. These methods share a common structure, taking 'pageNumber' and 'pageSize' as query parameters.

### Request parameters

| Parameter | Type | Default | Description |
|  --- | --- | --- | --- |
| `pageNumber` | integer | `1` | 1-indexed page to return. |
| `pageSize` | integer | `20` (`50` on `GET /users` and `GET /invitations`) | Items per page. See [Maximum page size](#maximum-page-size) for the limit of each endpoint. |


```bash
  curl --request GET \
    --url 'https://api.talli.ai/v3/distribution-manager/distributions?pageNumber=1&pageSize=20' \
    --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

Most list methods use the defaults above when you leave out a parameter. `GET /payout-instructions` and `GET /reports/distributions/{distributionId}/basic` have no defaults, so you must send both parameters. We recommend that you always send both.
Some list methods additionally accept sortBy and sortDirection (Asc | Desc); valid sortBy values are per-endpoint enums documented in the API Reference.

### Maximum page size

All paths are relative to `https://api.talli.ai/v3/distribution-manager`.

| Endpoint | Maximum `pageSize` |
|  --- | --- |
| `GET /distributions` | `100` |
| `GET /distributions/{id}/transactions` | `100` |
| `GET /distributions/{id}/payout-instructions/upload-status/{batchId}/rows` | `100` |
| `GET /payout-instructions` | `100` |
| `GET /bank-accounts` | `100` |
| `GET /bank-accounts/{id}/transactions` | `100` |
| `GET /bank-accounts/{id}/withdrawal-accounts` | `100` |
| `GET /funds-holders` | `100` |
| `GET /templates` | `100` |
| `GET /users` | `100` |
| `GET /invitations` | `100` |
| `GET /reports/distributions/{distributionId}/basic` | No fixed maximum |


If `pageSize` is less than `1` or more than the maximum, the API returns `400 Bad Request` with `type` set to `Invalid Paging Configuration`. The API does not reduce an out-of-range value to the maximum.

### Response structure

| Field | Description |
|  --- | --- |
| `pageNumber` | Derived from the request. |
| `pageSize` | Derived from the request. |
| `totalPages` | Total pages available for the current filter set. |
| `totalItems` | Total items across all pages. |
| `data` | The page contents, an array of the resource type for that endpoint. |


```json
{
    "pageNumber": 1,
    "pageSize": 20,
    "totalPages": 5,
    "totalItems": 87,
    "data": [
      {
        "distribution": {
          "id": "1a2b3c4d-e5f6-7890-1234-567890abcdef",
          "name": "Q2 Field Marketing Distribution",
          "externalId": "q2-field-marketing"
        },
        "details": null
      }
    ]
  }
```

The example shows a page of `GET /distributions`, shortened to a few fields. See the API Reference for the full shape of each resource.

## Rate Limits

The Talli API enforces rate limits to protect the platform from accidental overload and abuse. Limits are applied **per organization**, so other customer's traffic never affects your budget. Rate limits are scoped **per endpoint group** — different endpoints can have different limits.

### Reading your usage

Every response from a rate-limited endpoint includes these headers, so you can monitor your usage proactively without waiting to be throttled:

| Header | Description |
|  --- | --- |
| `X-RateLimit-Limit` | The maximum number of requests allowed in the current window for this endpoint. |
| `X-RateLimit-Remaining` | The number of requests still available in the current window. |
| `X-RateLimit-Reset` | The time the window resets, as a Unix timestamp (seconds since the epoch, UTC). |


We recommend reading `X-RateLimit-Remaining` and backing off as it approaches `0`, rather than waiting for a `429`.

> The exact limit for any endpoint is always reported in its `X-RateLimit-Limit` header — treat that
header as the source of truth rather than hard-coding the values above, as limits may be tuned over time.


### Exceeding the limit

If you exceed a limit, the API responds with **`429 Too Many Requests`**. The response includes:

- A **`Retry-After`** header - the number of seconds to wait before retrying.
- The same `X-RateLimit-` headers (with `X-RateLimit-Remaining: 0`).
- A [Problem Details](https://www.rfc-editor.org/rfc/rfc9457) body, consistent with the API's other error responses:


```json
{
  "type": "https://tools.ietf.org/html/rfc6585#section-4",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Please retry later.",
  "instance": "POST /v3/distribution-manager/payout-instructions/{id}/:initiate",
  "requestId": "0HN…"
}
```

### Endpoint limits

The table below lists the **current** limits. All limits are per organization and use a **sliding
window** (e.g. "60 / minute" means no more than 60 requests in any rolling 60-second period). In the
routes below, `…` stands for `/v3/distribution-manager` Url prefix.

| Endpoint(s) | Limit |
|  --- | --- |
| **Default** — every public API endpoint not listed below | **60 / minute** |
| `POST …/payout-instructions/{id}/:initiate` and `POST …/distributions/{distributionId}/payout-instructions/:direct-ach` (shared limit) | **20 / minute** |
| `POST …/payout-instructions/{id}/:reinitiate` | **20 / minute** |
| `POST …/payout-instructions/{id}/:send-reminder` | **20 / minute** |
| `POST …/distributions/{distributionId}/payout-instructions/:initiate` and `POST …/distributions/{distributionId}/payout-instructions/:send-reminders` (shared limit) | **5 / minute** |
| `GET …/distributions/{id}/payout-instructions/upload-status/{batchId}` and `GET …/distributions/{id}/payout-instructions/upload-status/{batchId}/rows` (shared limit) | **120 / minute** |
| `POST …/credentials`, `PATCH …/credentials/{clientId}`, `POST …/credentials/{clientId}/:rotate-secret` and `DELETE …/credentials/{clientId}` (shared limit) | **10 / minute** |
| `POST …/templates/:send-preview` | **50 / day** |


**Singular live-send endpoints** (`:initiate` / `:reinitiate` / `:send-reminder`) carry the tighter
**20 / minute** limit because each call triggers an outbound email to a beneficiary. The
`:direct-ach` endpoint counts against the same **20 / minute** limit as singular `:initiate`. The
`templates/:send-preview` endpoint, which sends a live test email, is capped at **50 / day**.

A "shared limit" row means that the endpoints in that row use one limit together. For example,
3 bulk `:initiate` calls and 2 bulk `:send-reminders` calls in one minute use the full **5 / minute**.

The sandbox `simulate` endpoints have their own limits. See [Sandbox testing](/docs/get-started/sandbox-testing).

For high-volume distribution, use the **bulk** endpoints (`…/payout-instructions/:initiate` and
`…/:send-reminders` at the distribution level) rather than scripting many singular calls.

## Versioning

Talli versions the API in the URL path segment. Versions are major-version-only, meaning there are no minor versions like v3.1.

**Currently** supported versions:

| Version | Surface | Status |
|  --- | --- | --- |
| `v3` | Distribution Manager | **Current** and recommended. |


### Backwards-compatible changes

We may make the following changes within a major version without advance notice:

- Adding new endpoints.
- Adding new optional request fields.
- Adding new fields to response bodies.
- Adding new enum values.
- Adding new HTTP headers.
- Adding new optional query parameters.



> Tip: Build your integration to ignore fields it does not recognize.


### Breaking changes

Breaking changes require a new major version. We treat the following as **breaking**:

- Removing or renaming an endpoint, field, or enum value.
- Changing the type of an existing field.
- Changing authentication or authorization requirements.


> You can check most recent changes done to the API in the [Changelog](/docs/changelog/changelog)