Error codes
Every non-2xx response has the shape { success: false, error: { code, message } }. The code is stable — safe to branch on in client code. The message is human-readable and may change.
Authentication (401, 403)
| Code | HTTP | Meaning |
|---|
missing_header | 401 | Authorization header absent. |
malformed_key | 401 | Key prefix not recognized (expected pk_live_ / sk_live_ / pk_test_ / sk_test_). |
unknown_key | 401 | No matching key in the database. |
revoked | 401 | The key was revoked by an admin. |
scope_mismatch | 403 | This key doesn't have the scope required for the endpoint. |
app_mismatch | 403 | Key is locked to a different app than the one in the path. |
salt_unset | 500 | Server misconfiguration — API_KEY_SALT secret not set. |
Request validation (400, 401)
| Code | HTTP | Meaning |
|---|
invalid_payload | 400 | Zod validation failed. The error.message field explains what field. |
invalid_query | 400 | A query parameter is malformed. |
missing_user_id | 400 | An endpoint that requires user_id in the query did not receive one. |
empty_update | 400 | PATCH request had no fields to change. |
invalid_code | 401 | TOTP or backup code did not match. |
Resource (404, 400)
| Code | HTTP | Meaning |
|---|
app_not_found | 404 | The app does not exist or is owned by a different account. |
user_not_found | 404 | No user with this id under your account. |
experiment_not_found | 404 | Experiment id is unknown or paused. |
offering_missing | 404 | A variant references an offering that was deleted. |
no_variants | 404 | Experiment has no variants defined. |
webhook_not_found | 404 | Webhook endpoint id unknown. |
offering_not_found | 404 | Offering slug unknown for this app. |
setup_not_started | 400 | 2FA verify called before setup. |
already_enabled | 400 | 2FA setup called while it was already enabled. |
not_enabled | 400 | 2FA disable called when not enabled. |
Server errors (5xx)
| Code | HTTP | Meaning |
|---|
server_sync_failed | 502 | The upstream receipt validator (Apple/Google/Stripe) rejected the receipt or timed out. |
internal_error | 500 | Generic server error. Please retry with exponential backoff. |
Retrying
5xx errors and network timeouts should be retried with exponential backoff (1s, 5s, 30s, 2m, 8m, 24m — matching stubkit's own webhook retry schedule). 4xx errors are permanent; fix the request instead of retrying.
Rate limits (429)
429 responses include a Retry-After header in seconds. The X-RateLimit-Reset header on every response tells you the unix timestamp when your budget refills.