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

# Service Tokens

> Create, rotate, and revoke long-lived API tokens for service-to-service integrations

## Overview

**Service tokens** are long-lived API tokens scoped to an account (all of its advertisers) or a single advertiser. Use them when the caller is not a human user — backend services, ETL jobs, agent runtimes, multi-tenant tools, partner integrations.

Each customer-owned service token also establishes a **workload identity** for
the software using that credential. `workloadUid` identifies the workload;
`credentialVersion` identifies the exact credential; and `adcpCallerUri` is the
stable caller identity used for AdCP governance delegation. Internal Scope3
system tokens do not receive buyer workload identities.

They are distinct from personal API keys, which belong to an individual user. Service tokens survive user offboarding, can be scoped down to a single advertiser with a specific role, and have explicit lifecycle controls (`expiresInDays`, `archive`).

| Personal API key                                  | Service token                                          |
| ------------------------------------------------- | ------------------------------------------------------ |
| Tied to a user account                            | Tied to an account or advertiser                       |
| Revoked when user leaves                          | Survives user changes                                  |
| One token per user                                | Many per account                                       |
| Best for: dashboards, scripts you run as yourself | Best for: server-to-server, agents, multi-tenant tools |

Both are bearer credentials — every request sends `Authorization: Bearer scope3_<token>`.

### Token format

A service token is a single opaque string with three parts joined by underscores:

```
scope3_<accessClientId>_<accessClientSecret>
```

* The `scope3_` prefix lets us recognize the token at the edge.
* `accessClientId` is a non-secret identifier for this credential version — safe to log, surface in admin UIs, and reference in audit trails.
* `accessClientSecret` is the credential — only shown once, at creation.

<Warning>
  The full token (`fullToken` field on the create response) is shown **exactly
  once**, on the response to `POST /service-tokens`. We store only a hash; we
  cannot show it to you again. If you lose it, revoke the token and create a
  new one.
</Warning>

## Prerequisites

