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 — never from a path or body parameter. This is the credential-carries-ownership model (see Tenancy).
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), issued to you when your store is 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": [ ... ] }'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 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 — an
fdk_key is otherwise online-guessable. A spent limiter returns429with aretry-afterheader. See Pagination and rate limits.