6 min read
On this page

Error Codes & Documentation

Machine-readable error codes are the interface between your API and your consumers' error-handling logic. Developers do not parse human-readable messages programmatically. They switch on error codes. When your code says insufficient_funds, a client can show a "please add funds" dialog. When it says card_declined, a client can prompt for a different payment method.

Error codes are part of your API contract. Changing them is a breaking change. Documenting them is not optional.

Designing Error Codes

Use Descriptive Snake Case

Error codes should be lowercase strings with underscores, readable by both humans and machines:

insufficient_funds
rate_limited
resource_not_found
invalid_api_key
card_declined
permission_denied
idempotency_conflict

Avoid numeric error codes. E10042 means nothing without a lookup table. insufficient_funds is self-documenting.

Avoid overly generic codes. error and invalid tell the developer nothing. Be specific about what went wrong.

{
  "error": {
    "code": "insufficient_funds",
    "message": "Your account balance of $5.00 is insufficient for this $20.00 charge.",
    "request_id": "req_abc123"
  }
}

Organize Codes by Category

Group error codes into logical categories. Stripe uses a two-level hierarchy:

Type: invalid_request_error
  Codes: parameter_missing, parameter_invalid, parameter_unknown

Type: card_error
  Codes: card_declined, expired_card, incorrect_cvc, processing_error

Type: authentication_error
  Codes: invalid_api_key, expired_api_key

Type: rate_limit_error
  Codes: rate_limited

Type: api_error
  Codes: internal_error

The type tells the developer what category of error occurred. The code tells them exactly what happened within that category. Client code can handle these at different levels of granularity:

if error.type == "card_error":
    # Show payment-related error message to user
    if error.code == "card_declined":
        suggest_different_card()
    elif error.code == "expired_card":
        suggest_update_card()
elif error.type == "rate_limit_error":
    # Retry with backoff
    retry_with_backoff()
elif error.type == "authentication_error":
    # Check API key configuration
    log_authentication_failure()

Keep Codes Stable

Error codes are a contract. Once you publish insufficient_funds, you cannot rename it to balance_too_low without breaking every consumer that handles insufficient_funds.

When you need to add specificity, add new codes rather than changing existing ones:

Before: card_declined
After:  card_declined (still works)
        card_declined_insufficient_funds (new, more specific)
        card_declined_lost_card (new, more specific)

Stripe handles this with decline_code as a sub-code of card_declined:

{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "message": "Your card has insufficient funds."
  }
}

Consumers that only check code still work. Consumers that need more detail can check decline_code.

Error Code Documentation

Every error code in your API needs documentation. Not a sentence — a complete entry that answers three questions.

What Does This Error Mean?

## insufficient_funds

**HTTP Status:** 402 Payment Required
**Type:** card_error

The charge amount exceeds the available balance on the payment method.
This typically occurs when a customer's bank account or prepaid card
does not have enough funds to cover the transaction.

When Does This Error Happen?

**Triggers:**
- Attempting to create a charge that exceeds the card's available balance
- Attempting to capture a payment intent where the hold has been
  partially refunded by the bank
- Recurring subscription charges where the balance has been depleted
  since the last successful charge

How Does the Developer Fix It?

**Resolution:**
- Prompt the customer to use a different payment method
- Reduce the charge amount
- For subscription charges, send the customer a notification to
  update their payment method before the next billing cycle

**Example handling:**
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "message": "Your card has insufficient funds.",
    "param": "source",
    "doc_url": "https://docs.example.com/errors/insufficient-funds"
  }
}

Include a doc_url field in every error response that links to the specific documentation page for that error code:

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Please retry after 30 seconds.",
    "doc_url": "https://docs.example.com/errors/rate-limited",
    "request_id": "req_abc123"
  }
}

When a developer encounters this error, they can click the link and immediately find documentation about rate limits, retry strategies, and how to request a higher limit.

Stripe's Error Code Catalog

Stripe maintains a public error code catalog at stripe.com/docs/error-codes. It is the gold standard for error documentation.

Each entry includes:

  • Error code — the machine-readable string
  • HTTP status — the status code returned
  • Description — what the error means
  • Common causes — when developers encounter this error
  • Resolution — how to fix it
  • Related — links to related errors and guides

Example from Stripe's catalog:

## card_declined

HTTP Status: 402

The card has been declined. The customer needs to contact their card
issuer for more information.

Decline codes:
- insufficient_funds: The card has insufficient funds
- lost_card: The card has been reported lost
- stolen_card: The card has been reported stolen
- generic_decline: The card was declined for an unknown reason

Resolution:
- Ask the customer to contact their bank
- Suggest trying a different card
- For recurring charges, notify the customer to update their
  payment method

Stripe's catalog is searchable, organized by category, and cross-referenced with API endpoints. It turns a frustrating debugging session into a 30-second lookup.

