OpenAPI Surface Area Seams
The madcatalog API surface is split across multiple OpenAPI spec files,
each owned by one logical domain. Payment-provider-specific endpoints live
in api/{provider}.openapi.yml. The core catalog API lives in
api/openapi.yml. All specs are linted by CI.
Context
The original api/openapi.yml contained all API endpoints in a single file.
As the API grew to include payment infrastructure, two categories of endpoints emerged
that have fundamentally different characteristics from the core catalog product:
POST /payments/stripe/spt— creates a Stripe Shared Payment Token. This is Stripe-specific implementation detail of the MPP payment rail. If a second payment method were added, an equivalent endpoint would appear next to it, polluting the product API with vendor names.POST /webhooks/stripe— called by Stripe's servers, not by API clients. Different caller, different trust model (Stripe-Signature), different lifecycle. It is not a client API at all.
Mixing these with /catalog, /ads, and /advertisers
makes the spec harder to read and creates a false impression that Stripe is a core
dependency of the catalog model rather than a replaceable payment rail.
Decision
The OpenAPI surface area is split along the following seams:
| File | Domain | Paths |
|---|---|---|
api/openapi.yml | Core madcatalog product API | /catalog, /ads/*, /advertisers/* |
api/stripe.openapi.yml | Stripe payment integration | /payments/stripe/spt, /webhooks/stripe |
A redocly.yaml at the project root registers both specs. CI lints
all registered specs on every commit — adding a spec file to redocly.yaml
is the only step required to bring a new spec under the quality gate.
Rule for future additions: before adding any new path, identify which
seam it belongs to. A new payment provider gets a new
api/{provider}.openapi.yml. A new catalog or advertiser feature goes in
api/openapi.yml. A path that spans seams (e.g. an MPP helper that is not
Stripe-specific) belongs in the core spec.
Seam Within the Stripe Spec
Within api/stripe.openapi.yml there is a second, softer seam:
POST /payments/stripe/spt is called by the client (the bidding
machine), while POST /webhooks/stripe is called by Stripe's
infrastructure. They share Stripe as the vendor but have different callers and
trust models. They coexist in one file at this scale because both are Stripe-specific
and splitting them further would add file overhead without clarity benefit. If either
grows significantly, they are natural candidates for further separation.
Consequences
Positive
- Core spec is free of payment-vendor vocabulary
- Adding a payment rail is additive — one new file, no changes to core
- CI lints all specs; the gate cannot be bypassed by creating an unlinted file
- Spec ownership is clear: catalog team owns
openapi.yml, payments team owns provider specs
Negative
- Shared schemas (e.g.
ErrorResponse) are referenced from both specs — not DRY at the OpenAPI level (mitigated: schemas live instatic/schemas/and are referenced by both) - Developers unfamiliar with the seam may add Stripe paths to the wrong file
- Two files to update when a base URL changes
Alternatives Considered
| Option | Why not chosen |
|---|---|
| Keep everything in one spec | Mixes vendor-specific infrastructure with product API; pollutes the catalog contract with Stripe vocabulary |
| Split webhook into its own spec | Over-engineering at current scale; both webhook and SPT are Stripe-specific and cohesive as a unit |
| Read/write split (public vs. authenticated) | Softer seam — same schemas, same base URL, same domain; tags handle the distinction without a file split |