# API tokens and connected apps (/api/tokens)

A script or an AI tool reaches a store in one of two ways: with an API token, or as an app or agent a person
approved. The choice decides whether a delete or a refund runs at once or waits for a person.

| | API token | Connected app or agent |
| --- | --- | --- |
| Examples | A script, an ERP sync, CI, `poltura` in a pipeline | Claude, ChatGPT, Cursor, VS Code, Codex; Poltura's own assistant |
| Created by | A person, in Poltura ID or with `poltura token` | A person approving a sign-in on the consent page |
| Secret | A `pltu_`, `plto_` or `pltd_` token, shown once | None you handle; the client keeps a refreshing OAuth session |
| Writes | Directly through the store's admin API **and** through actions on `/mcp` | Only through actions: `run_action`, `run_destructive_action` and the typed action tools |
| Deletes and refunds | Run at once on the admin API | Always wait for a person to approve them |
| Bounded by | Its permissions, stores, expiry, IP filter and points budget | The same, plus approval, the per-store switch and the daily caps |

A `poltura login` sign-in belongs on the token side: the CLI is Poltura's own client and acts as the person who
signed in, like a token. See [the CLI](/apps/cli#sign-in-or-use-a-token).

Give a token only the permissions its job needs, and use an OAuth connection when a person should approve deletes
and refunds. How approval works is on [How agents act on your store](/ai/agents#approval).

## Kinds of token [#kinds-of-token]

| Prefix | Owner | Use |
| --- | --- | --- |
| `pltu_` | You | Your own scripts. Acts as you: it never holds more than your role holds. A personal token reaches one organization. |
| `plto_` | An organization | Server-to-server integrations that should outlive any one person. Created by someone with `access.tokens.manage`. |
| `pltd_` | A developer organization | Publishing app releases from CI. See [the CLI](/apps/cli#sign-in-or-use-a-token). |

A token is 51 characters; do not assume a fixed length, since the documented maximum is 255. Its last six
characters are a checksum, so a mistyped or truncated token is recognised without a network call.

## Create a token [#create-a-token]

Open Poltura ID, **Account, API tokens** for a personal token, or the admin, **Settings, Organization, API and
access**, for an organization token.

Choose its name, permissions, stores, IP filter and expiry (see the table below).

Copy the secret. It is shown once.


Or create it on the command line:

```bash
poltura token create --name ERP --permissions orders.read,products.write --stores all --expires 90d
```

| Choice | Meaning |
| --- | --- |
| Name | What the token is for. |
| Permissions | Store permissions, such as `orders.read`, and organization permissions with `--org-permissions`. |
| Stores | `all` (including stores added later, with `--exclude <store id>` to leave some out) or a list of store ids. |
| IP filter | Allowed addresses or CIDR ranges, or none. A call from anywhere else answers `ip_not_allowed`. |
| Expiry | `7d`, `30d` (the default), `90d`, `1y`, a date, or `never`, which prints a warning. |

The secret is printed once, alone on standard output, so `TOKEN=$(poltura token create …)` works. Creating a token,
widening it, enabling it again and rolling it need a fresh second factor; the CLI asks for your authenticator code
or takes `--otp <code>`. Managing tokens takes your Poltura ID session (`--access-token` and `--id-token`, or
`POLTURA_ACCESS_TOKEN` and `POLTURA_ID_TOKEN`), never another token, so a token can never mint tokens.

Some permissions can never be given to a token or an app: managing the team, access itself, buying plans or domains, paying
invoices, and anything that needs a second factor to use. Asking for one answers `not_delegable`; do it in the admin.

## Use a token [#use-a-token]

```bash
curl https://api.poltura.com/v1/tokens/verify -H "Authorization: Bearer $POLTURA_TOKEN"
```

`verify` answers the token's id, status, expiry, owner and stores, and the IP filter does not apply to it. The same
check is `poltura token verify`. Send the token as `Authorization: Bearer …` to:

- the store's admin API, `https://api.poltura.com/v1/stores/<store id>/admin/…`, where each route needs the
  permission the token holds for it;
- `https://api.poltura.com/mcp`, as described in [Add the MCP server](/ai/install) and [Connect an AI tool](/ai/connect#use-an-api-token).

A token limited to one store needs no store id on `/mcp`; a token for several takes `store_id`.

## Change, roll and revoke [#change-roll-and-revoke]

| Command | Does |
| --- | --- |
| `poltura token list` | Lists tokens and when each was last used. |
| `poltura token edit <id>` | Changes the name, policy, IP filter or expiry. The secret stays. |
| `poltura token roll <id> --grace 2` | Issues a new secret. The old one keeps working for the grace period, 0 to 24 hours. After a leak, roll with no grace: every old secret stops at once. |
| `poltura token disable <id>`, `enable <id>` | Stops a token until it is enabled again. |
| `poltura token delete <id>` | Revokes it for good. |

A token reported as exposed is refused immediately and its owner is emailed. The owner is also emailed seven days
before a token expires, unless it was made to last a week or less. A personal token stops working when its owner
leaves the organization; an organization token does not.

## Limits [#limits]

Every call through a token costs points from a budget that refills steadily, set by the store's plan, and an
`/mcp` connection also has a session budget and daily caps for money, deletes and customer messages. The tables are on
[How agents act on your store](/ai/agents#rate-limits). Two things differ for tokens:

- The daily caps count only actions that connected agents run. A token's direct calls to the admin API are bounded
  by its points budget, not by the caps.
- Every response says what the call cost and what is left in the `X-Poltura-RateLimit-*` headers. A call over budget
  answers `429 rate_limited` with `Retry-After`, and a request that would cost more than 100 points answers
  `400 request_too_costly`. Neither reached the store, so a retry after the wait is safe. Both are explained in
  [Access errors](/api/errors).