Building Your Error Code Registry

Centralize Error Definitions

Define all error codes in a single source of truth. This prevents different services from inventing conflicting codes.

{
  "error_codes": {
    "resource_not_found": {
      "status": 404,
      "category": "client_error",
      "message_template": "The requested {resource_type} was not found.",
      "description": "The specified resource does not exist or has been deleted.",
      "resolution": "Verify the resource ID. Check that the resource has not been deleted."
    },
    "rate_limited": {
      "status": 429,
      "category": "rate_limit",
      "message_template": "Rate limit exceeded. Retry after {retry_after} seconds.",
      "description": "You have exceeded the allowed number of requests per time window.",
      "resolution": "Implement exponential backoff. Contact support to request a higher limit."
    },
    "insufficient_funds": {
      "status": 402,
      "category": "payment_error",
      "message_template": "Account balance of {balance} is insufficient for {amount}.",
      "description": "The payment method does not have enough funds for the transaction.",
      "resolution": "Use a different payment method or reduce the transaction amount."
    }
  }
}

This registry serves multiple purposes:

  • API servers consume it to generate error responses with correct status codes and messages
  • Documentation generators consume it to produce the error code catalog
  • Client SDKs consume it to generate typed error constants
  • Monitoring consumes it to categorize errors in dashboards

Generate Documentation Automatically

With a centralized error registry, documentation stays in sync with the API automatically. When a developer adds a new error code to the registry, the documentation builds include it in the next deploy.

Manual documentation inevitably drifts from reality. Automated documentation cannot.

Version Your Error Codes

Track when error codes were introduced and when they were deprecated:

{
  "insufficient_funds": {
    "introduced": "2023-01-15",
    "status": "active"
  },
  "balance_insufficient": {
    "introduced": "2021-03-01",
    "deprecated": "2023-01-15",
    "replaced_by": "insufficient_funds",
    "status": "deprecated"
  }
}

Error Codes in SDKs

When you provide client SDKs, expose error codes as typed constants:

// Python SDK
from myapi.errors import InsufficientFundsError, RateLimitedError

try:
    charge = client.charges.create(amount=2000, currency="usd")
except InsufficientFundsError as e:
    notify_customer_low_balance(e.message)
except RateLimitedError as e:
    retry_after(e.retry_after)

Typed error classes give developers compile-time checking (in typed languages) and IDE autocomplete. They eliminate string comparisons against error codes.

Stripe's SDKs in every language provide typed error classes that map to their error code catalog. Python has stripe.error.CardError, Ruby has Stripe::CardError, and so on.

Error Code Lifecycle

Adding New Codes

New error codes are additive changes. They do not break existing consumers because existing consumers never receive the new code from existing endpoints. However, if a new code replaces an existing code on an existing endpoint, that is a breaking change.

Deprecating Codes

Deprecate error codes the same way you deprecate endpoints:

  1. Add the new code alongside the old one
  2. Document the deprecation and the replacement
  3. Monitor usage of the old code
  4. Remove the old code after the grace period

Never Change Code Meanings

If rate_limited means "too many requests per second," do not expand it to also mean "too many concurrent connections." Create a new code concurrent_limit_exceeded. Consumers who handle rate_limited have specific retry logic that may not apply to concurrent connection limits.

Common Pitfalls

  • Numeric error codes — using 10042 instead of insufficient_funds. Numbers require a lookup table and are meaningless in logs and debugging.
  • Unstable codes — renaming error codes between API versions. Error codes are part of the contract; renaming them breaks client error-handling logic.
  • No documentation — publishing error codes without explaining what they mean, when they occur, and how to fix them. Undocumented error codes are useless.
  • No doc_url — forcing developers to search your documentation site for error information. Link directly from the error response to the relevant documentation page.
  • Too few codes — using invalid_request for every client error. Developers cannot write specific error-handling logic without specific error codes.
  • Too many codes — creating dozens of codes for edge cases that all require the same client-side handling. Group related errors under broader codes with sub-codes for detail.
  • Codes that leak internals — using postgres_unique_constraint_violation as an error code. Expose the business meaning (already_exists), not the implementation detail.

Key Takeaways

  • Error codes are machine-readable strings that clients use for programmatic error handling. Use descriptive snake_case: insufficient_funds, not E10042.
  • Every error code needs documentation answering three questions: what does it mean, when does it happen, and how do I fix it. Stripe's error code catalog is the model to follow.
  • Error codes are part of your API contract. Changing or removing them is a breaking change. Add new codes rather than modifying existing ones.
  • Include a doc_url in every error response that links directly to the documentation for that specific error code.
  • Centralize error code definitions in a single registry. Generate documentation and SDK error classes from this registry to prevent drift.
  • Expose typed error classes in SDKs so developers get IDE autocomplete and compile-time checking instead of string comparisons against error codes.