> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stableyard.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Credential types, restricted-key permissions, client sessions and the version header.

Stableyard separates machine credentials, human operator sessions, payer capabilities and account-bound client access. For a typical integration you need one of them: a standard server key on your backend. The other three exist so that a browser, a payer or an operator never has to hold it.

<Info>
  Products counterpart: Security and data handling is the same model written for a security reviewer, with what the disclosure of each credential costs you.
</Info>

## The four credential classes

| Credential | Who holds it | How it is sent | What it addresses |
| - | - | - | - |
| **App secret** (standard or restricted server key) | Your backend | HTTP Basic, app ID as username | Everything the app is entitled to, in one environment |
| **Client session token, then access token** | A browser or mobile app you control | `Authorization: Bearer <accessToken>` on `/v2/client/*` | One account, the permissions you granted, until expiry |
| **Payment client secret** | The payer's browser | `Authorization: Bearer <clientSecret>` on `/v2/public/payments/{paymentId}/*` | Exactly one payment |
| **Partner Dashboard session** | One human operator per app environment | A cookie held by the dashboard's own proxy | The dashboard. Not a substitute for an app secret, and not something to automate |

A credential can never widen. A client session cannot exceed the credential that issued it or the products the app has enabled, and a restricted key cannot reach a capability the app is not entitled to.

## The standard server key

Partner-authenticated endpoints use HTTP Basic auth.

* **Username**: your app ID
* **Password**: your app secret
* **Header**: `Authorization: Basic base64(<APP_ID>:<APP_SECRET>)`

```bash theme={null}
AUTH="$(printf '%s:%s' "$STABLEYARD_APP_ID" "$STABLEYARD_APP_SECRET" | base64 | tr -d '\n')"

curl --request GET "https://prod-api.stableyard.fi/v2/partners/config" \
  --header "Authorization: Basic $AUTH"
```

A `200` from `GET /v2/partners/config` confirms the key is valid for the environment behind that host. The response also reports `credentialType` (`standard` or `restricted`) and `credentialMode`, which should read `app_secret` for anything your backend does. What else the response contains, and how to branch on it, is on [Runtime configuration](/capabilities).

<Warning>
  Never place the app secret in a browser, a mobile app, a URL query string or a request body. Partner-authenticated calls stay on your backend.
</Warning>

## The date-based version contract

The `/v2` path identifies the product major. Each app environment is also pinned to an immutable date-based contract such as `2026-09-09`. Read `apiVersion` from `GET /v2/partners/config` and record it with your deployment; `supportedApiVersions` lists what the deployment still accepts on new requests.

You may omit `Stableyard-Version`, in which case Stableyard uses the environment pin. To assert a version explicitly, send the exact value you read:

```bash theme={null}
curl --request GET "https://prod-api.stableyard.fi/v2/partners/config" \
  --header "Authorization: Basic $AUTH" \
  --header "Stableyard-Version: 2026-09-09"
```

Every response echoes the effective `Stableyard-Version`, including error responses. A supported version that is not the one pinned to the environment is rejected rather than quietly changing request or response behaviour. Client session tokens, payment client secrets, Payments and webhook events all stay bound to the version under which they were created, so a version change does not rewrite resources that already exist.

## Browser and mobile: client sessions

Client API routes under `/v2/client/*` use short-lived bearer tokens.

<Steps>
  <Step title="Your backend creates the session">
    `POST /v2/client-sessions` with the standard server key, granting only the `permissions` that one client flow needs.
  </Step>

  <Step title="Your backend hands over the session token">
    The response carries an opaque `clientSessionToken`. That is what reaches the browser or mobile app, never the app secret.
  </Step>

  <Step title="The client exchanges it">
    `POST /v2/client/auth/exchange` returns an `accessToken`.
  </Step>

  <Step title="The client calls with the access token">
    `Authorization: Bearer <accessToken>` on subsequent `/v2/client/*` calls. The exchange response returns the effective permission list, so the client can hide unavailable actions rather than guess.
  </Step>
</Steps>

```json theme={null}
{
  "externalUserId": "user_123",
  "expiresInSeconds": 600,
  "permissions": ["account:read", "payments:read", "payments:write"]
}
```

A client token is always bound to one account. It defaults to `account:read` when `permissions` is omitted, and it cannot exceed the issuing credential's permissions or the app's enabled products.

| Permission | Grant it when |
| - | - |
| `account:read` | The client needs the account it is bound to. This is the default |
| `payments:read`, `payments:write` | The client reads or creates account-bound payments |
| `deposits:read`, `deposits:write` | The client runs a reusable deposit address flow |
| `vault_payments:write` | The client must select or authorize a Vault as a payment method |

