Skip to main content
Limits are generous for real integrations and tight enough to blunt abuse.

The numbers

A principal is whoever authenticated: a given API key and a browser session count separately, but all keys of one account share its account-level protections. The two Aro Mail claim endpoints have their own budgets because a claim code is a bearer secret: the public preview is limited per IP, and the claim itself is limited per principal and IP instead of the general write budget, so a recipient’s few attempts never compete with an integration’s other writes, and a code cannot be brute-forced through either door. The operator-only POST /transfer/letter/sweep is authenticated by a shared secret and has no per-user limit.

When you exceed a limit

The API returns 429 with the standard envelope:
Back off and retry. A simple pattern with jitter:

Staying well under the limits

  • Poll gas, not balances. GET /gas is designed for polling; /balances does FHE work. See Balances.
  • Poll transactions at 3–10 s intervals; faster adds no information (Ethereum blocks land every ~12 s) and burns read budget. See Track transactions.
  • Batch-poll in-flight rows. If you have several transactions in flight, one GET /transactions?status=QUEUED (and likewise PENDING, SETTLING) per interval replaces one GET /transactions/{id} per row; each list call costs a single read regardless of pageSize.
  • Paginate deliberately. pageSize goes up to 100; one big page beats five small ones.
  • Money movement returns immediately with a QUEUED transaction; a successful 201 is final for the write budget, so don’t retry it. Switch to polling the transaction (reads) until it reaches CONFIRMED, SETTLED, or FAILED.
  • Poll letters, not the claim preview. GET /transfer/letter/{id} is the owner’s polling target for Aro Mail (10 s intervals are plenty; mailed letters refresh Lob status at most every 15 minutes anyway). The public preview is for the recipient’s claim page, not for tracking.

Lockout behavior

There are three locks: two for passwords, one for 2FA codes. Each answers 423 account_locked. Passwords at sign-in. Five consecutive wrong passwords at POST /auth/login lock sign-in for 15 minutes; even correct credentials are refused until the window passes. After a lock passes, one more password may be tried, and a wrong one locks it again. A wrong password gets the generic 401 invalid_credentials, which never says whether the email exists. Passwords from a session. Wrong passwords on a step-up (creating an API key, adding a passkey, exporting a wallet key, turning 2FA on or off) and a wrong currentPassword when changing the password count toward a second lock with the same rules: five in a row lock those actions for 15 minutes, with the message “Too many wrong passwords. Try again later, or sign out and sign in again with your password.” Sign-in still works while it is on. The two password locks are kept apart so neither can be turned against you. Anyone who knows your email can lock sign-in, but that never stops you, while signed in, from stepping up or changing your password. A stolen session can lock step-up, but never your sign-in, and signing in again with your password lifts it (a passkey sign-in doesn’t). The price of keeping them apart is a budget per lock: someone holding a stolen session can try five passwords at step-up as well as five at sign-in, then one at each per 15 minutes, where a single shared lock would allow five in all. A correct password at either one resets both counts. At step-up that happens as soon as the password checks out, before any 2FA code is looked at, so a wrong code never counts as a wrong password. Each password counts before it is checked, so parallel guesses can’t slip past the lock. When two arrive at once for the same lock, only one is checked. At step-up the other gets 429 rate_limited with retryAfterSeconds: 1. At sign-in it gets the usual 401 invalid_credentials after a hash check against nothing, so it matches a wrong password in both body and timing. Retry it. 2FA codes. One per-account cap covers the sign-in code step (POST /auth/login/totp) and the step-up codes on POST /keys, POST /me/passkeys/options, POST /wallets/{id}/export and POST /me/totp/disable. Each code tried counts before it is checked. The 5th code tried without an accepted one locks code entry for 15 minutes; after a lock passes, one more code may be tried, and each further failure doubles the lock (30 minutes, 60 minutes, …) up to 24 hours. While locked, both the sign-in code step and step-up answer 423 account_locked, even for a valid code. A correct password does not reset this counter, and restarting sign-in from the password step gives no fresh attempts: only an accepted code, re-enrolling 2FA (POST /me/totp/enable), or an admin Reset 2FA or Reset password clears it. Changing your own password doesn’t: it needs only the password, so it can’t be allowed to hand out fresh code guesses. A step-up sent without a code on a 2FA account gets 401 totp_required and is not counted. When two attempts arrive at once, only one is checked; the other gets 429 rate_limited with retryAfterSeconds: 1.