# Authentication & Tokens

Every request to `/payments-api/v1` authenticates with a **seller bearer token**. Tokens are scoped to a single seller account and grant full API access — treat them like passwords.

## The auth header

Include the token in every request as an HTTP Bearer token:

```http
Authorization: Bearer <your_token>
```

Anything else — missing header, malformed value, or an unrecognised token — returns `401 Unauthorized`.

## How to get a token

You obtain your token yourself — there is no need to wait on the Fynex team.

**From the dashboard (easiest):**

1. Log in to the dashboard (`https://dashboard.fynex.ai`) with your username and password.
2. Select your seller account.
3. Open the **Integration** page. It shows the API token masked, with a reveal (eye) toggle and a copy button.
4. Click the reveal icon, then copy the token. This value is exactly what you pass as `Authorization: Bearer <token>`.

If you don't have a dashboard login yet, sign up self-service (see
**Programmatically** below).

**Programmatically:** `POST /api/v1/onboarding/start` (sign-up; sets the session cookie itself) — or, for an existing account, `POST /api/v1/login/dashboard` (which sets the
session cookie) → the `createSellerAccount` GraphQL mutation, which returns the new seller
account's `authorizationToken`. The first user of a new organization is automatically granted the
`selleraccounts:create` / `selleraccounts:update` permissions this requires. See the
[Account setup & onboarding](https://docs.fynex.ai/payments-api/v2/docs#tag/onboarding) guide for the full walkthrough.

> [!IMPORTANT]
> A new seller account is created in `Demo` mode and **activated automatically**, so your token
> works immediately for **sandbox testing** — you can take test payments straight away (see the
> [Quickstart](https://docs.fynex.ai/payments-api/v2/docs#tag/quickstart)). **Going live** (real-money processing) is separate: it requires
> KYB approval and a Fynex-assigned live acquirer terminal. If a sandbox call ever returns
> `403 seller account is not active`, the token is still valid — the account just isn't active
> yet; contact Fynex with your seller account ID.

> [!NOTE]
> A separate token is issued per seller account, and each environment's dashboard issues its own — a token only works against the API host of the dashboard that issued it.

> [!CAUTION]
> The Integration page also has a **Regenerate** button. Regenerating issues a new token and **immediately invalidates the old one** — there is no overlap window. Only use it when you intend to rotate (see [Rotating a token](#rotating-a-token)).

## What a token looks like

There are two credential shapes on this API, and which one you get depends on
how you got it:

- **Dashboard-issued (Integration page, above).** An opaque long-lived string
  with no prefix — treat the entire value as opaque, don't parse it.
- **Secret key** (Integration → API keys, see [/keys](/keys)). Prefixed
  `sk_test_…` while the account is in demo mode and `sk_live_…` once it has
  gone live.

Both authenticate identically as a Bearer token on the Payments API. Don't
assume every token on this API carries an `sk_` prefix — the dashboard-issued
shape doesn't.

The **Billing API** (`/billing-api/v1`) is stricter: it accepts only
`sk_test_…` / `sk_live_…` secret keys and answers `401` to the prefix-less
dashboard token. If you need both APIs, use an `sk_` key for both — one key
authenticates both surfaces, the legacy token authenticates only this one.

## Verifying a token

The quickest "is this token alive, and which account is it?" check is `GET /account`. It requires only a valid seller token and returns the account the token acts for — including `operationalMode`, which tells you whether you are in `Demo` (test cards, no real money) or `Live`.

```bash
curl -sS "$FYNEX_API/account" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
```

```json
{
  "sellerAccountUuid": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "name": "Acme Marketplace",
  "operationalMode": "Demo",
  "status": "Active",
  "currency": "EUR",
  "selfServeSandbox": false,
  "features": { "walletCreation": true, "payeePayoutAutoFunding": true, "refunds": true }
}
```

A `200 OK` JSON response confirms the token works and the account is active. A `401` means the token is missing, malformed, or invalid. A `403 seller account is not active` means the token is **valid and recognised** but the account isn't active — contact Fynex with your seller account ID (see [How to get a token](#how-to-get-a-token)).

## Storing tokens securely

- **Never commit tokens to source control.** Use a secret manager (HashiCorp Vault, AWS Secrets Manager, GitHub Encrypted Secrets) or environment variables loaded at runtime.
- **One token per environment.** Keep staging and production tokens separate.
- **One token per seller account.** There is no cross-account access; operate multiple sellers with one token each.

```bash
# .env — never commit this file
FYNEX_TOKEN=<your_token_here>
FYNEX_API=https://api.fynex.ai/payments-api/v1
```

## Rotating a token

The simplest way to rotate is the **Regenerate** button on the dashboard Integration page (the same page you got the token from). It issues a new token and reveals it for copying.

> [!CAUTION]
> Rotation **atomically replaces** the existing token. The old token becomes invalid **immediately** — there is no two-token overlap window. Plan a brief service restart or deploy when rotating.

If you prefer to automate it, the same operation is exposed as the GraphQL mutation `regenerateSellerAccountToken` on the `/dashboard/graphql` endpoint (cookie-session authenticated).

### Rotation steps (GraphQL)

1. **Authenticate with the dashboard** to obtain a `dashboard_session` cookie:

   ```bash
   curl -c cookies.txt -X POST https://api.fynex.ai/api/v1/login/dashboard \
     -H "Content-Type: application/json" \
     -d '{"email": "you@example.com", "password": "..."}'
   ```

2. **Call `regenerateSellerAccountToken`** with your seller account ID as `merchantId`:

   ```bash
   curl -b cookies.txt -X POST https://api.fynex.ai/dashboard/graphql \
     -H "Content-Type: application/json" \
     -d '{
       "query": "mutation Rotate($merchantId: ID!) { regenerateSellerAccountToken(merchantId: $merchantId) { authorizationToken } }",
       "variables": { "merchantId": "42" }
     }'
   ```

   The response contains the new `authorizationToken` value.

3. **Update your services** — replace the token in your secret manager / environment and restart affected services before the old token is invalidated (which happened in step 2).

### When to rotate

- A developer with access leaves the team.
- You suspect or confirm a token leak.
- As a precautionary measure on a regular schedule (quarterly is a common default).

## Rotating an API key without an outage

`sk_` API keys, unlike the dashboard token, rotate with an overlap: regenerating a key can leave the
old one working for up to 7 days beside its replacement, so you deploy the new key first and let the
old one lapse. Regenerating answers with the replacement's plaintext, once; the replacement keeps the
old key's name, permissions and lifetime.

```bash
# $FYNEX_TOKEN is the dashboard token from the Integration page, not an sk_ key.
curl -sS -X POST https://api.fynex.ai/api/v1/developer/api-keys/<id>/regenerate \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"overlapHours": 24}'
```

- `overlapHours` — `0` to `168`. `0`, or no body at all, stops the old key at once, as before.
- `expiresAt` — optional RFC 3339 time at which the replacement stops working. Without it the
  replacement lives as long as the old key was issued to, counted from now: a 30-day key regenerated
  today expires in 30 days, and a key that never expired is replaced by one that never expires.

Key management needs the dashboard token or a dashboard session: an `sk_` key cannot create,
regenerate or revoke keys.

## Naming and expiring keys

Give each integration its own key — production, CI, one per agent — so one can be revoked without
touching the others. `POST /api/v1/developer/api-keys` takes:

| Field | Required | Meaning |
|-------|----------|---------|
| `type` | yes | `secret` or `publishable` |
| `name` | no | What the key is for, up to 64 characters. Shown in the key list |
| `expiresAt` | no | RFC 3339 time at which the key stops working, at least a minute ahead. Omit it for a key that never expires |

A key used after its `expiresAt` is refused with `401` `api_key_expired`. The key list reports it
with `status: "expired"`; create or regenerate a key to replace it.

## Restricted keys

A secret key can be **restricted** to what one integration needs: a reporting agent that reads
payments and events, a metering job that only submits usage. Pass `permissions` when you create it:

```json
{ "type": "secret", "name": "metering", "permissions": ["billing_usage:write"] }
```

Each permission is a resource at `read` or `write`; `write` includes `read`. Without `permissions`
the key has full access, as every key had before.

| Resource | Covers |
|----------|--------|
| `payments` | Payments, checkout sessions, capture, payment methods, settling a test payment |
| `refunds` | Refunds and their cancellation, cancelling a promotion-paid order |
| `payees` | Payees |
| `payout_methods` | Payout methods, the seller's own and its payees' |
| `payouts` | Payouts |
| `wallets` | Wallets, their funding and transactions |
| `topups` | Top-up invoices |
| `split_rules` | Split rules and a payment's split decisions |
| `webhooks` | Webhook endpoints and the allowlist |
| `events` | The events feed |
| `vendors` | Marketplace vendors |
| `billing_contracts` | Billing customers, contracts, credits, prices and the test clock |
| `billing_subscriptions` | Subscriptions |
| `billing_invoices` | Billing invoices |
| `billing_usage` | Usage events, metrics, dead letters and usage webhook endpoints |

Every operation in the OpenAPI documents states the permissions it needs in
`x-fynex-permissions`. A restricted key calling an operation it lacks a permission for is refused
with `403` `insufficient_scope`, and the message names what is missing:

```json
{ "error": "this API key is restricted: this route needs payouts:write", "code": "insufficient_scope" }
```

A restricted key reaches only the documented Payments and Billing operations, under
`/payments-api/v1` and `/billing-api/v1`, and the MCP endpoint. `GET /account` works with any key
and reports the calling key's `access` and `permissions` under `apiKey`, so an integration can check
what it may do. Regenerating a restricted key keeps its permissions; only a secret key can be
restricted.

## OAuth for AI clients (MCP)

Not yet available on production.

## Token properties

| Property | Dashboard token | `sk_` API key |
|----------|-----------------|---------------|
| Scope | Single seller account | Single seller account |
| Expiry | None | Optional, per key (`expiresAt`) |
| Revoke | None — rotate to invalidate | Any key, at any time |
| Rotation | Replaces the token at once | At once, or with up to 7 days of overlap |
| Several per account | No — one per seller account | Yes, each with its own name |
| Permissions | Full access | Full access, or restricted to listed permissions |
| Rate limiting | Per-seller token-bucket rate limit; the budget differs by environment — read `X-RateLimit-Limit` / `X-RateLimit-Remaining` rather than assuming a rate. Returns `429` with a `Retry-After` header when exceeded | Same |

## Common errors

| Status | Body | Cause |
|--------|------|-------|
| `401` | `{"error": "authorization token is required", "code": "missing_api_key"}` | Header absent or malformed at the middleware level |
| `401` | `{"error": "invalid authorization token", "code": "invalid_api_key"}` | Token value is not recognised |
| `401` | `{"error": "invalid authorization token: this API key has expired …", "code": "api_key_expired"}` | An `sk_` key used after its `expiresAt` |
| `401` | `{"error": "invalid authorization token: invalid access token: …", "code": "invalid_token"}` | An OAuth access token that expired, whose connection was revoked, or presented outside the MCP endpoint |
| `401` | `{"error": "seller auth is required"}` | Missing seller context inside a handler |
| `403` | `{"error": "seller account is not active", "code": "account_not_active"}` | Token is valid but the seller account isn't active — new demo accounts activate automatically; if you see this, contact Fynex with your seller account ID |
| `403` | `{"error": "this API key is restricted: …", "code": "insufficient_scope"}` | A restricted `sk_` key without the permission this route needs — see [Restricted keys](#restricted-keys) |
| `403` | `{"error": "resource does not belong to this seller"}` | Token is valid but the resource belongs to a different seller |

> [!NOTE]
> Every error body is JSON. Branch on `code`, not on the `error` text — see [Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors).

## See also

- **[Quickstart](https://docs.fynex.ai/payments-api/v2/docs#tag/quickstart)** — Take your first test payment in 5 minutes.
- **[Errors](https://docs.fynex.ai/payments-api/v2/docs#tag/errors)** — Full status code and error body reference.
- **[Request headers](https://docs.fynex.ai/payments-api/v2/docs#tag/headers)** — All request headers reference — required, conditional, and optional.
