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

Session cookie

aropay_session: httpOnly, SameSite=Lax, 24-hour TTL. Minted by POST /auth/login (or passkey login). What the dashboard uses.

API key

Authorization: Bearer aro_sk_…, a 256-bit secret shown once at creation. What your integration uses. See API keys.

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

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

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.