# Partner accounts

A partner is an organization that creates RouterPlus accounts for its own customers. Each
account is an organization of its own. The partner funds each account from its balance and earns
a share of the account's spend.

To become a partner, contact RouterPlus. RouterPlus sets your revenue share and turns the partner
API on for your organization.

```bash
curl https://demo-api.routerplus.com/v1/partner/accounts \
  -H "Authorization: Bearer $PARTNER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id": "cust-1042", "name": "Acme Corp", "credit_usd": 25}'
```

## Authentication

The partner API uses a management key of the partner organization:
`Authorization: Bearer tm_mk_...`. Create one with `POST /v1/management-keys` (see the
[management API](api-management.md)). Every route is served under `/v1/...` and `/api/v1/...`.

- A partner sees and changes only its own accounts. An account of another partner returns `404`.
- A management key of an organization that is not a partner returns `403 not_a_partner`.
- An account's own keys cannot call the partner API.

## Routes

| Route | Does |
|---|---|
| `POST /v1/partner/accounts` | Creates an account. Returns `201`. |
| `GET /v1/partner/accounts` | Lists every account. |
| `GET /v1/partner/accounts/{external_id}` | Reads one account. |
| `PATCH /v1/partner/accounts/{external_id}` | `{"disabled": true}` or `{"disabled": false}`. |
| `POST /v1/partner/accounts/{external_id}/credits` | `{"usd": n}`. Moves credit to the account. |
| `POST /v1/partner/accounts/{external_id}/keys` | `{"name"?}`. Creates another inference key. Returns `201`. |
| `GET /v1/partner/earnings?period=YYYY-MM` | Spend and revenue share per account for one month. |

Each account shows `balance_usd` and `month_to_date_spend_usd`, its spend since the start of the
current UTC month.

## Create an account

| Field | Type | Notes |
|---|---|---|
| `external_id` | string | Your own id for the customer. 1 to 128 letters, digits or `. _ : @ -`. Unique among your accounts. |
| `name` | string | The account's organization name, 1 to 100 characters. |
| `credit_usd` | number | Optional. Credit to move from your balance to the account, 0 to 10,000. |

The answer is `201`:

```json
{
  "data": {
    "external_id": "cust-1042",
    "org_id": "6c1f0d1e-2a35-4a3e-9a51-0f4c2d7b9e10",
    "name": "Acme Corp",
    "disabled": false,
    "balance_usd": 25,
    "month_to_date_spend_usd": 0,
    "workspace_id": "0b8e3f52-7c41-4d55-8f0a-3e2d9a6c1b77",
    "created_at": "2026-10-31T12:00:00.000Z"
  },
  "key": "tm_vk_...",
  "key_id": "…",
  "management_key": "tm_mk_...",
  "management_key_id": "…"
}
```

The account gets a default workspace and two keys:

- `key` is an inference key. Give it to your customer, or call models with it for them.
- `management_key` is a management key for the account's own organization. With it the account
  can create keys, set guardrails and read its activity.

The answer shows each raw key once. RouterPlus stores only a hash. Keep both keys safe.

An `external_id` that you already use returns `409`.

## Move credit to an account

Send `{"usd": 10}` to `POST /v1/partner/accounts/{external_id}/credits`.

- The amount is above 0 and at most 10,000.
- RouterPlus takes the amount from your balance and adds it to the account's balance in one
  step. The answer gives the account, and your new balance in `transfer.partner_balance_usd`.
- When your balance is less than the amount, the answer is `402 insufficient_balance` and no
  credit moves. The same rule applies to `credit_usd` on create: the account is not created.
- Credit on an account cannot move back to the partner.

## Create another key

`POST /v1/partner/accounts/{external_id}/keys` returns `201` with a new inference key in `key`,
shown once. The key is in the account's default workspace.

## Disable an account

`{"disabled": true}` turns off every key of the account: its inference keys and its management
keys. Requests with those keys get `401`. You cannot create keys for a disabled account.

`{"disabled": false}` turns on the keys that the disable turned off. A key that was already off
before the disable stays off. A key can take up to 5 seconds to work again.

## Earnings

`period` is a UTC month as `YYYY-MM`. Without it, the current month.

```json
{
  "period": "2026-10",
  "revenue_share_percent": 20,
  "data": [
    { "external_id": "cust-1042", "org_id": "…", "spend_usd": 412.5, "revenue_share_usd": 82.5 }
  ],
  "totals": { "spend_usd": 412.5, "revenue_share_usd": 82.5 }
}
```

- `spend_usd` is the account's usage at RouterPlus prices in the month. Usage on the account's
  own provider keys is not in it.
- `revenue_share_usd` is `spend_usd` times your revenue share, rounded to a millionth of a
  dollar.
- `totals` adds up all accounts.

If RouterPlus set a markup for you, each account also shows `markup_usd`, and the answer shows
`markup_percent` and `markup_note`. The markup is a reporting field: the amount you can charge
your customers on top. RouterPlus bills your accounts at its own prices.

## Audit

Every change writes an audit row on your organization and on the account. Read your rows with
`GET /v1/audit`.

## Errors

| Status | Type | When |
|---|---|---|
| `400` | `invalid_request` | A field is missing, unknown or out of range. |
| `401` | `unauthorized` | No management key, or the key is not valid. |
| `402` | `insufficient_balance` | Your balance is less than the credit to move. |
| `403` | `not_a_partner` | The key's organization is not a partner. |
| `403` | `partner_disabled` | RouterPlus turned off the partner API for your organization. |
| `404` | `not_found` | No account with this `external_id` among your accounts. |
| `409` | `conflict` | The `external_id` is already in use. |
| `409` | `account_disabled` | You asked for a key on a disabled account. |
