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

# Authentication

> Two interchangeable credentials (browser sessions and API keys), plus the rules for which endpoints accept which.

Every protected endpoint accepts either of two credentials. A handful of
sensitive operations deliberately require the browser session.

<Columns cols={2}>
  <Card title="Session cookie" icon="browser" iconType="duotone">
    `aropay_session`: httpOnly, `SameSite=Lax`, 24-hour TTL. Minted by
    `POST /auth/login` (or passkey login). What the dashboard uses.
  </Card>

  <Card title="API key" icon="key" iconType="duotone">
    `Authorization: Bearer aro_sk_…`, a 256-bit secret shown once at
    creation. What your integration uses. See [API keys](/security/api-keys).
  </Card>
</Columns>

## Precedence

If a request carries an `Authorization` header, **it takes precedence over any
cookie** and must be a Bearer scheme. A malformed or unknown key fails with
`401 invalid_api_key` even if a valid session cookie is also present.

## Session-only endpoints

Operations that mint or destroy credentials (or export key material) require
a browser session. Called with an API key, they return `403 session_required`:

| Endpoint                                                             | Why session-only                                                       |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `POST /me/password`                                                  | A leaked key must not be able to rotate the password and lock you out. |
| `POST /me/totp/setup` / `enable` / `disable`                         | 2FA management changes login security.                                 |
| `POST /me/passkeys/options` / `register`, `DELETE /me/passkeys/{id}` | Same, for passkeys.                                                    |
| `POST /keys`, `DELETE /keys/{id}`                                    | A leaked key must not mint or revoke credentials.                      |
| `POST /wallets/{id}/export`                                          | Key material leaves the platform only with an interactive session.     |

The net effect: **an API key can move sandbox money, but it can never escalate
itself**. No new keys, no password changes, no key exports.

## Login flows

<Tabs>
  <Tab title="Password">
    ```bash theme={null}
    curl -s "$BASE/auth/login" -c cookies.txt \
      -H "content-type: application/json" \
      -d '{"email":"you@company.com","password":"…"}'
    ```

    Success sets the session cookie and returns your user object. If 2FA is
    enabled, you instead receive `{ "requiresTotp": true, "pendingToken": "…" }`;
    complete it with [`POST /auth/login/totp`](/security/two-factor-authentication).
  </Tab>

  <Tab title="Passkey">
    Fetch WebAuthn options with `POST /auth/passkey/options` (email optional;
    omit for usernameless), have the authenticator sign, then verify with
    `POST /auth/passkey/verify`. A passkey login counts as strong MFA on its
    own. See [Passkeys](/security/passkeys).
  </Tab>
</Tabs>

Sessions embed the account's **token version**; changing the password bumps it
and immediately invalidates every other session. `POST /auth/logout` clears
the cookie; `GET /auth/session` returns the current principal and which
`authMethod` authenticated it (`session` or `api_key`).

## First login & account provisioning

Accounts are provisioned by Aro Media admins; you receive a one-time
temporary password and are **forced to set a new one** on first login (the
user object carries `mustChangePassword: true` until you do). Password policy:
at least 10 characters with an uppercase letter, a lowercase letter, and a
digit.

## Request conventions

* Write requests **must** send `Content-Type: application/json`; anything
  else is rejected with `415 unsupported_media_type`. This doubles as CSRF
  protection: cross-origin HTML forms can't produce that content type without
  a preflight the API never allows.
* Every response uses the shared envelope: `{ "ok": true, "data": … }` or
  `{ "ok": false, "error": { "code", "message", "details?" } }`. See
  [Errors](/api-reference/errors).

## Login protections

* **Rate limit**: 10 login attempts per 15 minutes per IP.
* **Lockout**: 5 consecutive failures lock the account for 15 minutes.
* **Generic errors**: failed logins return the same `invalid_credentials`
  regardless of which part was wrong.
* Disabled accounts fail with `403 account_disabled` on any auth path.
