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.