# Authentication

> How to authenticate to the MyStocks Partner API — API keys via Authorization or x-api-key header, scoped read-only data keys for client-side use, Firebase-token endpoints, and the mandatory Idempotency-Key for money-movement calls.

Most requests require your API key. The production partner-portal endpoints `POST /register`,
`POST /session`, `GET /me`, `POST /upgrade-request`, and `POST /api-keys/revoke` instead authenticate
with a **Firebase ID token** from the portal session (see [Firebase-token endpoints](#firebase-token-endpoints)).
The OAuth token endpoint validates client credentials, and the root-level sandbox reset utility uses
the sandbox key. For API-key-authenticated requests, send the key using **either** method:

**Option A — Authorization header (recommended)**

```http
# Sandbox
Authorization: Bearer sk_sandbox_<your_key>

# Production
Authorization: Bearer pk_live_<your_key>
```

**Option B — Custom header**

```http
x-api-key: sk_sandbox_<your_key>  # or pk_live_<your_key>
```

<Callout type="warn">
  Never expose a full key (`pk_live_`) in client-side code or public repositories. Prefer a backend
  proxy or short-lived token for browser and mobile contexts. A **data key** (`pk_data_`) is read-only,
  but it remains extractable and shares the parent key's quota. Sandbox keys carry no real-money risk;
  production full keys control real funds — guard them accordingly.
</Callout>

## Data keys — scoped read-only credentials

<TryEndpoint id="createDataKey" label="Try data key endpoint" />

A `pk_data_` key is a scoped, read-only credential derived from your full key. It cannot trade, move
funds, or access sub-account PII, but exposing it still permits quota exhaustion and unapproved reuse.
Reserve embedded keys for low-stakes public widgets. For production mobile or web apps, prefer proxying market data through
your backend or minting short-lived tokens via `POST /oauth/token`: an embedded key can be extracted
by anyone, it shares your full key's rate-limit bucket (an abuser can exhaust your quota), and since
only one data key is active per partner, rotating it breaks every shipped install at once.

Generate a data key via `POST /api-keys/data-key` (see [Key Management](/partners/docs/key-management)).
Data keys share the rate-limit bucket of their parent full key. Any call outside the allowed list
returns `403 FORBIDDEN`.

**Allowed endpoint families (GET only):** `/stocks/**`, `/etfs/**`, `/bonds/**`, `/funds/**`,
`/market/**`, the deprecated `/market-data/**` aliases, `/market-intel/**`, `/opportunities/**`,
`/dividends/**`, `/companies/**`, and `/fx/**`. Every other path and every non-GET method returns
`403 FORBIDDEN` for a data key.

## Short-lived tokens — OAuth client credentials

<TryEndpoint id="createOAuthToken" label="Try OAuth token endpoint" />

For client-side surfaces where you don't want to embed a long-lived key, exchange your key for a
short-lived bearer token via `POST /api/v1/partner/oauth/token` — a standard OAuth 2.0
**client-credentials** grant. The token inherits your key's type (full or data) and scopes, expires in
**15 minutes**, and is prefixed `ms_oauth_`. This keeps a leaked credential useful for minutes rather
than indefinitely, and lets you rotate without breaking shipped installs.

<Callout type="info">
  This grant must be enabled for your partner account first (`oauthClientCredentialsEnabled`). Until it
  is, the endpoint returns `403` — contact us to switch it on.
</Callout>

Send your `client_secret` (your API key) via HTTP Basic auth, or in a form / JSON body. An optional
`scope` narrows the token to a subset of your key's scopes:

```http
POST /api/v1/partner/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=<your_key_id>&client_secret=pk_live_<your_key>&scope=market:read
```

```json
{
  "access_token": "ms_oauth_…",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "market:read"
}
```

Use the returned token exactly like a key: `Authorization: Bearer ms_oauth_…`. When it expires, request
a new one. The token endpoint is rate-limited per source IP to deter credential stuffing.

## Firebase-token endpoints

Five production portal operations authenticate with a Firebase ID token
(`Authorization: Bearer <firebase-id-token>`) rather than a partner API key: `POST /register` (issues
and rotates a key), `POST /session` (mints a short-lived portal API token), `GET /me` (powers the
dashboard UI), `POST /upgrade-request` (requests production access), and `POST /api-keys/revoke`
(revokes a supplied compromised key). Their sandbox counterparts can differ: for example,
`POST /api/sandbox/v1/reset` and sandbox key-management operations authenticate with the sandbox key.

## Idempotency — safe retries for authenticated mutations

<TryEndpoint id="createDeposit" label="Try an idempotent deposit" />

<Callout type="info">
  On unstable mobile networks a POST can succeed on the server but time out on the client — causing a
  double-charge if the app retries. Every authenticated `POST`, `PATCH`, `PUT`, and `DELETE` request
  **requires** a unique `Idempotency-Key` header. The only exceptions are sandbox `register` and
  `reset`, partner `apply`, `upgrade-request`, `session`, and `oauth/token`. Calls that omit the header are
  rejected with `400 MISSING_IDEMPOTENCY_KEY`. Keys must contain **8 to 200 characters**. We deduplicate by key for 24 hours and return the
  cached response on retry.
</Callout>

```http
POST /api/v1/partner/users/{userId}/deposit
Authorization: Bearer pk_live_<your_key>
Idempotency-Key: dep_riven_user_42_1743152580   # unique per attempt
```

Use any unique string — a UUID or your own transaction ID works well. If a concurrent duplicate is
detected you receive HTTP 409 until the first request completes.

### When is it safe to retry?

| Call type | Safe to retry? | How |
| --- | --- | --- |
| `GET` (all read endpoints) | Always | Retry freely with exponential backoff. Reads have no side effects. |
| Authenticated `POST`, `PATCH`, `PUT`, or `DELETE` | Only with `Idempotency-Key` | Required except for `apply`, `upgrade-request`, `session`, and `oauth/token`. Resend the exact request with the same key to get the original response back. |
| Sandbox `POST /register` and `POST /reset` | Yes | Explicitly exempt from the idempotency-header requirement. |

On `429`, wait for `Retry-After` before retrying (see [Rate Limits](/partners/docs/rate-limits)). On
`5xx` for an authenticated mutation, always retry _with the same Idempotency-Key_ — never generate a
fresh key for a retry of the same logical operation. Generate a new key only for a new logical action.