Both `accountId` and `externalUserId` are strict lookups here. The account must already exist: creating a client session never creates or updates one. Call `POST /v2/accounts` from your backend first, with an explicit `subjectType`.

**Account-bound Vault checkout needs two credentials at once.** Send the client access token in `Authorization: Bearer <accessToken>` to identify the payer's account, and the receive Payment's secret in `X-Stableyard-Payment-Secret` to bind the request to exactly one Payment. Neither is sufficient alone. A payment client secret on its own can use only the public direct, routing and entitled hosted on-ramp methods; it can never authorize Vault spending.

An expired but correctly signed client token returns `401` with `error.code: "client_token_expired"`, and the client should ask your backend for a new session. Malformed, forged, replayed or already-consumed tokens return `401 unauthorized` and must not be retried. Do not treat the two as the same signal.

## Public checkout: the payment capability token

Endpoints under `/v2/public/payments/{paymentId}/*` take neither an app secret nor a client token. They take the Payment `clientSecret` in `Authorization: Bearer <clientSecret>`. The payment ID is an identifier, not a credential. The shorter code inside a hosted `paymentUrl` is exchanged server-side for the payment ID and its client secret.

Call these directly from payer-facing code. Do not attach Partner Basic auth, and keep the client secret out of URLs, analytics payloads, third-party logs, referrers and screenshots. Treat the hosted link itself as an opaque capability for one payment: it is deliberately resolvable more than once until the payment expires, so refresh, resume and multi-device payer flows work. Cancel the payment to stop accepting new payer actions when a link is disclosed unexpectedly.

## Public app settings: standalone wallet funding

`POST /v2/public/apps/{appId}/payments` accepts no app secret. The app ID selects environment-scoped server settings; it does not authenticate the caller. The `Origin` check is a browser policy control, not cryptographic caller authentication, and a non-browser caller can supply that header. Treat this as a deliberate anonymous receive surface.

Stableyard requires an exact allowed `Origin`, an `Idempotency-Key`, an allowed destination and amount, and an active Payments entitlement. A successful response returns a canonical payment ID plus the scoped client secret used for the rest of checkout.

This route can only create a receive Payment for a policy-approved wallet. It cannot choose fees, provider credentials, webhooks, the environment, arbitrary methods or an account identity. Those settings are written from a trusted backend through `GET/PUT /v2/partners/payment-settings`. Use partner-authenticated creation when the caller itself must be authenticated.

## Permissions are not entitlements

A permission answers a narrow question: what may this particular key call? It never answers whether the app may offer the capability at all, and it never proves the capability is operational. Those are separate conditions, and the taxonomy is on [What's live, and how it gets switched on](/supported-regions-and-currencies).

The API field is named `scopes`. Everywhere else, including the Partner Dashboard, they are called permissions.

## Restricted keys

Use a restricted key for a narrowly scoped backend service, a reporting process or a session issuer. Do not make raw permission selection part of the normal integration path: start with a standard server key and narrow later where a service has a real least-privilege requirement.

| Product area | Permission | Covers |
| - | - | - |
| Accounts | `v2:accounts` | Accounts, wallets, handles, settlement preferences |
| Payments | `v2:payments` | Receive, collect, send, refund and reconcile Payments |
| Deposit Addresses | `v2:deposit_addresses` | Reusable addresses and deposits |
| Identity and compliance | `v2:kyc` | Account verification operations when enabled |
| Vaults | `v2:vaults` | Vault provisioning, policies, mandates and yield settings |
| Webhook administration | `v2:webhooks` with `v2:console` | Endpoint configuration, delivery review and retries |

A restricted key keeps the permissions it was issued with. Enabling a module for the app later does not widen an existing key, so plan on issuing a new one. A standard key tracks the app's enabled products instead.

<Warning>
  A permission does not prove operational availability. Routing, hosted on-ramp, cards, External QR and External Bank Transfer are rails inside Payments, and external fiat destinations have no standalone off-ramp API. Discover payment methods at runtime rather than from a stored permission list.
</Warning>

<CardGroup cols={2}>
  <Card title="Runtime configuration" icon="sliders" href="/capabilities">
    The single source of truth for the intersection of entitlement, permission and deployment readiness.
  </Card>

  <Card title="Errors and idempotency" icon="triangle-exclamation" href="/api-reference">
    The error envelope, the named codes, and which operations require an `Idempotency-Key`.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.