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"
}
}
Link from Error Responses to Documentation
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:
- Add the new code alongside the old one
- Document the deprecation and the replacement
- Monitor usage of the old code
- 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
10042instead ofinsufficient_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_requestfor 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_violationas 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, notE10042. - 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_urlin 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.