# API overview

A REST API over every object in your program, with an interactive reference generated from the API itself.

The AffiliateRail API is a REST API at `https://api.affiliaterail.com`. You send JSON and get JSON back, and the resource URLs are predictable. It covers every object in your program, from partners and links to commissions, payouts and webhook endpoints.

Anything the dashboard shows you, the API can read. It can also do the daily work of running a program: adding partners, recording sales and approving commissions.

An **OpenAPI document** is a file that describes an API in a format tools can read. The API publishes its own, and the [interactive reference](/reference) is generated from it. So the reference can't drift from the running product. Machines can read the same document at [/v1/openapi.json](https://api.affiliaterail.com/v1/openapi.json). Models can read it as plain text at [/llms.txt](https://api.affiliaterail.com/llms.txt) and [/llms-full.txt](https://api.affiliaterail.com/llms-full.txt).

## The contract in one screen

| Concern | Rule |
| --- | --- |
| Auth | `Authorization: Bearer rail_live_…`. Keys are minted under **Settings**, **API keys** and carry [scopes](/getting-started/authentication). `rail_test_…` keys stamp `X-Rail-Livemode: false` but read and write the same data as a live key; see [authentication](/getting-started/authentication). |
| Lists | `{ "data": [...], "has_more": true, "total_count": 42 }`, flat, every time. `limit` 1 to 100 (default 25); `starting_after` and `ending_before` take an object id. `program_id` is required on every list except `/v1/programs`. |
| Expand | `expand[]=partner&expand[]=sale` (or `expand=partner,sale`) inlines related objects. The same relation name means the same object everywhere; an unknown value is a 400 naming the allowed ones. `include[]=stats` adds counters where they exist. |
| Money | Integer minor units in `*_minor` fields, always with a `currency` in hand. Rates and deltas are basis points. Never a float, never a string; [a test walks every endpoint to keep it that way](/api/money). |
| Time | ISO 8601, UTC. |
| Ids | Prefixed: `prg_`, `part_`, `cus_`, `sale_`, `com_`, `pyt_`, `flw_`, `whe_`. Sample rows are `sample_…`. |
| Idempotency | An `Idempotency-Key` header on any POST replays the first response for 24 hours; the same key with a different request is a 422, and the same key while the first request is still running is a 409. See [rate limits and errors](/api/rate-limits). |
| Rate limit | 25 requests per second per key, with `X-RateLimit-*` headers on every response. |
| Errors | `{ "error": { "type", "code", "message", "param" } }`. See [the error catalogue](/api/rate-limits#errors). |
| Versioning | Every response carries `X-Rail-Version`. Additive changes never bump it. |
| Tenancy | An id from another organization is a 404, never a 403, so ids can't be probed. |

Clicks and referrals are readable on purpose. A partner who can rebuild the path from visit to lead to sale can audit their own tracking. Tax forms show their status only. The form itself is the partner's tax identity, so it never travels over an API key.

## A first call

List your partners, with each one's group inlined by `expand`:

<Tabs items={["cURL", "Node", "Python"]}>
<Tab value="cURL">

```bash
curl "https://api.affiliaterail.com/v1/partners?program_id=prg_...&limit=5&expand[]=group" \
  -H "Authorization: Bearer rail_live_..."
```

</Tab>
<Tab value="Node">

```js
const res = await fetch(
  "https://api.affiliaterail.com/v1/partners?program_id=prg_...&limit=5&expand[]=group",
  { headers: { Authorization: `Bearer ${process.env.RAIL_API_KEY}` } },
);
const { data, has_more, total_count } = await res.json();
```

</Tab>
<Tab value="Python">

```python
import os, requests

res = requests.get(
    "https://api.affiliaterail.com/v1/partners",
    params={"program_id": "prg_...", "limit": 5, "expand[]": "group"},
    headers={"Authorization": f"Bearer {os.environ['RAIL_API_KEY']}"},
)
body = res.json()  # {"data": [...], "has_more": bool, "total_count": int}
```

</Tab>
</Tabs>

## Creating a partner

To write, send a JSON body. Add an `Idempotency-Key` header so a retry can't create the partner twice. You get the created object back:

<Tabs items={["cURL", "Node", "Python"]}>
<Tab value="cURL">

```bash
curl https://api.affiliaterail.com/v1/partners \
  -H "Authorization: Bearer rail_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signup-8412" \
  -d '{"program_id": "prg_...", "email": "alice@example.com", "status": "active"}'
```

</Tab>
<Tab value="Node">

```js
const res = await fetch("https://api.affiliaterail.com/v1/partners", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RAIL_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "signup-8412",
  },
  body: JSON.stringify({ program_id: "prg_...", email: "alice@example.com", status: "active" }),
});
const partner = await res.json(); // { id: "part_...", ... }
```

</Tab>
<Tab value="Python">

```python
res = requests.post(
    "https://api.affiliaterail.com/v1/partners",
    headers={
        "Authorization": f"Bearer {os.environ['RAIL_API_KEY']}",
        "Idempotency-Key": "signup-8412",
    },
    json={"program_id": "prg_...", "email": "alice@example.com", "status": "active"},
)
partner = res.json()  # {"id": "part_...", ...}
```

</Tab>
</Tabs>

## Going deeper

- [The interactive reference](/reference): every endpoint, parameter and schema, with a try-it console.
- [Money, reports and write ergonomics](/api/money): the rules every amount follows, and the answers writes give you.
- [Rate limits, errors and versioning](/api/rate-limits)
- [Webhooks](/api/webhooks): signed delivery for every event in your program.
- [Server-side attribution](/api/attribution): three lines that attribute a sale without Stripe Checkout.
- [Typed clients and LLM access](/api/clients)
