Errors & Request IDs - cred.diy API documentation
Errors & Request IDs
Every error is JSON with a human-readable error, a machine-readable code where one applies, any fields the code documents, and the call's request_id. Branch on code (and on error_code for 402s), never on the message text.
Request IDs
Every response carries an X-Request-Id header (exposed to browsers via CORS), and every JSON error body (status 400 and up) includes the same value as request_id. Send your own X-Request-Id (8–128 characters: letters, digits, . _ : -) to trace a call end to end; without one (or with a malformed one) a UUID is generated. Quote it when you contact support so the call can be found in our logs. The SDK returns it as requestId on every response and error.requestId on failures, and takes { requestId } in per-call options.
HTTP/1.1 400 Bad Request
X-Request-Id: 6f1c2e4a-9b7d-4c1e-8a2f-3d5b7e9f1a2c
{ "error": "Insufficient credits", "code": "INSUFFICIENT_CREDITS", "requested_amount": 100,
"request_id": "6f1c2e4a-9b7d-4c1e-8a2f-3d5b7e9f1a2c" }HTTP statuses
| 400 | Invalid parameters or JSON, or a business rule (INSUFFICIENT_CREDITS, NO_FIELDS_TO_UPDATE, SWAP_NOT_BIDIRECTIONAL, …) |
| 401 | Missing, unknown, disabled or expired API key (INVALID_API_KEY, API_KEY_DISABLED, API_KEY_EXPIRED) |
| 402 | Out of CRD (PLATFORM_CREDITS_EXHAUSTED, payable via x402 when enabled), or Stripe refused money (STRIPE_REFUND_FAILED, STRIPE_TRANSFER_FAILED, CARD_DECLINED) |
| 403 | Platform Stripe Connect blocked for the project (AUP_NOT_ACCEPTED, AUP_VERSION_STALE, CATEGORY_RESTRICTED) |
| 404 | User or resource not found; an unknown endpoint lists available_endpoints |
| 409 | Conflict: IDEMPOTENCY_KEY_REUSED, TIER_IN_USE, USER_HAS_HISTORY, ALREADY_REFUNDED, HOLD_NOT_ACTIVE |
| 429 | RATE_LIMITED — free (no CRD charged); back off until X-RateLimit-Reset |
| 500 | Internal error — quote the request_id to support |
| 502 | Stripe's outcome unknown: PAYOUT_PENDING (/redeem) or charge_not_started (/charge-saved) — retry with the same idempotency key |
Authentication, metering and rate limits
| Code | Status | Meaning |
|---|---|---|
| INVALID_API_KEY | 401 | Missing, malformed (not sk_live_) or unknown API key. (every API-key call) |
| API_KEY_DISABLED | 401 | The key was disabled on the API Keys page. (every API-key call) |
| API_KEY_EXPIRED | 401 | The key's expiration date has passed. Create a new key; expiry is enforced. (every API-key call) |
| RATE_LIMITED | 429 | Over 100 requests/minute for this key (30/minute per IP on the public x402 routes). Checked before the 1 CRD charge, so a 429 costs nothing. Retry after X-RateLimit-Reset (unix seconds). (every API-key call) |
| PLATFORM_CREDITS_EXHAUSTED | 402 | Your CRD (platform credit) balance is spent. Sent as both code and error_code, with credits_remaining; when x402 is enabled the 402 also carries a PAYMENT-REQUIRED offer you can pay and retry the same request with. (every API-key call) |
| AUP_NOT_ACCEPTED / AUP_VERSION_STALE / CATEGORY_RESTRICTED | 403 | The project has not accepted the current acceptable-use policy, or its business category is restricted on the platform account. (checkout and charges on platform Stripe Connect) |
Idempotency
| Code | Status | Meaning |
|---|---|---|
| INVALID_IDEMPOTENCY_KEY | 400 | The idempotency key is longer than 255 characters. (any write with a key) |
| IDEMPOTENCY_KEY_REUSED | 409 | This key was already used for a different request. A retry of the same request returns the first result (idempotent_replay: true) instead. (/issue, /deduct, /refund, /swap, /redeem, /escrow/hold, /charge-saved) |
| IDEMPOTENCY_KEY_REQUIRED | 400 | Off-session charges need an Idempotency-Key header (or idempotency_key). (/charge-saved) |
Credits, users and pricing tiers
| Code | Status | Meaning |
|---|---|---|
| INSUFFICIENT_CREDITS | 400 | The user's balance is too low. /deduct and /redeem include requested_amount; /swap includes required. Not 422. (/deduct, /escrow/hold, /redeem, /swap, /refund (clawback)) |
| INVALID_CREDIT_TYPE | 400 | credit_type_id is not an active credit type in this project. (/issue, /deduct, /escrow/hold, referral settings) |
| USER_NOT_FOUND | 404 | No user with that user_email / user_id (external_id) in this project. Only /issue and POST /users create users. (user-scoped endpoints) |
| NO_FIELDS_TO_UPDATE | 400 | The body has none of email, name, metadata, signup_url, signup_host, signup_credit_type_id, wallet_address. (PATCH /users) |
| USER_HAS_HISTORY | 409 | The user has swap history, kept as a financial record, so the user cannot be deleted (swap_transactions gives the count). (DELETE /users) |
| TIER_IN_USE | 409 | Auto top-up rules buy this tier (auto_topup_rules gives the count). Move them, or retry with force: true to delete the rules and the tier in one transaction. (DELETE /pricing-tiers) |
| tier_not_one_time | 400 | The tier is recurring-only; saved-card charges need a one-time (or both) tier. (/charge-saved, POST /auto-topup) |
Escrow holds
| Code | Status | Meaning |
|---|---|---|
| HOLD_NOT_FOUND | 404 | No hold with that id in this project. (/escrow/release, /escrow/capture) |
| HOLD_NOT_ACTIVE | 409 | The hold was already released or captured. (/escrow/release, /escrow/capture) |
| CAPTURE_EXCEEDS_HELD | 400 | The capture amount is more than the hold. (/escrow/capture) |
| INVALID_AMOUNT | 400 | amount must be a positive number. (/escrow/*) |
Swaps
| Code | Status | Meaning |
|---|---|---|
| SWAP_NOT_BIDIRECTIONAL | 400 | direction "reverse" on a one-way pair. Enable Bidirectional on the pair to swap target → source. (/swap) |
| INVALID_DIRECTION | 400 | direction must be "forward" or "reverse". (/swap) |
Refunds
| Code | Status | Meaning |
|---|---|---|
| PURCHASE_NOT_FOUND | 404 | No purchase with that purchase_id / stripe_payment_intent_id in this project. (/refund) |
| TRANSACTION_NOT_FOUND | 404 | No transaction with that transaction_id / reference_id in this project. (/refund) |
| ALREADY_REFUNDED | 409 | The source was already refunded in full. (/refund) |
| PURCHASE_NOT_REFUNDABLE | 400 | Only completed purchases can be refunded. (/refund) |
| NOT_REFUNDABLE | 400 | Redeem and escrow transactions have their own reversal flows and cannot be refunded here. (/refund) |
| REFUND_EXCEEDS_ORIGINAL | 400 | The amount is more than what is left to refund. (/refund) |
| STRIPE_REFUND_FAILED | 402 | Stripe refused the money refund; no credits moved. (/refund) |
Cash payouts (redeem)
| Code | Status | Meaning |
|---|---|---|
| NOT_REDEEMABLE | 400 | The credit type is not marked Redeemable for Cash. (/redeem) |
| PAYOUT_TOO_SMALL | 400 | amount × exchange_rate rounds to zero. (/redeem) |
| INVALID_DESTINATION | 400 | destination is not this user's own payout account from /onboard-payout (or they have none). (/redeem) |
| STRIPE_KEYS_MISSING | 400 | The project has no Stripe secret key under Payments → Your Own Keys. (/redeem, /onboard-payout) |
| STRIPE_TRANSFER_FAILED | 402 | Stripe refused the transfer; the credits were returned (new_balance). (/redeem) |
| PAYOUT_PENDING | 502 | Stripe's answer was lost; the credits stay deducted. Retry with the returned idempotency_key to finish it — it never pays twice. If you never retry, the reconciliation sweep settles it after 15 minutes (paid, or credits returned only if Stripe definitively refuses). A claim the sweep cannot settle safely is flagged needs_review for manual review instead of being paid or refunded; retries keep answering PAYOUT_PENDING until then. (/redeem) |
Saved cards (POST /charge-saved, auto top-up)
| Code | Status | Meaning |
|---|---|---|
| CARD_DECLINED | 402 | Sent as error_code. code is Stripe's (e.g. card_declined), with decline_code, message and purchase_id. (/charge-saved) |
| CARD_AUTHENTICATION_REQUIRED | 402 | Sent as error_code with requires_action: true — the bank wants the buyer present (3-D Secure). Send them through hosted checkout. (/charge-saved) |
| no_saved_payment_method | 404 | The user has no saved card. (DELETE /payment-method answers 200 { deleted: false } with this code instead.) (/charge-saved) |
| saved_payment_method_unusable | 409 | The card was saved on a Stripe account the project no longer charges through. The buyer must save a card again at checkout. (/charge-saved) |
| charge_not_started | 502 | Stripe could not be reached to start the charge. Retry with the same idempotency key; it never charges twice. (/charge-saved) |
Referrals
| Code | Status | Meaning |
|---|---|---|
| INVALID_REFERRAL_CODE | 400 | No referrer has this code. (/referrals/apply) |
| ALREADY_REFERRED | 400 | The user already used a referral code. (/referrals/apply) |
| SELF_REFERRAL | 400 | A user cannot use their own code. (/referrals/apply) |
Own-keys Stripe webhook (payment-webhook, seen in Stripe's delivery log)
| Code | Status | Meaning |
|---|---|---|
| MISSING_SIGNATURE | 400 | The delivery has no Stripe-Signature header. (payment-webhook) |
| INVALID_PROJECT | 400 | ?project_id in the endpoint URL is not a valid project id. (payment-webhook) |
| WEBHOOK_SECRET_NOT_CONFIGURED | 400 | No signing secret saved for this project. Save the endpoint's whsec_ secret under Payments → Your Own Keys. (payment-webhook) |
| INVALID_SIGNATURE | 400 | The signature does not match the saved signing secret (wrong or rotated whsec_). (payment-webhook) |
SDK-side (no HTTP response)
| Code | Status | Meaning |
|---|---|---|
| TIMEOUT | — | The request exceeded the client timeout (default 30 s). (@cred-diy/sdk) |
| NETWORK_ERROR | — | fetch failed, or no fetch implementation is available. (@cred-diy/sdk) |
| INVALID_RESPONSE | — | The API answered 2xx with a body that is not JSON. (@cred-diy/sdk) |
Your first credit is one request away.
Create a project, copy an API key, and issue a credit from your server.
