Authentication
Flightdeck has two external credential types. Which one you hold determines both the header you send and what you are allowed to do. Neither credential ever belongs in browser code — both are server-side secrets.
The two credential types
Merchant credential (fdk_)
The tenant-scoped partner credential. It is an opaque bearer key prefixed
fdk_, sent in the standard Authorization header:
curl https://api.ruo.pro/v1/my/store \
-H "Authorization: Bearer fdk_your_key_here"The tenant it acts on and the scopes it is granted are carried by the key,
resolved server-side from the credential row — never from a path or body
parameter. This is the credential-carries-ownership model (see
Tenancy). The OpenAPI security scheme is fdkBearer
(type: http, scheme: bearer, bearerFormat: fdk_<opaque>).
fdk_ keys reach the self-serve /v1/my/* routes and the merchant API under
/v1/merchant/*.
Storefront token
The narrow storefront BFF credential. It is an opaque secret (no fixed prefix) — a single platform-wide static value, operator-provisioned — sent on its own header, not as a bearer token:
curl -X POST https://api.ruo.pro/v1/tenants/nova-peptide/checkout \
-H "x-storefront-token: your_storefront_token" \
-H "Idempotency-Key: cart-8c1f2a:attempt-1" \
-H "Content-Type: application/json" \
-d '{ "customer_ref": "guest:jane@example.com", "items": [ ... ] }'The server reads it from the STOREFRONT_API_TOKEN environment secret; the SDK
and other consumers supply it as FLIGHTDECK_STOREFRONT_TOKEN. It places a
checkout and reads a shopper’s own data for one deployment, and it is rejected on
every operator route. The OpenAPI security scheme is storefrontToken
(type: apiKey, in: header, name: x-storefront-token).
The storefront token is platform-wide today, not per-tenant. There is one shared token for the whole platform, and the tenant for a storefront call comes from the
{tenant}path segment, not from the token. Folding the storefront credential into per-tenantfdk_keys with a narrow checkout scope is planned but not shipped. Until then, treat the token as a shared secret and rely on the pinned tenant path.
The public catalog reads (/v1/tenants/{tenant}/catalog/*) take no
credential at all — they are the one deliberately public read surface.
Tenant resolution — from the credential, never the request
There is no default tenant anywhere in the system. Tenant-facing routes resolve their tenant from the credential:
fdk_self routes live under/v1/my/*— the tenant is the credential’s own tenant. You never pass it.- Public per-tenant routes live under
/v1/tenants/{tenant}/*— the tenant is the path segment, used by public catalog reads and storefront-token checkout.
A request whose tenant cannot be resolved fails closed. See Tenancy for the full fail-closed model and the one written host-resolution exception.
Scopes
Every fdk_ key is issued against a closed scope allowlist. A scope says “some
tenant can do X”; the key row says which tenant. Merchant routes enforce the
required scope per route. The allowlist, grouped by resource:
| Resource | Scopes |
|---|---|
| Orders | read:own_orders, manage:own_orders |
| Catalog | read:own_catalog, manage:own_catalog |
| Store | read:own_store, write:own_store |
| Webhooks | manage:own_webhooks |
| Domains | manage:own_domains |
| Credentials | manage:own_credentials |
| Billing | read:own_billing |
| Customers | read:own_customers |
| Affiliates | read:own_affiliates, manage:own_affiliates |
| Media | manage:own_media |
| Fraud | manage:own_fraud |
| Fulfillment | manage:own_fulfillment |
| Inventory | manage:own_inventory |
| Marketing | manage:own_marketing |
| Returns | manage:own_returns |
| Gift cards | manage:own_giftcards |
| Store credit | manage:own_store_credit |
| Subscriptions | manage:own_subscriptions |
| Analytics | manage:own_analytics |
Read and write are deliberately split where money or catalog state is at stake:
a read:own_orders key can list orders but can never move money — that requires
manage:own_orders.
A request whose credential lacks the route’s required scope is refused with
403 forbidden.
Credential lifecycle
Bootstrapping the first key
The first merchant key is self-serve. A store owner mints it with
POST /v1/my/credentials/initial, authenticated by the owner identity
session (not by an existing fdk_ key) — this route mints the pre-live scope
set so a new store can start building before it goes live. From there,
POST /v1/my/credentials (below) issues any further keys, subset-gated by the
issuing key’s own grant. See Get API keys for the full
bootstrap walkthrough.
Manage keys through the /v1/my/credentials routes. Every one of these requires
the manage:own_credentials scope.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/my/credentials | List the tenant’s keys — metadata only, revoked rows included. No key value or hash is ever returned. |
POST | /v1/my/credentials | Issue a new key. The plaintext key appears exactly once, in the 201 body. |
POST | /v1/my/credentials/{id}/revoke | Revoke a key. Revocation is a recorded state, never a delete. |
Issuance is subset-gated
A key can only mint a key whose scopes its own grant already includes.
Requesting a scope the issuing key does not hold is refused with 403 scope_escalation (the message names the offending scope) — key management can
never become privilege escalation.
curl -X POST https://api.ruo.pro/v1/my/credentials \
-H "Authorization: Bearer fdk_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "label": "ci-reader", "scopes": ["read:own_orders", "read:own_catalog"] }'The 201 response carries the minted credential including its plaintext
key — store it immediately; it is never retrievable again. Only the hash is
kept server-side.
Revocation
curl -X POST https://api.ruo.pro/v1/my/credentials/{id}/revoke \
-H "Authorization: Bearer fdk_your_key_here"The response reports alreadyRevoked (the end state already held; nothing
changed now) and selfRevoked (the key that made this call is now dead — its
next request will 401). Self-revocation is allowed as a last action. There is
no lastUsedAt — it is not tracked, so it is not reported.
Credential-verification is rate-limited on the attempt, before the database lookup — an
fdk_key is otherwise online-guessable. A spent limiter returns429with aretry-afterheader. See Pagination and rate limits.