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

# Errors

Error handling for the AnySpend Platform API

The AnySpend Platform API uses a consistent, structured error format across all endpoints. Errors follow the Stripe convention -- a JSON object with an `error` key containing the type, code, human-readable message, and (where applicable) the parameter that caused the error.

## Error response format

Every error response has this shape:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_required_field",
    "message": "The 'name' field is required.",
    "param": "name"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `type` | `string` | The category of error. Use this for broad error handling logic. |
| `code` | `string` | A machine-readable error code. Use this for specific error handling. |
| `message` | `string` | A human-readable description of what went wrong. Safe to display to developers (but not end users). |
| `param` | `string` or absent | The request parameter that caused the error, if applicable. |

## Error types

Error types group errors into broad categories. Use these for top-level error handling.

| Type | HTTP status | Description |
|------|-------------|-------------|
| `invalid_request_error` | `400` | The request was malformed or contained invalid parameters. Check the `param` field for which field caused the issue. |
| `authentication_error` | `401` | The API key is missing, invalid, expired, or revoked. |
| `permission_error` | `403` | The API key is valid but lacks the required permission level for the requested operation. |
| `not_found_error` | `404` | The requested resource does not exist, or does not belong to your organization. |
| `rate_limit_error` | `429` | You have exceeded the rate limit for your authentication tier. |
| `idempotency_error` | `409` | An idempotency key was reused with a different request body. |
| `api_error` | `500` | An unexpected internal error occurred on our side. These are rare and are automatically reported. |

## Error codes

Error codes provide specific, machine-readable identifiers for each error condition.

### Request validation errors

| Code | Type | HTTP | Description |
|------|------|------|-------------|
| `missing_required_field` | `invalid_request_error` | `400` | A required field was not included in the request body. The `param` field indicates which field is missing. |
| `invalid_field_value` | `invalid_request_error` | `400` | A field was included but its value is invalid (wrong type, format, or out of range). |
| `invalid_address` | `invalid_request_error` | `400` | An Ethereum address field is not a valid `0x` + 40 hex character address. |
| `no_updates_provided` | `invalid_request_error` | `400` | A `PATCH` request was sent with no valid fields to update. |
| `invalid_status_transition` | `invalid_request_error` | `400` | The requested status change is not allowed (e.g., completing an already-expired session). |
| `duplicate_resource` | `invalid_request_error` | `409` | A resource with a conflicting unique field already exists. |

### Authentication and authorization errors

| Code | Type | HTTP | Description |
|------|------|------|-------------|
| `key_missing` | `authentication_error` | `401` | No API key was found in the `Authorization` or `X-API-Key` header. |
| `key_invalid` | `authentication_error` | `401` | The provided API key does not match any key in the system. |
| `key_expired` | `authentication_error` | `401` | The API key has passed its `expires_at` timestamp. Create a new key or extend the expiration. |
| `key_revoked` | `authentication_error` | `401` | The API key was explicitly revoked. Create a new key. |
| `insufficient_permissions` | `permission_error` | `403` | The API key does not have the permission level required for this operation (e.g., a `read` key attempting a `POST`). |

### Resource errors

| Code | Type | HTTP | Description |
|------|------|------|-------------|
| `resource_not_found` | `not_found_error` | `404` | The requested resource does not exist, or it belongs to a different organization. |

### Rate limiting and idempotency errors

| Code | Type | HTTP | Description |
|------|------|------|-------------|
| `rate_limit_exceeded` | `rate_limit_error` | `429` | Too many requests in the current time window. The error message includes the retry delay. |
| `idempotency_conflict` | `idempotency_error` | `409` | An `Idempotency-Key` header was reused with a different request body than the original request. |

### Server errors

| Code | Type | HTTP | Description |
|------|------|------|-------------|
| `internal_error` | `api_error` | `500` | An unexpected server error. These are automatically tracked. If this persists, contact support. |

## HTTP status codes

| Status | Meaning |
|--------|---------|
| `200` | Success -- the resource was retrieved or updated |
| `201` | Created -- a new resource was successfully created |
| `400` | Bad Request -- the request was invalid |
| `401` | Unauthorized -- authentication failed |
| `403` | Forbidden -- the API key lacks the required permission |
| `404` | Not Found -- the resource does not exist |
| `409` | Conflict -- idempotency conflict or duplicate resource |
| `429` | Too Many Requests -- rate limit exceeded |
| `500` | Internal Server Error -- something went wrong on our end |

## Example error responses

### Missing required field

```bash
curl -X POST https://platform-api.anyspend.com/api/v1/payment-links \
  -H "Authorization: Bearer asp_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "amount": "10000000" }'
```

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_required_field",
    "message": "The 'name' field is required.",
    "param": "name"
  }
}
```

### Invalid Ethereum address

```bash
curl -X POST https://platform-api.anyspend.com/api/v1/payment-links \
  -H "Authorization: Bearer asp_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test Link",
    "token_address": "not-an-address",
    "chain_id": 8453,
    "recipient_address": "0xabc123..."
  }'
