> ## Documentation Index
> Fetch the complete documentation index at: https://agents.laso.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling: HTTP status codes and retries

> Reference for Laso Finance API status codes, x402 payment errors, and strategies to recover from rate limits, insufficient funds, and timeouts.

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

```json theme={null}
{
  "x402Version": 1,
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "maxAmountRequired": "5000000",
      "resource": "https://laso.finance/get-card?amount=5",
      "description": "Get Laso Finance prepaid card",
      "payTo": "0x3291e96b3bff7ed56e3ca8364273c5b4654b2b37"
    }
  ]
}
```

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

```json theme={null}
{
  "success": false,
  "errorReason": "insufficient_funds",
  "errorMessage": "the transfer could not be settled on-chain",
  "payer": "9sZEFeQDPyjjFjZM9jK6i6L5eWMCrTHB7pCWjU1k9WXR",
  "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
  "x_laso_guidance": "…the concrete next step…"
}
```

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

```json theme={null}
{
  "error": "Amount must be at least $5. Received: $2"
}
```

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

```json theme={null}
{
  "error": "Invalid or expired token"
}
```

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

```json theme={null}
{
  "error": "Not authorized to view this card"
}
```

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

```json theme={null}
{
  "error": "Card not found"
}
```

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

```json theme={null}
{
  "error": "Daily card refresh limit reached. You can request at most 12 refreshes for a card in a 24-hour period. Please try again later.",
  "code": "rate_limited",
  "retry_after_seconds": 3142,
  "hint": "This limit is per card: at most one refresh every 5 minutes, and 12 in any rolling 24-hour period. Wait retry_after_seconds and try again, or read the cached balance with GET /get-card-data in the meantime."
}
```

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

| Header                | Meaning                                                 |
| --------------------- | ------------------------------------------------------- |
| `RateLimit-Limit`     | Requests permitted per window                           |
| `RateLimit-Remaining` | Requests still available                                |
| `RateLimit-Reset`     | Seconds until the window rolls over                     |
| `RateLimit-Policy`    | The policy these numbers describe, as `limit;w=seconds` |

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.

```json theme={null}
{
  "error": "Failed to order card. Please contact support."
}
```

**How to handle:** Retry after a few seconds. If it persists, contact [support@laso.finance](mailto:support@laso.finance) with the request details.

## Error handling pattern

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

```typescript theme={null}
async function safeApiCall(fn) {
  try {
    return await fn();
  } catch (error) {
    const status = error.response?.status;
    const message = error.response?.data?.error;

    switch (status) {
      case 400:
        console.error("Bad request:", message);
        throw new Error(`Invalid request: ${message}`);
      case 401:
        console.log("Token expired, refreshing...");
        await refreshTokens();
        return await fn(); // Retry with new token
      case 403:
        console.error("Not authorized:", message);
        throw new Error(`Access denied: ${message}`);
      case 404:
        console.error("Not found:", message);
        throw new Error(`Resource not found: ${message}`);
      default:
        console.error(`Unexpected error (${status}):`, message);
        throw error;
    }
  }
}
```
