Stripe-Inspired API Design Best Practices
A reference distillation of the design conventions behind Stripe's API — the most widely admired and copied JSON HTTP API in the industry. Contains 52 actionable rules across 8 categories, prioritised by how irreversibly a wrong decision cascades through every endpoint, every SDK, and every client integration. Each rule explains the WHY, shows incorrect-vs-correct code, and links to the canonical source.
When to Apply
Reach for this skill when:
- Designing a new JSON HTTP API or a new endpoint on an existing one
- Reviewing an API design proposal, OpenAPI spec, or PR that adds/changes endpoints
- Debugging an integration where the "wrong" shape of the API is causing client bugs
- Auditing an API for naming consistency, error shape uniformity, or compatibility risks
- Producing an API design report (the kind your inspector tool emits —
Critical / Warning / Suggestion / Positive) - Picking between two designs and looking for an authoritative source to back the choice
- Onboarding to API design — these are the canonical patterns to internalise first
The rules are general — they apply to any JSON HTTP API, not just APIs imitating Stripe. Triggers include "API design", "OpenAPI", "endpoint", "schema", "webhook", "idempotency", "pagination", "API versioning", and reviews of YAML/JSON spec files.
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Resource Modeling & Identifiers | CRITICAL | resource- |
| 2 | URL Structure & HTTP Semantics | CRITICAL | url- |
| 3 | Request & Response Format | HIGH | format- |
| 4 | Errors & Status Codes | HIGH | error- |
| 5 | Idempotency & Safe Retries | HIGH | idem- |
| 6 | Versioning & Backwards Compatibility | HIGH | ver- |
| 7 | Naming, Polymorphism & Metadata | MEDIUM-HIGH | naming- |
| 8 | Authentication, Webhooks & Search | MEDIUM-HIGH | ops- |
Earlier categories cascade harder: a wrong choice in resource modeling (numeric IDs, no object discriminator) propagates to every endpoint forever; a wrong choice in webhook event naming is a single category to fix.
Quick Reference
1. Resource Modeling & Identifiers (CRITICAL)
resource-prefixed-string-ids— Use Prefixed String IDs for Every Resourceresource-object-discriminator— Include a Read-OnlyobjectDiscriminator on Every Resourceresource-opaque-ids— Treat IDs as Opaque Strings up to 255 Charactersresource-unix-seconds-timestamps— Use Unix Seconds (Integer) for All Datetimesresource-iso-date-only— Use ISO 8601 Date Strings for Date-Only Valuesresource-birthdate-hash— Represent Birth Dates as{day, month, year}Hashesresource-integer-minor-units— Use Integer Minor Units for Money, Never Floatsresource-currency-field-not-name— Colocate acurrencyField; Never Bake Currency into Field Namesresource-decimal-suffix-strings— Use_decimalString Suffix for Precise Decimals That Can't Be Integers
2. URL Structure & HTTP Semantics (CRITICAL)
url-plural-collections— Pluralize Collection URLs; Singularize Object Typesurl-post-for-updates— Use POST for Updates (Not PUT or PATCH)url-action-verbs-as-subpaths— Express Non-CRUD Actions as Imperative Sub-Pathsurl-no-bulk-endpoints— One Object Per Request — No Bulk Endpointsurl-version-in-path-and-header— Version in URL Path andStripe-VersionHeaderurl-dedicated-search-endpoint— Use a Dedicated/searchEndpoint for Complex Queries
3. Request & Response Format (HIGH)
format-form-encoded-requests— Accept Form-Encoded Requests, Always Return JSONformat-bracket-notation-nesting— Use Bracket Notation for Nested Fields in Form Bodiesformat-list-envelope— Return Lists in a{object, url, has_more, data}Envelopeformat-cursor-pagination— Paginate by Cursor (starting_after/ending_before), Not by Offsetformat-no-total-counts— Usehas_moreBoolean; Never Return Total Countsformat-expand-parameter— Expand Related Objects withexpand[]in One Round Tripformat-dot-notation-expansion— Allow Dot-Notation for Nested Expansion (Max Depth 4)
4. Errors & Status Codes (HIGH)
error-top-level-object— Always Wrap Failures in a Top-LevelerrorObjecterror-four-type-enum— Use a Small FixedtypeEnum, Don't Proliferate Typeserror-message-mandatory-code-optional— Requiremessage; MakecodeOptional and Only for Programmatic Handlingerror-lowercase-snake-case-codes— Use Lowercase snake_case for Error Codes, Not SCREAMING_SNAKE_CASEerror-http-status-mapping— Map Error Types to HTTP Status Codes Consistentlyerror-doc-url-on-every-error— Includedoc_urlLinks and Request IDs on Every Error
5. Idempotency & Safe Retries (HIGH)
idem-key-header— AcceptIdempotency-KeyHeader on All Mutating Requestsidem-scoped-per-account— Scope Idempotency Keys per Account, Not Globallyidem-24h-ttl— Keep Idempotency Keys for 24 Hours, Reap at 72idem-fail-on-key-reuse— Return 409 When a Key Is Reused with Different Paramsidem-recovery-points— Use Recovery Points for Multi-Step Idempotent Operations
6. Versioning & Backwards Compatibility (HIGH)
ver-dated-versions— Use Dated Versions (YYYY-MM-DD), Not v1/v2/v3ver-account-pinning— Pin Each Account to Its First-Request Versionver-additive-changes— Define What Counts as a Backwards-Compatible Changever-version-change-modules— Encapsulate Each Breaking Change in a Version-Change Modulever-tolerate-unknown— Document That Clients Must Tolerate Unknown Fields, Events, and Enum Values
7. Naming, Polymorphism & Metadata (MEDIUM-HIGH)
naming-snake-case-wire-format— Use snake_case for All Wire Identifiersnaming-american-english— Use American English Spelling (canceled, Notcancelled)naming-simple-unambiguous— Names — Simple, Unambiguous, No Leading Digits, No Jargonnaming-type-discriminator-polymorphism— Discriminate Polymorphic Types with atypeField and Sibling Objectsnaming-metadata-pattern— Provide ametadataPass-Through with Strict Limitsnaming-boolean-past-tense— Booleans — Past-Tense Verbs and Plain Adjectives, Notis_/has_Prefixesnaming-enums-over-booleans— Prefer Enums over Booleans for New Status/Flag Fields
8. Authentication, Webhooks & Search (MEDIUM-HIGH)
ops-prefixed-api-keys— Prefix API Keys with Scope and Mode (sk_live_,pk_test_,rk_)ops-https-only-basic-auth— Enforce HTTPS Only and Use HTTP Basic Auth with the Key as Usernameops-on-behalf-of-header— Use a DedicatedOn-Behalf-OfHeader for Multi-Tenant Callsops-webhook-event-envelope— Webhook Events Use a Fixed{id, object, type, data, created}Envelopeops-webhook-event-naming— Event Type Naming —<resource>.<past_tense_action>ops-webhook-signature— Sign Webhook Deliveries with HMAC and a Timestamp Tolerance Windowops-webhook-at-least-once-handlers-idempotent— Document At-Least-Once Delivery; Handlers Must Dedupe onevent.id
How to Use
For a focused question ("should this field be a boolean or an enum?"), jump directly to the relevant rule (naming-enums-over-booleans) — each rule is self-contained with the WHY, code examples, and the canonical source.
For a full API review or audit, work through the categories top-to-bottom. The order matches Stripe's own design priority: get resource modeling and URL structure right first; format, errors, idempotency, and versioning are the next layer; naming and operational surface come last because they're the easiest to evolve.
For producing a structured findings report (the kind an inspector tool emits), cite rules by their slug — resource-unix-seconds-timestamps, format-no-total-counts — so each finding traces back to a specific, defensible source.
Read section definitions for the cascade-impact rationale behind the category ordering, or the rule template when adding a new rule.
Reference Files
| File | Description | |------|-------------| | references/_sections.md | Category definitions and ordering by design-propagation impact | | assets/templates/_template.md | Template for authoring new rules | | metadata.json | Version and reference URLs | | gotchas.md | Failure points discovered when applying the rules |