---
title: "API keys, workspaces and environments"
description: "The x-api-key header, creating and revoking keys in the console, what a key can do, testnet vs mainnet with one key, and the endpoints that need no key"
source: https://docs.thru.la/auth.md
html: https://docs.thru.la/auth
index: https://docs.thru.la/llms.txt
---
# API keys, workspaces and environments

## The header

Every server-to-server request carries your key in a header:

```
x-api-key: thru_sk_<48 lowercase hex characters>
```

There is no `Authorization: Bearer` scheme.

| Response | Meaning |
|---|---|
| `401 Authentication required` | No key was sent. |
| `401 Invalid API key` | The key is unknown or revoked, or the workspace is suspended. The response does not say which. |

## Creating and revoking keys

A signed-in person creates keys in the console, under **Developers → API keys**. The raw key is shown **once**. thru stores only a hash, so a lost key cannot be recovered; revoke it and create another.

| Endpoint | Who can call it |
|---|---|
| `GET /v1/api-keys` | Any key or console session. Lists keys without the secret. |
| `POST /v1/api-keys` `{ "name": "…" }` (up to 80 characters) | A console session only |
| `DELETE /v1/api-keys/:id` | A console session only |

An API key cannot create or revoke keys; those calls return `401 A dashboard session is required`. This means a leaked key cannot mint itself a replacement or lock you out by revoking your other keys.

## What a key can do

A key acts with the workspace's full authority, except for the actions reserved for a signed-in person: managing keys, members and roles, and deleting the workspace. A key **can**:

- create payments, checkout sessions, products and invoices;
- register and change webhook endpoints, and read their signing secrets;
- read and rotate the checkout signing secret;
- issue refunds;
- add and change settlement addresses.

Treat a key like the password to your revenue. Keep it on your server, and never put it in a browser, a mobile app or a public repository.

## Testnet and mainnet

There are no separate test and live keys. One key, one base URL and one set of webhook endpoints serve both networks. The network is a property of each payment:

| Object | How its network is chosen |
|---|---|
| Payment (`POST /v1/payments`) | The `network` field. If you leave it out, the server's default applies (mainnet on api.thru.la). Always send it. |
| Product | The `network` field. It defaults to `mainnet`, and every checkout of the product uses it. |
| Invoice | The `network` field. It defaults to `mainnet`. |
| Checkout session | Copied from its product or invoice. You cannot set it on the session. |

Every payment and session carries `network`, and so do the events you fulfil from: `payment.confirmed`, `payment.underpaid`, `payment.overpaid` and `payment.refunded` (in `data.payment.network`), `payment.expired` (`data.network`), every `checkout.session.*` event, and `settlement.completed` / `settlement.failed`. A session also carries `livemode` (`true` exactly when `network` is `mainnet`). **Your fulfilment code must check it.** Otherwise a payment made with free testnet tokens could unlock something you sell for real money.

## Use a separate workspace for staging

Because keys are not tied to a network, the way to keep staging away from production money is a **second workspace**, created in the console. It has its own keys, webhook endpoints, return origins, checkout secret and settlement addresses. Point your staging deployment at that workspace's key.

## Endpoints that need no key

These are called by thru's hosted pages and by the browser SDKs. The id in the path is the credential, and the responses never include your `reference` or `metadata`.

| Endpoint | Used by |
|---|---|
| `GET /v1/public/payments/:id` | Your own payment screen, or `@thru-payment/checkout-core` |
| `GET /v1/public/checkout/sessions/:id`, `POST /v1/public/checkout/sessions/:id/redeem`, `POST /v1/public/checkout/sessions/:id/cancel` | The hosted page at `/c/:id`. Limited to 120 requests per minute per checkout session per IP. |
| `GET /v1/public/products/:slug`, `POST /v1/public/products/:slug/checkout` | The hosted payment link at `/pay/:slug` |
| `GET /v1/public/invoices/:publicId`, `POST /v1/public/invoices/:publicId/pay` | The hosted invoice at `/i/:publicId` |
| `GET /v1/docs`, `GET /v1/docs/:topic` | These docs |

Do not grant anything based on a public endpoint's answer. A browser can be made to show anything, so grant from your webhook handler or from an authenticated read.

---

Related: [start-here](https://docs.thru.la/start-here.md) · [api-conventions](https://docs.thru.la/api-conventions.md) · [going-live](https://docs.thru.la/going-live.md)