```

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_address",
    "message": "The 'token_address' field must be a valid Ethereum address (0x + 40 hex characters).",
    "param": "token_address"
  }
}
```

### Authentication failure

```bash
curl https://platform-api.anyspend.com/api/v1/payment-links \
  -H "Authorization: Bearer asp_live_expired_key..."
```

```json
{
  "error": {
    "type": "authentication_error",
    "code": "key_expired",
    "message": "This API key has expired."
  }
}
```

### Insufficient permissions

```bash
# A read-only key trying to create a resource
curl -X POST https://platform-api.anyspend.com/api/v1/payment-links \
  -H "Authorization: Bearer asp_live_readonly_key..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "Test" }'
```

```json
{
  "error": {
    "type": "permission_error",
    "code": "insufficient_permissions",
    "message": "This API key does not have 'write' permission."
  }
}
```

### Resource not found

```bash
curl https://platform-api.anyspend.com/api/v1/payment-links/pl_nonexistent \
  -H "Authorization: Bearer asp_live_abc123..."
```

```json
{
  "error": {
    "type": "not_found_error",
    "code": "resource_not_found",
    "message": "No payment_link found with id 'pl_nonexistent'."
  }
}
```

### Rate limit exceeded

```json
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 42 seconds."
  }
}
```

### Idempotency conflict

```json
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_conflict",
    "message": "An idempotency key was used with a different request body."
  }
}
```

## Handling errors in code

### TypeScript / JavaScript

```typescript
async function createPaymentLink(data: PaymentLinkInput) {
  const response = await fetch(
    "https://platform-api.anyspend.com/api/v1/payment-links",
    {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.ANYSPEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    }
  );

  const body = await response.json();

  if (!response.ok) {
    const error = body.error;

    switch (error.type) {
      case "invalid_request_error":
        // Fix the request -- check error.param for the bad field
        console.error(`Invalid request: ${error.message} (field: ${error.param})`);
        break;

      case "authentication_error":
        // Check your API key -- it may be expired or revoked
        console.error(`Auth failed: ${error.message}`);
        break;

      case "permission_error":
        // Upgrade the API key permissions
        console.error(`Insufficient permissions: ${error.message}`);
        break;

      case "rate_limit_error":
        // Back off and retry
        console.warn(`Rate limited: ${error.message}`);
        await sleep(5000);
        return createPaymentLink(data); // retry
        break;

      case "not_found_error":
        console.error(`Not found: ${error.message}`);
        break;

      case "idempotency_error":
        // The idempotency key was reused with different data
        console.error(`Idempotency conflict: ${error.message}`);
        break;

      case "api_error":
        // Server error -- retry with exponential backoff
        console.error(`Server error: ${error.message}`);
        break;
    }

    throw new Error(`AnySpend API error: ${error.code} - ${error.message}`);
  }

  return body;
}
```

### Python

```python
import os
import requests
import time

def create_payment_link(data: dict) -> dict:
    response = requests.post(
        "https://platform-api.anyspend.com/api/v1/payment-links",
        headers={
            "Authorization": f"Bearer {os.environ['ANYSPEND_API_KEY']}",
            "Content-Type": "application/json",
        },
        json=data,
    )

    body = response.json()

    if not response.ok:
        error = body["error"]
        error_type = error["type"]

        if error_type == "rate_limit_error":
            time.sleep(5)
            return create_payment_link(data)  # retry

        if error_type == "authentication_error":
            raise PermissionError(f"Auth failed: {error['message']}")

        if error_type == "invalid_request_error":
            raise ValueError(
                f"Bad request on field '{error.get('param', 'unknown')}': "
                f"{error['message']}"
            )

        raise Exception(f"AnySpend API error: {error['code']} - {error['message']}")

    return body
```

## Best practices

**Always check the error type first**

Use `error.type` for broad control flow (e.g., retry on `rate_limit_error`, fail fast on `authentication_error`). Use `error.code` for specific handling within a type.

**Use the param field to build form validation**

When `error.param` is present, you can map it directly to a form field to show inline validation errors in your UI.

**Implement retry logic for transient errors**

`rate_limit_error` and `api_error` (500) are transient. Use exponential backoff when retrying:

```typescript
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err: any) {
      const isRetryable =
        err.status === 429 || err.status === 500;

      if (!isRetryable || attempt === maxRetries) throw err;

      const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  throw new Error("Unreachable");
}
```

**Never retry 4xx errors (except 429)**

Client errors like `400`, `401`, `403`, and `404` will not resolve on retry. Fix the request or credentials before retrying.

**Log the full error object for debugging**

Always log `error.type`, `error.code`, `error.message`, and `error.param` together. This gives you the full picture when debugging production issues.