Skip to main content

HTTP status codes

The Laso Finance API uses standard HTTP status codes. Here’s what each means and how to handle it.

402 Payment Required

This is the normal x402 flow — not an error. The server is telling you the price and how to pay.
How to handle: If you’re using an x402 client library (like x402-axios), this is handled automatically. The client reads the payment details, constructs a payment, and replays the request.

A second 402: the payment failed to settle

There are two different 402s, and only the one above is routine. If your paid retry comes back 402 as well, the payment header verified but the transfer could not settle on-chain. That response carries the standard x402 settlement-failure body instead of a new challenge:
Tell them apart by body, not status: a challenge has accepts; a settlement failure has success: false. Branch on errorReason; errorMessage is prose and x_laso_guidance is a Laso-specific hint when one applies. The most common cause is an underfunded wallet, and the usual reason for that is the fee. Laso fees are added on top of the amount you request, so a wallet holding exactly $2,000 cannot send a $2,000 bank payment. That request costs $2,005.00. Compare the challenge’s amount field (the fee-inclusive total, in atomic units) against your balance, then retry with a smaller amount. Nothing is charged when settlement fails, so retrying is safe. Note that GET /auth also returns 402 when a SIGN-IN-WITH-X header is present but fails verification (invalid or expired signature, reused nonce, domain mismatch). It does not return 401 for those failures. The 402 response carries a fresh challenge in the PAYMENT-REQUIRED response header, so sign the new challenge and retry. If you keep receiving 402 after sending a SIGN-IN-WITH-X header, treat it as a signature failure rather than retrying with the same payload.

400 Bad Request

Invalid or missing parameters.
Common causes:
  • Missing amount query parameter on /get-card
  • Amount below $5 or above $1,000
  • Missing grant_type or refresh_token in POST /auth request body
  • Invalid format parameter (must be json or html)
How to handle: Check the error message and fix the request parameters.

401 Unauthorized

Token is missing, expired, or invalid.
Common causes:
  • id_token has expired (tokens last ~1 hour)
  • Malformed Authorization header
  • Using a revoked refresh token
How to handle: Refresh your token using POST /auth with grant_type: "refresh_token". If that also returns 401, re-authenticate via GET /auth.

403 Forbidden

You’re authenticated but not authorized for this resource.
Common causes:
  • Trying to access a card that belongs to a different user
  • Using a token from one wallet to access another wallet’s data
  • The account is frozen (the response includes a frozen_message field explaining why)
How to handle: Ensure you’re using the correct token for the card you’re trying to access. Each wallet address has its own cards. For frozen accounts, contact support.

404 Not Found

The requested resource doesn’t exist.
Common causes:
  • Invalid card_id in /get-card-data
  • Typo in the card ID
How to handle: Verify the card_id from the original /get-card response.

429 Too Many Requests

A rate limit was exceeded.
Common causes:
  • Calling POST /refresh-card-data for the same U.S. card less than 5 minutes after the previous refresh
  • Requesting more than 12 refreshes for the same U.S. card in any rolling 24-hour period
  • Calling POST /signup more than 3 times in a minute or 20 times in an hour from one IP address
How to handle: Read retry_after_seconds from the body, or the Retry-After response header, which carries the same number. Wait that long, then retry once. Both fields are computed from the limit that actually rejected you, so there is no need to parse the message or guess a backoff. Do not retry in a tight loop.

Pacing yourself before you hit a limit

Every response carries the current request budget, so you can slow down before anything rejects you: The same values are repeated as X-RateLimit-* for clients that only parse that spelling. Most routes advertise the service-wide ceiling. POST /signup enforces its own per-IP budget on top of it and overwrites these headers with its own numbers, so there they describe the limit you are actually up against. POST /refresh-card-data is the exception: its limits are per card rather than per caller, so the headers keep their service-wide values and the per-card budget is reported only when it rejects, through Retry-After and retry_after_seconds.

500 Internal Server Error

Something went wrong on the server.
How to handle: Retry after a few seconds. If it persists, contact support@laso.finance with the request details.

Error handling pattern

Here’s a robust error handling pattern for agent code: