Skip to main content
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.
Products counterpart: Security and data handling is the same model written for a security reviewer, with what the disclosure of each credential costs you.

The four credential classes

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>)
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.
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.

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:
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.
1

Your backend creates the session

POST /v2/client-sessions with the standard server key, granting only the permissions that one client flow needs.
2

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.
3

The client exchanges it

POST /v2/client/auth/exchange returns an accessToken.
4

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.
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. 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. 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. 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.
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.

Runtime configuration

The single source of truth for the intersection of entitlement, permission and deployment readiness.

Errors and idempotency

The error envelope, the named codes, and which operations require an Idempotency-Key.