Get startedAuthentication

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-tenant fdk_ 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:

ResourceScopes
Ordersread:own_orders, manage:own_orders
Catalogread:own_catalog, manage:own_catalog
Storeread:own_store, write:own_store
Webhooksmanage:own_webhooks
Domainsmanage:own_domains
Credentialsmanage:own_credentials
Billingread:own_billing
Customersread:own_customers
Affiliatesread:own_affiliates, manage:own_affiliates
Mediamanage:own_media
Fraudmanage:own_fraud
Fulfillmentmanage:own_fulfillment
Inventorymanage:own_inventory
Marketingmanage:own_marketing
Returnsmanage:own_returns
Gift cardsmanage:own_giftcards
Store creditmanage:own_store_credit
Subscriptionsmanage:own_subscriptions
Analyticsmanage: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.

MethodPathPurpose
GET/v1/my/credentialsList the tenant’s keys — metadata only, revoked rows included. No key value or hash is ever returned.
POST/v1/my/credentialsIssue a new key. The plaintext key appears exactly once, in the 201 body.
POST/v1/my/credentials/{id}/revokeRevoke 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 returns 429 with a retry-after header. See Pagination and rate limits.