---
title: "Authentication"
description: "Authenticate with a secret or restricted key, give each platform its own named key, and read the errors a key can get."
url: https://docs.fluveo.com/start/authentication/
---

> Index of every page: https://docs.fluveo.com/llms.txt. Integrating Fluveo? Read https://docs.fluveo.com/agents/integrate.md first.

> Too long? This page with code in one language only: https://docs.fluveo.com/start/authentication.curl.md (cURL), https://docs.fluveo.com/start/authentication.node.md (Node.js), https://docs.fluveo.com/start/authentication.python.md (Python), https://docs.fluveo.com/start/authentication.php.md (PHP).

# Authentication

Authenticate with a secret or restricted key, give each platform its own named key, and read the errors a key can get.

Every `/v1` request carries one of your store's secret or restricted keys. Fluveo reads the key, finds the store it belongs to, and answers only with that store's objects.

## Know your keys

A secret key belongs to exactly one store. Your store's id starts with `mch_`, and you can read it with `GET /v1/account`.

| Key | What it does today |
| --- | --- |
| `sk_test_...` | Secret key, test mode. Every call works on test data, and no real money moves. |
| `rk_test_...` | Restricted key, test mode. It calls only the endpoints its permissions allow. See [Restricted keys](https://docs.fluveo.com/start/authentication.md#restricted-keys). |
| `sk_live_...`, `rk_live_...` | Live mode. Fluveo does not serve live mode yet, so a request with your store's live key answers `403`. |
| `pk_test_...`, `pk_live_...` | Publishable key, for the browser. [fluveo.js](https://docs.fluveo.com/payments/fluveo-js.md) uses it to show the payment form on your own page and to confirm the payment of a PaymentIntent whose client secret the page holds. It creates, lists and changes nothing: every other `/v1` request that sends it answers `403` with code `secret_key_required`. |

Each object carries `livemode`. With a test key it is always `false`.

## Get a key from the Dashboard

1. Sign in to the [Fluveo Dashboard](https://dash.fluveo.com).
2. Open **Settings** in the sidebar and go to the **API keys** section. Check that the bottom of the sidebar shows **Test mode**.
3. Under **Standard keys**, click **Create secret key**, give it a name, such as the platform account that will use it, click **Create**, copy the key and click **Done**. The Dashboard shows the full key only once. After that it lists only the name and a masked hint.

There is no API call that creates a key.

## One key per platform account

A store can hold many secret keys and many restricted keys at the same time, each with its own name. Give each platform account its own key: if your store sells through three accounts on a checkout platform, create three keys named after them. Each key works on its own:

- **Rotate key** replaces one key with a new one that works at once. In **Expiration** you choose when the old key stops working: **Now**, or in 1 hour, 24 hours, 3 days or 7 days. Until then both keys work, so you can move each platform to the new key without downtime. The new secret key is named **Secret key** and the new publishable key **Publishable key**; a new restricted key keeps the old key's name and permissions.
- **Edit key** changes a secret or restricted key's name and note, and a restricted key's permissions.
- **Expire key** stops a secret or restricted key at once and cannot be undone. A publishable key cannot be expired; rotate it instead.

Rotating, editing or expiring one key never changes any other key. The list shows each key's name, creation date, last use and, after a rotation, the time left before the old key expires. There is no limit on how many secret or restricted keys a store creates.

> **Keep secret keys on your server:** A secret key can create payments and refunds for your store. Never put it in a browser, a mobile app, a public repository or a log. If a key leaks, rotate it with **Now**.

## Restricted keys

A restricted key (`rk_test_...`) works like a secret key, but only on the endpoints its permissions allow. Use one when a platform, a report or a tool needs only part of your store, such as reading payments without creating them.

Create one in **Settings > API keys > Restricted keys > Create restricted key**: give it a name and choose **None**, **Read** or **Write** for each resource. **Read** allows the `GET` requests of the resource. **Write** allows every request of the resource, reads included. Every resource left at **None**, and every `/v1` URL no resource covers, answers `403` to a restricted key.

[Permissions](https://docs.fluveo.com/permissions.md) lists each resource with its paths and the permission every operation of the reference needs. A resource's name there can differ from its Dashboard label: Prices is `plan`, and Charges and Refunds is `charge`. Paths match leading whole URL segments. The first matching resource on the Permissions page applies, so `/v1/customers/{customer}/payment_methods` needs `payment_method`, not `customer`. A request a key's permissions do not allow answers `403` before Fluveo does anything else, and the message names the permission that would allow it:

*403 Forbidden*

```json
{
  "error": {
    "type": "invalid_request_error",
    "message": "The provided key 'rk_test_****abcd' does not have the required permissions for this endpoint on account 'mch_...'. Having the 'rak_payment_intent_write' permission would allow this request to continue."
  }
}
```

The permission in the message is `rak_<resource>_read` or `rak_<resource>_write`, with the resource names [Permissions](https://docs.fluveo.com/permissions.md) lists. An endpoint no permission covers answers the same `403` with "This is a restricted API key, but the required permissions are not available for use by restricted keys." in place of the last sentence.

A path under a resource the key may call that is not an endpoint, such as `/v1/payment_intents/{intent}/unknown`, answers `404` "Unrecognized request URL" once the key holds that resource's permission, as it does for a secret key. Without the permission it answers the `403` above.

A restricted test key created with **This key is for an AI agent** checked is an agent key. The API applies the agent policy to its calls, and it is the key Fluveo MCP takes. [MCP authentication](https://docs.fluveo.com/agents/mcp/authentication.md) shows how to make, use and revoke one.

## Send the key

Put the key in the `Authorization` header as a Bearer token.

**Authenticated request**

cURL:

```bash
curl https://api.fluveo.com/v1/balance \
  -H "Authorization: Bearer sk_test_..."
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/balance", {
  headers: { Authorization: "Bearer sk_test_..." },
});
console.log(response.status, await response.json());
```

Python:

```python
import requests

response = requests.get(
    "https://api.fluveo.com/v1/balance",
    headers={"Authorization": "Bearer sk_test_..."},
)
print(response.status_code, response.json())
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/balance");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
echo curl_exec($ch);
```

HTTP Basic also works: the key is the user name and the password is empty, as in `curl -u sk_test_...: https://api.fluveo.com/v1/balance`. The trailing colon tells curl the password is empty. Use one scheme per request.

Request bodies are form-encoded (`application/x-www-form-urlencoded`). Nested values use brackets: `metadata[order_id]=1042` for a map and `payment_method_types[]=pix` for a list.

## One store per key

Fluveo has no header that switches to another account. A request that sends a header meant to act for another account answers `400` with that header in `param`, whatever its value. To act for another store, use that store's own key.

This is also how platforms work. A checkout platform never holds one key for many stores: each merchant pastes its own Fluveo key into the platform. See [Platforms](https://docs.fluveo.com/platforms.md).

## When the key is wrong

| Case | Status | Error |
| --- | --- | --- |
| No `Authorization` header, or an empty key | `401` | `invalid_request_error`, message "You did not provide an API key. ..." |
| A key in the wrong shape or an unknown key, a live or publishable one included | `401` | `invalid_request_error`, message "Invalid API Key provided: sk_test_****abcd" with the key masked |
| An expired key: expired in the Dashboard, or rotated and past the expiration you chose | `401` | `invalid_request_error`, code `api_key_expired`, message "Expired API Key provided: sk_test_****abcd" with the key masked |
| Your store's publishable key | `403` | `invalid_request_error`, code `secret_key_required`, message "This API call cannot be made with a publishable API key. Please use a secret API key." |
| A restricted key on an endpoint its permissions do not allow | `403` | `invalid_request_error`, the message in [Restricted keys](https://docs.fluveo.com/start/authentication.md#restricted-keys) |
| Your store's `sk_live_` or `rk_live_` key | `403` | `invalid_request_error`, message "Live mode is not available yet: this API serves test mode only. Use your test secret key (sk_test_...)." |
| A valid key on a route your store may not use | `403` | `invalid_request_error`, the "not available" message below |
| A header that acts for another account | `400` | `invalid_request_error` with that header in `param` |

A `403` "not available" refusal carries `code` `not_available`:

*403 Forbidden*

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "not_available",
    "message": "This API is not available on this account. If its setup is not finished or is still under review, finish it in the Dashboard under Settings and try again once the account is approved."
  }
}
```

Test keys work from the moment your store exists. In a new store's first seconds, while its test mode is still being set up, a test call can get this same 403 `not_available` with the message "This account's test mode is still being set up. Try again in a few seconds." Send the same request again a few seconds later; nothing in the Dashboard is needed. Activation in the Dashboard under Settings opens live mode only: if it is not finished or is still in review, your live keys wait for it.

Every answer carries a `Request-Id` header (`req_...`). Keep it when you report a problem. An error of a call your store's log keeps also carries `error.request_log_url`, a link to that call's log in the Dashboard; see [Request logs](https://docs.fluveo.com/dashboard/request-logs.md#from-an-error). The full list of errors is on [Errors](https://docs.fluveo.com/errors.md).

## Test it

- Call `GET /v1/account` with your test key. You get `200` and an `id` that starts with `mch_`.
- Call it with no `Authorization` header. You get `401`.
- Call it with your store's publishable key (`pk_test_...`). You get `403` with code `secret_key_required`.
- Create a restricted key with **PaymentIntents: Read**. `GET /v1/payment_intents` answers `200` and `POST /v1/payment_intents` answers `403` naming `rak_payment_intent_write`.

> **For AI agents:**
>
> Authenticate with Authorization: Bearer sk_test_... (or a restricted rk_test_... key) on every request. Never send a header that acts for another account, and never try live keys: the store's live key answers 403, live mode is not available yet. A publishable key never works on /v1: it answers 403 secret_key_required. A restricted key answers 403 on every endpoint its permissions do not allow, and the message names the permission (rak_<resource>_read or _write) a human must grant in the Dashboard. There is no endpoint that creates or lists API keys; a human creates a named key in Dashboard > Settings > API keys (Create secret key, or Create restricted key with its permissions). Never print, log or commit the key.

## Next

- [Testing](https://docs.fluveo.com/start/testing.md): What test mode can do, and the test values for each outcome.
- [Idempotency](https://docs.fluveo.com/start/idempotency.md): Retry a POST safely with the Idempotency-Key header.
- [Versioning](https://docs.fluveo.com/start/versioning.md): The one API version Fluveo serves.
