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 (once you have accepted the privacy terms), but it can never escalate itself. No new keys, no password changes, no key exports.

Step-up

A session is not enough on its own to create a credential that outlives it, or to take one away. Creating an API key, starting a passkey registration, exporting a custodial wallet key, and turning 2FA on or off also ask you to re-enter your password, plus a fresh authenticator code when 2FA is on: Codes are single-use, as at sign-in. A wrong password or code returns 401 invalid_credentials / invalid_totp_code (totp_required when the code is missing, which isn’t counted; totp_code_reused for a replay). A wrong password counts toward the step-up password lock, which is separate from sign-in’s: five in a row lock step-up and password change for 15 minutes, while sign-in keeps working (and failed sign-ins never block a step-up). Each code tried counts toward the same 2FA code cap as the sign-in code step: five without an accepted one lock code entry for 15 minutes, doubling with each further failure up to 24 hours. Each lock answers 423 account_locked (lockout behavior). Someone holding a stolen session cookie therefore can’t plant a key or passkey without also knowing your password and holding your authenticator, nor export a wallet key.

Recovery and credential revocation

Changing your password, or an admin resetting your password or 2FA, revokes everything a past session could have left behind: An admin password reset also lifts every lock on the account, the 2FA code lock included; 2FA stays on, so sign-in still asks for a code. Changing your own password leaves the 2FA code lock in place. Revocation is permanent: a reactivated account gets its sign-in back, never its old keys. Create new keys after signing in (with step-up). Each revoked key shows why in revokedReason (password_changed, admin_password_reset, admin_2fa_reset, account_deactivated, or user when you revoked it yourself).

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 (or turning 2FA on) bumps it and immediately invalidates every other session. API keys carry the token version they were created under too, so a password change or admin reset retires them as well (see Recovery). 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. Before your first transaction you also accept the AroPay data-privacy terms in the dashboard. Until you do, money movement and Aro Mail send and claim return 403 terms_not_accepted, for API keys as well as sessions.

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 wrong passwords lock sign-in for 15 minutes (423 account_locked). Failed step-up passwords count toward a separate lock, so someone who only knows your email can’t stop you from changing your password while signed in. 2FA codes have their own cap, shared by the sign-in code step and step-up, which escalates up to 24 hours and which a correct password doesn’t reset (details).
  • 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.