Browse docs

Developers

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.

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:

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

Error codes

HTTPCodeMeaning
401unauthorizedMissing, malformed, invalid, or revoked API key. See Authentication.
400invalid_jsonThe request body is not valid JSON.
400invalid_requestThe body failed validation. The message says which field and why.
400mode_mismatchThe key's mode (test/live) does not match the environment the request was sent to.
400amount_too_smallBelow the minimum. Plans require at least 1.00 USDC (1000000 base units).
400invalid_intervalThe plan interval / interval_days combination is not valid.
400missing_valueThe meter is not count-aggregated, so the event needs a numeric value.
400customer_requiredCredit grants/debits and entitlement grants need customer_id or external_customer_id.
400 / 404unknown_customerNo such customer in this account. 400 when referenced in a request body; 404 when it is the {id} in a URL path.
404unknown_meterNo meter with that event_name. Create it in the dashboard first.
404unknown_featureNo feature with that feature_key in this account.
404no_credit_accountThe customer has no credit account yet. Grant credits before debiting.
404resource_not_foundUnknown /v1/{resource}. Valid list resources: payments, subscriptions, invoices, events, meters.
405method_not_allowedPOST to a read-only resource; only invoices and plans are creatable.
429rate_limitedToo many requests. See the limits table in Authentication and the handling advice below.

SocaError in the SDK

The SDK turns every non-2xx response into a thrown SocaError with the envelope's code and the HTTP status:

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:

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.

Was this page helpful?

A quick signal helps us improve the right pages.