# Errors and testing

> The Soca error envelope and every error code, SocaError in the SDK, test mode isolation, idempotency keys, cursor pagination, and how to handle rate limits.

Part of the Soca documentation (https://soca.finance/docs). Human version: https://soca.finance/docs/developers/errors-and-testing
## The error envelope

Every API error is JSON in one shape: a machine-readable `code` and a human-readable `message` that names the offending field where relevant:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "description is required."
  }
}
```

## Error codes

| HTTP | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthorized` | Missing, malformed, invalid, or revoked API key. See [Authentication](/docs/developers/authentication). |
| 400 | `invalid_json` | The request body is not valid JSON. |
| 400 | `invalid_request` | The body failed validation. The message says which field and why. |
| 400 | `mode_mismatch` | The key's mode (test/live) does not match the environment the request was sent to. |
| 400 | `amount_too_small` | Below the minimum. Plans require at least 1.00 USDC (`1000000` base units). |
| 400 | `invalid_interval` | The plan interval / `interval_days` combination is not valid. |
| 400 | `missing_value` | The meter is not count-aggregated, so the event needs a numeric `value`. |
| 400 | `customer_required` | Credit grants/debits and entitlement grants need `customer_id` or `external_customer_id`. |
| 400 / 404 | `unknown_customer` | No such customer in this account. 400 when referenced in a request body; 404 when it is the `{id}` in a URL path. |
| 404 | `unknown_meter` | No meter with that `event_name`. Create it in the dashboard first. |
| 404 | `unknown_feature` | No feature with that `feature_key` in this account. |
| 404 | `no_credit_account` | The customer has no credit account yet. Grant credits before debiting. |
| 404 | `resource_not_found` | Unknown `/v1/{resource}`. Valid list resources: payments, subscriptions, invoices, events, meters. |
| 405 | `method_not_allowed` | POST to a read-only resource; only `invoices` and `plans` are creatable. |
| 429 | `rate_limited` | Too many requests. See the limits table in [Authentication](/docs/developers/authentication) and the handling advice below. |

## SocaError in the SDK

The [SDK](/docs/developers/sdk) turns every non-2xx response into a thrown `SocaError` with the envelope's `code` and the HTTP `status`:

```typescript
import { SocaError } from '@usesoca/sdk';

try {
  await soca.meterEvents.create({ event_name: 'api_call', external_customer_id: 'user_812' });
} catch (err) {
  if (err instanceof SocaError && err.code === 'unknown_meter') {
    // The meter isn't defined yet. Create it in the dashboard.
  } else {
    throw err;
  }
}
```

## Test mode

Your account has two parallel environments, selected purely by key prefix: `sk_test_` and `sk_live_`. Everything is isolated per mode: invoices, plans, payments, customers, meters, credits, entitlements, events, webhooks, and integrations. Build the whole flow against a test key, walk a test checkout end to end, watch the test webhooks arrive, then swap in a live key, no other change is needed, and test data can never contaminate live reports.

## Idempotency keys

The endpoints that get retried under load accept an idempotency key in one of two places: an `idempotency_key` field in the body or an `Idempotency-Key` request header.

- `POST /v1/meter-events`: per event (in a batch, use the body field on each event).
- `POST /v1/credit_grants` and `POST /v1/credit_debits`.

A replay with the same key returns `200` with `deduplicated: true` and does **not** double-count, double-grant, or double-debit, so on a timeout or a 5xx, simply retry with the same key. Keys can be up to 255 characters; derive them from something naturally unique in your system (a request ID, `"{customer}:{period}:{reason}"`, and so on).

## Pagination recipe

Every list endpoint returns `{ object, data, has_more, next_cursor }` and accepts `limit` (1–100, default 25) and `before` (an ISO timestamp cursor). To walk an entire collection, loop until `has_more` is false:

```typescript
const all = [];
let before: string | undefined;

do {
  const page = await soca.payments.list({ limit: 100, before });
  all.push(...page.data);
  before = page.next_cursor ?? undefined;
  if (!page.has_more) break;
} while (before);
```

## Handling rate limits

- A `429` response includes a `retry-after` header in seconds. Wait at least that long before retrying, and add jitter if many workers share the limit.
- For usage metering, prefer **batching**: one `POST /v1/meter-events` call carries up to 500 events, which turns the 600/min ingestion budget into hundreds of thousands of events per minute.
- Cache entitlement checks briefly (seconds to a minute) if you gate a hot code path. The check endpoint allows 600/min, but a short-lived local cache removes the ceiling entirely.
- Retries of idempotent writes are always safe when you reuse the idempotency key.
