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

# Affiliate REST

> Authentication, request variants, and response shape for the Affiliate REST API

`GET /affiliate/v1/daily-activity` returns what your referred users did on a UTC
trade date, broken down by plan and then by product.

Authentication uses the same API key and secret as the rest of the Kraken REST API.
For key setup, see [REST Authentication](/exchange/guides/rest/authentication).
Requests sign each call and send `api-key`, `api-sign`, and `api-nonce` headers
(the signed path includes the query string). The key needs permission to query
referrals — see [API key permissions](/exchange/guides/rest/api-keys).

Schemas are in [Get Daily Activity](/api-reference/affiliate/get-daily-activity).

### Shared Python helper

```python theme={null}
import base64
import hashlib
import hmac
import json
import time
import urllib.error
import urllib.parse
import urllib.request

BASE_URL = "https://api.kraken.com"
API_KEY = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"  # base64-encoded

_last_nonce = 0


class KrakenApiError(Exception):
    """Non-2xx Kraken API response with a typed error body."""

    def __init__(self, status: int, body: dict):
        self.status = status
        self.body = body
        super().__init__(f"{status}: {body}")


def _nonce() -> str:
    global _last_nonce
    candidate = int(time.time() * 1000)
    _last_nonce = max(candidate, _last_nonce + 1)
    return str(_last_nonce)


def _flat_query(params: dict | None) -> str:
    if not params:
        return ""
    return urllib.parse.urlencode(params, doseq=True)


def _nested_query(params: dict | None) -> str:
    if not params:
        return ""
    pairs: list[tuple[str, str]] = []

    def add(name: str, value) -> None:
        if isinstance(value, dict):
            for child_name, child_value in value.items():
                add(f"{name}[{child_name}]", child_value)
        elif isinstance(value, bool):
            pairs.append((name, "true" if value else "false"))
        elif value is not None:
            pairs.append((name, str(value)))

    for key, value in params.items():
        add(key, value)
    return urllib.parse.urlencode(pairs)


def signed_rest_request(
    method: str,
    path: str,
    params: dict | None = None,
    body: dict | None = None,
    *,
    nested_query: bool = False,
):
    """Send a signed Kraken REST API request."""
    nonce = _nonce()
    query = _nested_query(params) if nested_query else _flat_query(params)
    signed_path = f"{path}?{query}" if query else path
    body_bytes = (
        json.dumps(body, separators=(",", ":")).encode() if body is not None else b""
    )
    digest = hashlib.sha256(nonce.encode() + body_bytes).digest()
    signature = base64.b64encode(
        hmac.new(
            base64.b64decode(API_SECRET),
            signed_path.encode() + digest,
            hashlib.sha512,
        ).digest()
    ).decode()

    headers = {
        "api-key": API_KEY,
        "api-sign": signature,
        "api-nonce": nonce,
    }
    if body is not None:
        headers["content-type"] = "application/json"

    request = urllib.request.Request(
        f"{BASE_URL}{signed_path}",
        data=body_bytes or None,
        headers=headers,
        method=method,
    )
    try:
        with urllib.request.urlopen(request) as response:
            return json.loads(response.read().decode())
    except urllib.error.HTTPError as exc:
        error_body = json.loads(exc.read().decode())
        raise KrakenApiError(exc.code, error_body) from exc


def funding_request(
    method: str,
    path: str,
    params: dict | None = None,
    body: dict | None = None,
):
    """Send a signed Funding API request."""
    return signed_rest_request(method, path, params, body, nested_query=True)
```

## Two ways to call it

Both variants are signed with your own API key. Dates are UTC calendar days
(`YYYY-MM-DD`) and must be **today or one of the previous 89 UTC days**. A future
date or a date older than that window is a `400`. An in-window day with no rows
is an empty page, not an error.

### By day

Give a trade date. You get every referred user who was active that day, is
still enrolled, and has not opted out, split by plan and then by product, plus
day-wide totals.

```python theme={null}
page = signed_rest_request(
    "GET",
    "/affiliate/v1/daily-activity",
    {"activity_date": "2026-09-16", "limit": 50},
)
print(page["activity_date"], page["active_users"], page["currency"])
for item in page["items"]:
    print(item["referee_reference"], item["masked_iiban"])
```

```
GET /affiliate/v1/daily-activity?activity_date=2026-09-16&limit=50
```

### By person

Give up to ten full IIBANs that participants have shared, plus an inclusive date
range. You get the same per-day entries for just those people — never a range
total. Day-wide fields (`totals`, `active_users`, `revision`, `estimated`,
`opted_out`) are omitted.

