Follow me at @dane_albaugh
Stripe just made a fun little automated API review tool available for $2 at api-reviews-by-stripe.vercel.app.
The whole process was really simple - just give it your doc URL, your email, pay $2 and you are all set. The analysis was complete in less than 3 minutes.
Thoughts
Fun! Although it is obviously keyed in on Stripe conventions - for example, using POST for resource updates rather than PATCH, and using POST for all custom actions rather than PUT even when idempotency guarantees would make PUT reasonable - there were some genuinely interesting insights mixed in.
That said, it does hallucinate. It flagged that resource type identifiers were missing from our API responses, but we already return these on every object (with minor exceptions). It also suggested we rename our API key deletion endpoint to "Revoke" rather than "Delete" - but we already call it Revoke.
It also seemed to produce some contradictory conclusions. It flagged "PUT used for custom action methods" as a critical error, then turned around and listed "Custom methods correctly use POST with verb-prefixed names" as a passing positive in the same report. It can't both be a critical failure and a passing check.
The Future
Although the results were a bit underwhelming, I do think this kind of automated style check is useful. We do something similar when publishing new API endpoints. Our API repo has markdown docs that spell out our endpoint conventions, and it is straightforward to ask Claude or another agent to read those docs and check whether a new endpoint follows them.
I have gotten even better results from a small macro that spins up one agent per endpoint and asks each one to evaluate its target deeply. Each agent writes a short report to /tmp/audit-results, which I can review and turn into follow-up work. Running this every few weeks has been a helpful way to keep up with drift, though well-written e2e tests are still a better way to make sure important conventions keep holding.
I suspect a deterministic tool that validates your OpenAPI spec against a clear set of standards would be more cost efficient and more effective, but it sure is fun to burn tokens.
The Full Report
Below is the complete review the tool generated for Augno's API.
Critical (2)
1PATCH used for all Update methods instead of POST
- Category: HTTP Methods
- Endpoint:
PATCH /customers/{id},PATCH /carriers/{id},PATCH /roles/{id}, and all other Update endpoints across every domain - Issue: Every Update endpoint across all resource domains uses the PATCH HTTP verb (e.g., PATCH Update Customer, PATCH Update Carrier, PATCH Update Role, PATCH Update Unit). The standard method pattern explicitly requires the POST HTTP verb for updates. PATCH carries REST semantics of partial replacement that conflict with the expected behavior of an update endpoint in this pattern, and breaks consistency with Create endpoints (also POST) that share the same path shape. This will also cause friction in SDK generation and documentation tooling.
- Recommendation: Change all Update method HTTP verbs from PATCH to POST. The URL path shape remains unchanged:
POST /<resource>/{id}. This is the same convention used by well-known APIs built on this pattern, such as Stripe'sPOST /v1/customers/{id}.
// Incorrect
PATCH /v1/sales/customers/{id}
{ "name": "Acme Corp" }
// Correct
POST /v1/sales/customers/{id}
{ "name": "Acme Corp" }
2PUT used for custom action methods
- Category: HTTP Methods
- Endpoint:
PUT /addresses/validate,PUT /item_categories/{id}/add_property,PUT /account_users/{id}/status - Issue: Three endpoints use the PUT HTTP verb for action-oriented operations: 'PUT Validate' for address validation (Core), 'PUT Add Category Property' for associating a property with an item category (Catalog), and 'PUT Update User Status' under Account Users (Identity). Custom methods are only permitted to use GET, POST, or DELETE. PUT is not an allowed verb for custom methods, and its use here introduces incorrect semantics (idempotent full replacement) for operations that are clearly not resource replacements.
- Recommendation: Replace PUT with POST for all three endpoints. Address validation:
POST /addresses/validate(or GET with query params if side-effect-free). Adding a category property:POST /item_categories/{id}/add_property. Updating user status:POST /account_users/{id}/activateor/deactivate. Each of these is a custom method and must use a permitted verb.
// Incorrect
PUT /v1/core/addresses/validate
PUT /v1/catalog/item_categories/{id}/add_property
PUT /v1/identity/account_users/{id}/status
// Correct
POST /v1/core/addresses/validate
POST /v1/catalog/item_categories/{id}/add_property
POST /v1/identity/account_users/{id}/activate
POST /v1/identity/account_users/{id}/deactivate
Warning (3)
1Account User Status modeled as a sub-resource update rather than explicit state-transition methods
- Category: Resource Design
- Endpoint:
PUT /identity/account_users/{id}/status - Issue: The 'Status > Update' endpoint under Account Users implies a path like
/account_users/{id}/statustreated as a child resource. Changing a user's status (e.g., active → suspended → deactivated) typically triggers side effects such as permission revocation, audit log entries, and downstream notifications. The pattern of using a generic sub-resource update is inappropriate here - it obscures which transitions are valid, makes it unclear what side effects occur, and breaks idempotency expectations for update-style calls. - Recommendation: Replace the Status sub-resource with explicit named state-transition custom methods using POST, one per transition:
POST /account_users/{id}/activate,POST /account_users/{id}/deactivate,POST /account_users/{id}/suspend(or equivalent verbs matching your domain model). Each method clearly communicates intent, allows transition-specific validation, and can document its own side effects.
// Correct - explicit state transition custom methods
POST /v1/identity/account_users/{id}/activate
POST /v1/identity/account_users/{id}/deactivate
POST /v1/identity/account_users/{id}/suspend
// Each returns the updated AccountUser resource
{ "id": "user_abc123", "status": "active", ... }
2Asymmetric operation sets on Item Category child resources
- Category: Resource Design
- Endpoint:
Item Categories > Properties, Item Categories > Unit Groups - Issue: Under Item Categories, the child resources Properties and Unit Groups expose only Update and Delete (Properties: Update + Delete; Unit Groups: Update only), with no visible Create or Retrieve operations at those paths. Full CRUD exists for analogous resources under Product Lines. This asymmetry suggests that creation of Item Category Properties happens via the 'Add Category Property' action endpoint rather than a standard child resource Create, splitting creation from management across two different path patterns. This is confusing and inconsistent.
- Recommendation: Align Item Category child resources with the pattern used under Product Lines: expose a standard POST Create at the child resource path (
POST /item_categories/{id}/properties), a GET List, a GET Retrieve, and retain Update/Delete. If the 'Add Category Property' action associates an existing property object rather than creating a new one, that distinction should be clearly documented and the paths should reflect the semantic difference (e.g.,POST /item_categories/{id}/propertiesfor creation vs.POST /item_categories/{id}/add_propertyfor association).
// Consistent child resource pattern:
POST /v1/catalog/item_categories/{id}/properties // Create
GET /v1/catalog/item_categories/{id}/properties // List
GET /v1/catalog/item_categories/{id}/properties/{pid} // Retrieve
POST /v1/catalog/item_categories/{id}/properties/{pid} // Update
DELETE /v1/catalog/item_categories/{id}/properties/{pid} // Delete
3No visible resource type identifier on API responses
- Category: Resource Design
- Endpoint:
All resource responses - Issue: The API documentation overview and resource listings do not reference a consistent resource type discriminator field on response objects. Every API resource should include a read-only field that uniquely identifies which resource type the response represents (e.g., a field named 'object', 'type', or 'kind' with a value like 'customer' or 'carrier'). Without this, SDK deserialization, logging, and polymorphic handling become fragile - consumers cannot reliably determine resource type without relying on request context alone.
- Recommendation: Add a consistent read-only resource type identifier field to every resource response. Choose one field name (e.g., 'object' or 'type') and use it uniformly across the entire API. Values should be unique, human-readable strings matching the resource name (e.g., 'customer', 'carrier', 'payment_term'). For namespaced resources, use dot notation: 'catalog.unit', 'sales.customer'.
// Example Customer response with type identifier
{
"id": "cus_abc123",
"object": "customer",
"name": "Acme Corp",
"created": 1714000000
}
// Example namespaced resource
{
"id": "unit_xyz789",
"object": "catalog.unit",
"name": "Each"
}
Suggestion (2)
1API Key deletion action labeled 'Revoke' rather than 'Delete'
- Category: Naming
- Endpoint:
DELETE /auth/api_keys/{id} - Issue: The API Key deletion endpoint is labeled 'Revoke' (DELETE Revoke) while all other resource deletion endpoints are consistently labeled 'Delete'. While 'Revoke' is semantically meaningful for credentials, it introduces a naming inconsistency that may result in different SDK method names (revoke_api_key() vs delete_api_key()) and could confuse developers expecting a uniform delete pattern. If revocation has meaningfully different behavior from deletion (e.g., immediate credential invalidation with an audit trail vs. permanent removal), the distinction warrants either clear documentation or a separate custom method.
- Recommendation: If revoke and delete have identical semantics, rename the action to 'Delete' for consistency. If they differ (revoke = invalidate but retain record; delete = hard remove), model revocation as a custom POST method (
POST /api_keys/{id}/revoke) and retain DELETE for hard deletion. Document the distinction clearly.
// If semantics differ:
POST /v1/auth/api_keys/{id}/revoke // Immediate invalidation, record retained
DELETE /v1/auth/api_keys/{id} // Hard deletion of the key record
2Inconsistent retrieve operation labels ('Get' vs 'Retrieve') across docs
- Category: Naming
- Endpoint:
All single-resource GET /{id} endpoints - Issue: The API reference summary tables use 'GET Get' for retrieve-by-ID operations (e.g., 'GET Get Customer', 'GET Get Carrier', 'GET Get Log'), while the sidebar uses 'Retrieve' in some sections. This inconsistency in display naming is likely to propagate into SDK method names (get_customer() in some SDKs vs. retrieve_customer() in others) and creates a rough experience when developers search documentation.
- Recommendation: Standardize all retrieve-by-ID operation labels to 'Retrieve' throughout both the sidebar navigation and the summary tables. This ensures SDK generators and documentation tooling produce consistent method names.
// Consistent SDK method naming
client.customers.retrieve("cus_abc123")
client.carriers.retrieve("carrier_xyz")
client.roles.retrieve("role_123")
Positive (4)
1Custom methods correctly use POST with verb-prefixed names
- Category: HTTP Methods
- Endpoint:
POST /customers/{id}/merge, POST /api_keys/{id}/rotate - The two custom action endpoints visible in the API - 'POST Merge' under Customers and 'POST Rotate' under API Keys - both correctly use the POST HTTP verb and are named starting with a verb. Both operations have clear side effects (data consolidation across customer records; credential invalidation and regeneration) that make them inappropriate to model as standard CRUD operations. The path structure follows the expected pattern:
/<resource>/{id}/<method_name>.
2Sandbox environment clearly separated from production resources
- Category: Resource Design
- Endpoint:
POST /core/sandboxes, GET /core/sandboxes, DELETE /core/sandboxes/{id} - The Sandboxes resource (Create, List, Retrieve, Delete) is nested under Core and provides a dedicated test environment that is clearly distinct from production API resources. This follows the best practice of separating test helper functionality from production paths, giving developers a safe environment to validate integration flows without risk to live data. The design makes it obvious that sandbox operations do not affect production state.
3Well-structured domain namespacing across resource groups
- Category: API Structure
- The API surface is cleanly organized into domain namespaces - Auth, Core, Catalog, Finance, Identity, Operations, and Sales - with related resources consistently co-located (Carriers + Service Levels in Operations; Customers + Account Groups in Sales; Units + Unit Groups + Properties in Catalog). This structure makes the API easy to navigate, supports team ownership of distinct domains, and provides a natural extension model for adding new resources in the right namespace.
4Read-only lookup resources correctly expose only List and Retrieve
- Category: Resource Design
- Endpoint:
GET /account_statuses, GET /location_types, GET /priorities - System-defined reference resources - Account Statuses, Location Types, and Priorities - correctly expose only List and Retrieve operations, with no Create, Update, or Delete endpoints. This accurately signals to consumers that these are managed enumerations they can query but not modify, preventing accidental write attempts and keeping the API surface honest about what is actually mutable.
API Design Pattern Insights
Update method HTTP verb
Source: Core concepts
Update method must use the POST HTTP verb. Update method HTTP path must be <primary_path>/{id} for non-singleton resources. All update method request body fields must be optional. Update method must return an instance of the resource after the update was applied.
Custom methods - permitted HTTP verbs and naming
Source: Core concepts
Custom methods must use GET, POST, or DELETE HTTP verbs. Custom methods must define a name. Custom methods' names must start with a verb (e.g., void, collect_inputs). Custom method HTTP path must be <primary_path>/{id}/<method_name> for non-singleton resources. Custom method path must end in a single segment.
Verby paths for state changes with side effects
Source: Core concepts
Use 'verby' paths for updates that have side effects, or meaningfully change the state of other objects. BAD: POST /v1/invoices/:id -d state=uncollectible - this state update has other side effects and is only allowed from certain other states. The return value of these action APIs should be the object that the action was taken on.
Resource type identifier
Source: Core concepts
Every API resource should have a read-only field that uniquely identifies which type of resource the response describes. A consistent field name and value format should be used across the entire API. The field should be read-only and always present on resource responses. For non-Stripe APIs, this field may be called 'type', 'kind', 'resource', or any other name appropriate to the API - what matters is that it is consistent.
Test helper resource separation
Source: Core concepts
Test helper resources must be clearly separated from production resources so it is obvious they are not available in production. This can be achieved by namespacing test helpers under a dedicated path, providing a separate test mode with its own credentials or base URL, or using a distinct test-only path prefix. The key requirement is clear separation, not a specific mechanism.
Favor extensibility - enums over booleans, hashes over scalars
Source: Best practices & anti-patterns
Prefer enums to booleans for new properties in the API. They're typically more extensible and descriptive. Use good basic techniques: enums rather than booleans, hashes rather than scalars, if there's a good chance that you'll need to expand the feature set later. A default enum value should always be an explicit option.
Prefixed string identifiers for readability
Source: Model design
All resources have a prefix for better readability of the token. Common reusable and transactional objects should have short, memorable identifiers. Second-order objects should have prefixes that better match their literal name, up to 7 characters. Stripe uses prefixed string IDs like cus_123, pm_123, and fa_123 - users begin to recognize resource types at a glance from the ID prefix alone.