<Steps>
  <Step title="An admin or read-write API key">
    You need an existing credential with permission to manage tokens — a
    user API key from
    [interchange.io/user-api-keys](https://interchange.io/user-api-keys),
    or another service token with `ADMIN` role.
  </Step>

  <Step title="Decide the scope">
    Pick `CUSTOMER` (acts across every advertiser) or `ADVERTISER` (limited
    to one advertiser). For advertiser-scoped tokens, you also choose a role
    (`ADMIN`, `READ_WRITE`, `READ`) and an expiry between 1 and 365 days.
  </Step>

  <Step title="A secret manager">
    Plan where you'll store `fullToken`. Service tokens act on your
    organization's behalf — keep them in Google Secret Manager, AWS Secrets
    Manager, HashiCorp Vault, or equivalent. Never commit them to git.
  </Step>
</Steps>

All examples below use:

```
BASE = https://api.interchange.io/api/v2
AUTH = Authorization: Bearer scope3_<your_token>
```

<Note>
  Service-token endpoints live under `/api/v2/service-tokens`, not under
  `/api/v2/buyer/...` or `/api/v2/storefront/...`. They are mounted on the v2
  shared router because the same token-management surface works for buyer
  and storefront accounts alike. Most other v2 endpoints sit under a
  buyer/storefront mount — service tokens are the exception.
</Note>

## Step 1: Create a service token

`POST /service-tokens` returns the new token, including the one-time-only
`fullToken` field. Capture it before doing anything else.

<CodeGroup>
  ```bash Account-scoped theme={null}
  curl -X POST "$BASE/service-tokens" \
    -H "Authorization: Bearer scope3_<your_token>" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Nightly ETL — Snowflake export",
      "description": "Pulls reporting data into the warehouse every 02:00 UTC",
      "expiresInDays": 365,
      "scope": "CUSTOMER"
    }'
  ```

  ```bash Advertiser-scoped theme={null}
  curl -X POST "$BASE/service-tokens" \
    -H "Authorization: Bearer scope3_<your_token>" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Acme — campaign automation",
      "scope": "ADVERTISER",
      "advertiserName": "Acme Co.",
      "role": "READ_WRITE",
      "expiresInDays": 90
    }'
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "12345",
  "customerId": "cust_abc",
  "advertiserId": null,
  "userId": null,
  "name": "Nightly ETL — Snowflake export",
  "description": "Pulls reporting data into the warehouse every 02:00 UTC",
  "accessClientId": "ak_01HXYZ...",
  "fullToken": "scope3_ak_01HXYZ..._sk_live_9c8d7e6f...",
  "expiresAt": "2027-04-26T00:00:00.000Z",
  "createdAt": "2026-04-26T16:30:00.000Z",
  "role": null,
  "createdByUserId": "98765",
  "workloadUid": "4f7d713a-94a5-4f64-9d55-13f99bb2dc42",
  "credentialVersion": 1,
  "replacesServiceTokenId": null,
  "adcpCallerUri": "https://api.interchange.io/api/v2/buyer/workloads/4f7d713a-94a5-4f64-9d55-13f99bb2dc42"
}
```

<Warning>
  Capture `fullToken` and store it in your secret manager **immediately**.
  After this response, only `accessClientId` is retrievable.
</Warning>

### Request body

| Field            | Type    | Required    | Notes                                                               |
| ---------------- | ------- | ----------- | ------------------------------------------------------------------- |
| `name`           | string  | no          | Human-readable label, max 255 chars                                 |
| `description`    | string  | no          | Free-form, max 1000 chars                                           |
| `expiresInDays`  | integer | conditional | 1–365. Required when `scope: ADVERTISER`                            |
| `scope`          | enum    | no          | `CUSTOMER` (default) or `ADVERTISER`                                |
| `advertiserName` | string  | conditional | Required when `scope: ADVERTISER`                                   |
| `role`           | enum    | conditional | `ADMIN`, `READ_WRITE`, or `READ`. Required when `scope: ADVERTISER` |

### Selecting an advertiser per request

Account-scoped (`CUSTOMER`) tokens and user API keys select the advertiser to
act on by sending the `x-scope3-seat-id` request header with the numeric
advertiser ID:

```bash theme={null}
curl "$BASE/campaigns" \
  -H "Authorization: Bearer scope3_<your_token>" \
  -H "x-scope3-seat-id: 88421"
```

<Note>
  The header name intentionally keeps its legacy `seat` spelling — it is a
  stable wire contract that predates the seat→advertiser rename. The value it
  carries is the advertiser ID. Advertiser-scoped tokens don't need it: they
  are scoped to one advertiser at creation, and a header naming a different
  advertiser is rejected with `403`.
</Note>

## Step 2: List or fetch existing tokens

Use the list endpoint to see what tokens exist for your account (or for a
specific advertiser). The list view never includes the secret — only
`accessClientId`, workload and credential identity, scope, expiry, and audit
metadata.

```bash theme={null}
curl "$BASE/service-tokens?take=50&skip=0" \
  -H "Authorization: Bearer scope3_<your_token>"
```

| Query param       | Type    | Default | Notes                  |
| ----------------- | ------- | ------- | ---------------------- |
| `advertiserId`    | string  | —       | Filter by advertiser   |
| `includeArchived` | boolean | `false` | Include revoked tokens |
| `take`            | integer | `50`    | Page size, max 100     |
| `skip`            | integer | `0`     | Offset                 |

To inspect a single token (for example, before extending its expiry):

```bash theme={null}
curl "$BASE/service-tokens/12345" \
  -H "Authorization: Bearer scope3_<your_token>"
```

You can also rename a token or **extend** its expiry through the update
endpoint. You cannot shorten the expiry through this endpoint — to
invalidate a token sooner, archive it (Step 4).

```bash theme={null}
curl -X PUT "$BASE/service-tokens/12345" \
  -H "Authorization: Bearer scope3_<your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Nightly ETL — Snowflake export (renamed)",
    "expiresInDays": 365
  }'
```

## Step 3: Rotate a token

Treat service tokens like any other long-lived credential and rotate on a
schedule. The pattern is "create new, deploy, revoke old" — never edit the
existing token in place.

Today, `POST /service-tokens` creates a new workload identity. Until the
lineage-preserving replacement flow is available for your account, the manual
rotation below therefore starts a new `workloadUid`; retain the old and new ids
in your deployment record so their activity can be audited separately.

<Steps>
  <Step title="Create the new token">
    `POST /service-tokens` with the same scope and role as the old one.
    Capture `fullToken` and `id` from the response.
  </Step>

  <Step title="Deploy to consumers">
    Update your secret manager and roll the new token out to every service
    that holds the old one.
  </Step>

  <Step title="Confirm rollout completed">
    Wait until you've confirmed all consumers have switched — for example,
    by checking deploy status, draining staging traffic, or watching error
    rates after a forced restart. Don't revoke the old token until you're
    sure nothing still depends on it.
  </Step>

  <Step title="Revoke the old token">
    `DELETE /service-tokens/<old_id>`. Any service still using the old
    token will start receiving `401 Unauthorized`, which is your final
    signal that rollout missed somewhere.
  </Step>
</Steps>

<Tip>
  Set `expiresInDays` to a value shorter than your rotation window so an
  unrotated token expires on its own — defense-in-depth for the case where
  a token is forgotten.
</Tip>

## Step 4: Revoke a token

Revocation is a soft delete — the token stops authenticating immediately,
but its audit row is preserved.

```bash theme={null}
curl -X DELETE "$BASE/service-tokens/12345" \
  -H "Authorization: Bearer scope3_<your_token>"
```

Returns `204 No Content`.

<Warning>
  **Rotate on suspected leak.** If a token might have been exposed
  (CI log, shared screen, leaked dependency), revoke it via
  `DELETE /service-tokens/:id` immediately, then issue a replacement.
  Do not wait for confirmation.
</Warning>

## Best practices

* **Use advertiser scope when possible.** An `ADVERTISER`-scoped token with `READ` role
  cannot move spend, even if exfiltrated. Default to least-privilege: pick
  the narrowest scope and lowest role that lets the integration do its job.
* **One token per workload.** If two services have different lifecycles or
  different blast radii, give them separate tokens. That way a leak or
  rotation only affects one workload at a time.
* **Treat as a credential.** Never commit tokens to git, never paste them
  into chat or email, never hard-code them in client-side bundles.
* **Multi-tenant tools have two patterns.**
  * *One token per managed advertiser.* Create an `ADVERTISER`-scoped token
    per managed advertiser with the minimum role each integration needs.
    Best when the advertisers have different administrators or you want
    clean per-tenant audit trails.
  * *One account-scoped token.* Use `scope: CUSTOMER` and rely on
    per-request advertiser selection. Best when a single team owns
    automation across every managed advertiser.
  * Either way, never share a single token across unrelated tenants — it
    conflates audit trails and forces a global revocation if any tenant is
    compromised.

## Endpoint reference

All paths are relative to `https://api.interchange.io/api/v2`.

| Method   | Path                  | Purpose                                           |
| -------- | --------------------- | ------------------------------------------------- |
| `GET`    | `/service-tokens`     | List service tokens                               |
| `POST`   | `/service-tokens`     | Create a service token (returns `fullToken` once) |
| `GET`    | `/service-tokens/:id` | Get a single service token                        |
| `PUT`    | `/service-tokens/:id` | Update name, description, or extend expiry        |
| `DELETE` | `/service-tokens/:id` | Archive (revoke) a service token                  |

## Related

<CardGroup cols={2}>
  <Card title="Authentication" href="/v2/authentication" icon="key">
    Bearer token format, header conventions, and how personal API keys
    differ from service tokens.
  </Card>

  <Card title="Errors" href="/v2/reference/errors" icon="triangle-exclamation">
    Status codes and JSON error shapes returned by the service token
    endpoints — including auth and validation failures.
  </Card>
</CardGroup>