```python theme={null}
page = signed_rest_request(
    "GET",
    "/affiliate/v1/daily-activity",
    {
        "iiban": "AA45N84GQK2VUN7A,BB12N84GQK2VUN7B",
        "start_date": "2026-08-20",
        "end_date": "2026-09-16",
        "limit": 50,
    },
)
for item in page["items"]:
    print(item["activity_date"], item["referee_reference"])
```

```
GET /affiliate/v1/daily-activity?iiban=AA45N84GQK2VUN7A,BB12N84GQK2VUN7B&start_date=2026-08-20&end_date=2026-09-16&limit=50
```

Empty, duplicate, or malformed IIBAN tokens are a `400`. Validity is a format
check only, not account existence. Unattributed, unknown, unenrolled, and
opted-out valid IIBANs all return the same empty result. The full identifier is never
echoed.

Sending both variants on one request is a `400`.

## Identifying a referred user

Use `referee_reference` as the join key across days. It is stable for the same
referred user under your account, it is not a Kraken account id, and it is not
joinable across partners.

`masked_iiban` is the last four characters of the IIBAN. It is not unique. Do
not key on it. History responses do not echo which requested IIBAN produced the
row — correlate via `referee_reference` from day-variant results. When multiple
requested IIBANs share the same last four characters, `masked_iiban` cannot
disambiguate them. `enrolled_at` on each plan is truncated to the hour.

## Products

One key per product that had activity. The map is open-ended: ignore unknown
keys; an absent key means zero.

| Key                             | Meaning                                   | `volume` | `maker` / `taker` |
| ------------------------------- | ----------------------------------------- | -------- | ----------------- |
| `spot_trading`                  | Order-book spot                           | yes      | yes               |
| `ptl_trading`                   | Instant buy/sell                          | yes      | no                |
| `margin_trading`                | Margin trades                             | yes      | yes               |
| `margin_rollover`               | Financing charge on open margin positions | no       | no                |
| `futures_order_fill`            | Futures                                   | yes      | yes               |
| `tokenized_equity_spot_trading` | xStocks, order book                       | yes      | yes               |
| `tokenized_equity_ptl_trading`  | xStocks, instant                          | yes      | no                |

Options is not included. `maker` and `taker` appear only on execution products,
and only when that side had classified fills. When both are present they sum to
the classified portion; headline minus maker minus taker is unclassified. Absence
means unclassified, not zero. Instant buy/sell and financing charges have no
liquidity side.

`geo_blocked` is present only when that product had blocked-region activity.
Blocked events pay zero commission. Parent volume, fees, event\_count, and
commission cover payable activity only; `geo_blocked` is an additional breakdown
and is not included in those parent figures.

## Totals, opt-out, and payments

Day-wide `totals` include every referred user except those who have opted
out. Opted-out users do not appear as full rows; their remainder is
`opted_out`. Unenrolled users are omitted from `items`, including on old
dates, but stay in `totals` so the headline does not move. They are not
counted in `opted_out`:

* `{ "suppressed": {} }` when the opted-out group is too small to show, or when
  there are no opted-out participants
* `{ "summary": { "active_participants", "products" } }` otherwise

Payable for the day is `totals` plus `summary` when `summary` is present.

Daily figures will not match weekly payments exactly:

* Headlines are payable-only
* Geo-blocked activity is shown separately and pays zero
* This endpoint windows on trade date; payments window on insert time

## Staleness

A trade day is not sealed. Late events land on their trade date and rewrite that
partner's rows.

`revision` (day variant, first page) is when **your** figures for that UTC day
were last written. A later value on re-fetch means your figures moved. Another
partner's late event does not move this value. Re-check the trailing 35 days.

When `estimated` is `true` on the first page, amounts for that calendar day are
still an estimate, including the opted-out remainder.

## Pagination

`limit` defaults to 50 and rejects values over 200. When `next_cursor` is
present, pass it as `cursor` on the next request.

* Day variant cursor: the last item's `referee_reference`
* History variant cursor: `YYYY-MM-DD:` plus that day's `referee_reference`

`active_users`, `totals`, `revision`, `estimated`, and `opted_out` are
first-page only. An empty `totals` map on a later page is omission, not a zero
day.

## Errors

| HTTP  | When                                                                                                                                                        |
| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Both variants sent; a required history field missing; a date outside the 90-day window; `limit` out of range; empty, duplicate, or malformed `iiban` tokens |
| `401` | Missing or invalid API key / signature                                                                                                                      |
| `403` | Key lacks permission to query referrals, or the caller is not the main referrer on a live KOL plan (`ReferrerNotWhitelisted`)                               |

Successful bodies are the object itself. Errors are a non-2xx status with a typed
error body. The helper raises `KrakenApiError` with the parsed body on failure.
Throttling is covered in [Rate limits](/exchange/guides/affiliate/ratelimits).
