{"openapi":"3.1.0","info":{"title":"MockApiHub REST API","version":"1.1.0","description":"Free, deterministic mock REST API for testing and prototyping.\n\nTwo surfaces:\n- Generic datasets (/api/users, /api/posts, …) for traditional mock-API needs.\n- Stripe-shaped surface (/api/stripe/*) for testing Stripe integrations without a Stripe account — cursor pagination, Stripe-style error envelopes, HMAC-signed webhook delivery, and chaos query params (simulate / delay / fail_rate).\n\nWrites (POST/PATCH/DELETE) return realistic, shape-correct responses but are NOT persisted — the next GET returns the same deterministic data.","license":{"name":"MIT"}},"servers":[{"url":"https://mockapihub.com","description":"Production"},{"url":"http://localhost:4000","description":"Local dev"}],"tags":[{"name":"Users"},{"name":"Posts"},{"name":"Products"},{"name":"Recipes"},{"name":"Todos"},{"name":"Comments"},{"name":"Stripe — Customers","description":"Mock Stripe customer objects."},{"name":"Stripe — Charges","description":"Mock Stripe charge objects."},{"name":"Stripe — PaymentIntents","description":"Mock Stripe PaymentIntents including the confirm flow."},{"name":"Stripe — Subscriptions","description":"Mock Stripe subscription objects."},{"name":"Stripe — Invoices","description":"Mock Stripe invoice objects."},{"name":"Stripe — Webhooks","description":"Sign and deliver Stripe-shaped events to a target URL. SSRF-hardened."}],"paths":{"/api/users":{"get":{"tags":["Users"],"summary":"List users","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}},"503":{"description":"Simulated error (when errorRate triggers)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Users"],"summary":"Create a user (not persisted)","description":"Writes return a realistic, shape-correct response but are NOT persisted. The next GET returns the same deterministic data as before.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/User"}}}}}},"400":{"description":"Missing required fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/users/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Users"],"summary":"Get one user","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/User"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Users"],"summary":"Update a user (not persisted)","description":"Writes return a realistic, shape-correct response but are NOT persisted. The next GET returns the same deterministic data as before.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/User"}}}}}}}},"delete":{"tags":["Users"],"summary":"Delete a user (not persisted)","description":"Writes return a realistic, shape-correct response but are NOT persisted. The next GET returns the same deterministic data as before.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"integer"}}}}}}}}}}},"/api/users/{id}/posts":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Users","Posts"],"summary":"List posts authored by this user","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}}}}},"/api/users/{id}/todos":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Users","Todos"],"summary":"List todos owned by this user","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Todo"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}}}}},"/api/posts":{"get":{"tags":["Posts"],"summary":"List posts","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}},"503":{"description":"Simulated error (when errorRate triggers)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/posts/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Posts"],"summary":"Get one post","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Post"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/posts/{id}/comments":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Posts","Comments"],"summary":"List comments on this post","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}}}}},"/api/products":{"get":{"tags":["Products"],"summary":"List products","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}},"503":{"description":"Simulated error (when errorRate triggers)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/products/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Products"],"summary":"Get one product","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Product"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/recipes":{"get":{"tags":["Recipes"],"summary":"List recipes","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}},"503":{"description":"Simulated error (when errorRate triggers)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/recipes/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Recipes"],"summary":"Get one recipe","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Recipe"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/todos":{"get":{"tags":["Todos"],"summary":"List todos","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Todo"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}},"503":{"description":"Simulated error (when errorRate triggers)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/todos/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Todos"],"summary":"Get one todo","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Todo"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/comments":{"get":{"tags":["Comments"],"summary":"List comments","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":3},"description":"Page size. Capped at 100."},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring search across string fields."},{"name":"sort","in":"query","schema":{"type":"string"},"description":"Field name to sort by."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"description":"Sort direction."},{"name":"fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of fields to include in each item."},{"name":"seed","in":"query","schema":{"type":"string"},"description":"Override the seed used by the deterministic data generator."},{"name":"delay","in":"query","schema":{"type":"integer","minimum":0},"description":"Sleep this many ms before responding (chaos testing)."},{"name":"errorRate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a simulated 503 server error."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}},"pagination":{"type":"object","required":["page","limit","total","hasNext"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"hasNext":{"type":"boolean"}}}}}}}},"503":{"description":"Simulated error (when errorRate triggers)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/comments/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"get":{"tags":["Comments"],"summary":"Get one comment","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Comment"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/stripe/customers":{"get":{"tags":["Stripe — Customers"],"summary":"List customers","parameters":[{"$ref":"#/components/parameters/StripeLimit"},{"$ref":"#/components/parameters/StripeStartingAfter"},{"$ref":"#/components/parameters/StripeEndingBefore"},{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeListCustomers"}}}},"400":{"description":"Invalid cursor (resource_missing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}},"402":{"description":"Card error (when ?simulate=card_declined / insufficient_funds)."},"429":{"description":"Rate limit (when ?simulate=rate_limited)."},"500":{"description":"Simulated server error (when ?fail_rate triggers)."},"504":{"description":"Timeout (when ?simulate=timeout)."}}},"post":{"tags":["Stripe — Customers"],"summary":"Create a customer (not persisted)","description":"Writes return a Stripe-shaped object with a fresh id, but the data is NOT persisted. A follow-up GET for the same id returns 404.","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"phone":{"type":"string"},"currency":{"type":"string"},"metadata":{"type":"object"}}}}}},"responses":{"200":{"description":"Created (shape-correct response, not stored).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeCustomer"}}}}}}},"/api/stripe/customers/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"Stripe-style id with the resource prefix."}}],"get":{"tags":["Stripe — Customers"],"summary":"Retrieve a customer","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeCustomer"}}}},"404":{"description":"No such object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}},"/api/stripe/charges":{"get":{"tags":["Stripe — Charges"],"summary":"List charges","parameters":[{"$ref":"#/components/parameters/StripeLimit"},{"$ref":"#/components/parameters/StripeStartingAfter"},{"$ref":"#/components/parameters/StripeEndingBefore"},{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeListCharges"}}}},"400":{"description":"Invalid cursor (resource_missing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}},"402":{"description":"Card error (when ?simulate=card_declined / insufficient_funds)."},"429":{"description":"Rate limit (when ?simulate=rate_limited)."},"500":{"description":"Simulated server error (when ?fail_rate triggers)."},"504":{"description":"Timeout (when ?simulate=timeout)."}}},"post":{"tags":["Stripe — Charges"],"summary":"Create a charge (not persisted)","description":"Writes return a Stripe-shaped object with a fresh id, but the data is NOT persisted. A follow-up GET for the same id returns 404.","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount","currency"],"properties":{"amount":{"type":"integer","description":"Smallest currency unit (cents)."},"currency":{"type":"string"},"customer":{"type":"string","pattern":"^cus_"},"description":{"type":"string"},"metadata":{"type":"object"}}}}}},"responses":{"200":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeCharge"}}}},"400":{"description":"parameter_missing for amount or currency.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}},"/api/stripe/charges/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"Stripe-style id with the resource prefix."}}],"get":{"tags":["Stripe — Charges"],"summary":"Retrieve a charge","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeCharge"}}}},"404":{"description":"No such object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}},"/api/stripe/payment_intents":{"get":{"tags":["Stripe — PaymentIntents"],"summary":"List paymentintents","parameters":[{"$ref":"#/components/parameters/StripeLimit"},{"$ref":"#/components/parameters/StripeStartingAfter"},{"$ref":"#/components/parameters/StripeEndingBefore"},{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeListPaymentIntents"}}}},"400":{"description":"Invalid cursor (resource_missing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}},"402":{"description":"Card error (when ?simulate=card_declined / insufficient_funds)."},"429":{"description":"Rate limit (when ?simulate=rate_limited)."},"500":{"description":"Simulated server error (when ?fail_rate triggers)."},"504":{"description":"Timeout (when ?simulate=timeout)."}}},"post":{"tags":["Stripe — PaymentIntents"],"summary":"Create a PaymentIntent (not persisted)","description":"Writes return a Stripe-shaped object with a fresh id, but the data is NOT persisted. A follow-up GET for the same id returns 404. When body.confirm is true the returned status is succeeded; otherwise requires_payment_method.","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount","currency"],"properties":{"amount":{"type":"integer"},"currency":{"type":"string"},"customer":{"type":"string","pattern":"^cus_"},"description":{"type":"string"},"capture_method":{"type":"string","enum":["automatic","manual"]},"confirm":{"type":"boolean"},"metadata":{"type":"object"}}}}}},"responses":{"200":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripePaymentIntent"}}}},"400":{"description":"parameter_missing for amount or currency.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}},"/api/stripe/payment_intents/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"Stripe-style id with the resource prefix."}}],"get":{"tags":["Stripe — PaymentIntents"],"summary":"Retrieve a paymentintent","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripePaymentIntent"}}}},"404":{"description":"No such object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}},"/api/stripe/payment_intents/{id}/confirm":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^pi_"}}],"post":{"tags":["Stripe — PaymentIntents"],"summary":"Confirm a PaymentIntent","description":"Accepts any pi_* id (including ones from a previous POST that we never persisted).\n\n?simulate=requires_action returns the requires_action status with a next_action object — useful for testing 3DS challenge flows.\nOtherwise the PI is returned with status=succeeded.","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Confirmed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripePaymentIntent"}}}},"404":{"description":"id doesn't start with pi_.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}},"/api/stripe/subscriptions":{"get":{"tags":["Stripe — Subscriptions"],"summary":"List subscriptions","parameters":[{"$ref":"#/components/parameters/StripeLimit"},{"$ref":"#/components/parameters/StripeStartingAfter"},{"$ref":"#/components/parameters/StripeEndingBefore"},{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeListSubscriptions"}}}},"400":{"description":"Invalid cursor (resource_missing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}},"402":{"description":"Card error (when ?simulate=card_declined / insufficient_funds)."},"429":{"description":"Rate limit (when ?simulate=rate_limited)."},"500":{"description":"Simulated server error (when ?fail_rate triggers)."},"504":{"description":"Timeout (when ?simulate=timeout)."}}}},"/api/stripe/subscriptions/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"Stripe-style id with the resource prefix."}}],"get":{"tags":["Stripe — Subscriptions"],"summary":"Retrieve a subscription","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeSubscription"}}}},"404":{"description":"No such object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}},"/api/stripe/invoices":{"get":{"tags":["Stripe — Invoices"],"summary":"List invoices","parameters":[{"$ref":"#/components/parameters/StripeLimit"},{"$ref":"#/components/parameters/StripeStartingAfter"},{"$ref":"#/components/parameters/StripeEndingBefore"},{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeListInvoices"}}}},"400":{"description":"Invalid cursor (resource_missing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}},"402":{"description":"Card error (when ?simulate=card_declined / insufficient_funds)."},"429":{"description":"Rate limit (when ?simulate=rate_limited)."},"500":{"description":"Simulated server error (when ?fail_rate triggers)."},"504":{"description":"Timeout (when ?simulate=timeout)."}}}},"/api/stripe/invoices/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"Stripe-style id with the resource prefix."}}],"get":{"tags":["Stripe — Invoices"],"summary":"Retrieve a invoice","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeInvoice"}}}},"404":{"description":"No such object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}},"/api/stripe/webhooks/trigger":{"post":{"tags":["Stripe — Webhooks"],"summary":"Sign and POST a Stripe-shaped event to a public HTTPS URL","description":"Defenses (see src/lib/stripe-webhook.ts for details):\n- target_url must be https://. http, file, ftp, etc. are rejected.\n- Refuses localhost, *.local, *.internal, private IP literals, and any hostname that DNS-resolves to a private/internal IP (including 169.254.169.254 cloud metadata).\n- redirect:'manual', 5s timeout, target's response body is discarded so this can't be used as a port-scan oracle.\n- Per-IP rate limit: 10 calls/min. count is clamped to 5 events per call.\n\nSigned with HMAC-SHA256(secret, `${t}.${payload}`). Stripe-Signature header format: t=<unix>,v1=<hex>. The public test secret is published in the response so devs can verify with stripe.js's webhooks.constructEvent().","parameters":[{"$ref":"#/components/parameters/ChaosSimulate"},{"$ref":"#/components/parameters/ChaosDelay"},{"$ref":"#/components/parameters/ChaosFailRate"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeWebhookTriggerRequest"}}}},"responses":{"200":{"description":"Events delivered (or attempted).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeWebhookTriggerResponse"}}}},"400":{"description":"Missing field, unsupported event_type, or unsafe target_url.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}},"429":{"description":"Per-IP rate limit. Retry-After header included.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeError"}}}}}}}},"components":{"schemas":{"User":{"type":"object","required":["id","name","email","phone"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"phone":{"type":"string"}}},"Post":{"type":"object","required":["id","userId","title","body"],"properties":{"id":{"type":"integer"},"userId":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"}}},"Product":{"type":"object","required":["id","name","price","inStock"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"price":{"type":"number"},"inStock":{"type":"boolean"}}},"Recipe":{"type":"object","required":["id","title","servings","ingredients","steps"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"servings":{"type":"integer"},"ingredients":{"type":"array","items":{"type":"string"}},"steps":{"type":"array","items":{"type":"string"}}}},"Todo":{"type":"object","required":["id","userId","title","completed"],"properties":{"id":{"type":"integer"},"userId":{"type":"integer"},"title":{"type":"string"},"completed":{"type":"boolean"}}},"Comment":{"type":"object","required":["id","postId","author","body"],"properties":{"id":{"type":"integer"},"postId":{"type":"integer"},"author":{"type":"string"},"body":{"type":"string"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["type","message"],"properties":{"type":{"type":"string"},"message":{"type":"string"}}}}},"StripeCustomer":{"type":"object","required":["id","object","created","livemode"],"properties":{"id":{"type":"string","pattern":"^cus_"},"object":{"type":"string","enum":["customer"]},"created":{"type":"integer","description":"Unix epoch seconds (NOT ISO 8601)."},"email":{"type":["string","null"],"format":"email"},"name":{"type":["string","null"]},"description":{"type":["string","null"]},"phone":{"type":["string","null"]},"currency":{"type":"string","description":"Three-letter ISO currency code (lowercase)."},"delinquent":{"type":"boolean"},"livemode":{"type":"boolean","enum":[false]},"metadata":{"type":"object","additionalProperties":{"type":"string"}}}},"StripeCharge":{"type":"object","required":["id","object","amount","currency","status","livemode"],"properties":{"id":{"type":"string","pattern":"^ch_"},"object":{"type":"string","enum":["charge"]},"amount":{"type":"integer","description":"Amount in the smallest currency unit (e.g. cents). 1000 = $10.00."},"amount_captured":{"type":"integer"},"amount_refunded":{"type":"integer"},"captured":{"type":"boolean"},"created":{"type":"integer"},"currency":{"type":"string"},"customer":{"type":["string","null"],"pattern":"^cus_"},"description":{"type":["string","null"]},"paid":{"type":"boolean"},"status":{"type":"string","enum":["succeeded","pending","failed"]},"livemode":{"type":"boolean","enum":[false]},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"payment_method_details":{"type":"object","properties":{"type":{"type":"string","enum":["card"]},"card":{"type":"object","properties":{"brand":{"type":"string"},"last4":{"type":"string"},"exp_month":{"type":"integer"},"exp_year":{"type":"integer"}}}}}}},"StripePaymentIntent":{"type":"object","required":["id","object","amount","currency","status","livemode"],"properties":{"id":{"type":"string","pattern":"^pi_"},"object":{"type":"string","enum":["payment_intent"]},"amount":{"type":"integer"},"amount_capturable":{"type":"integer"},"amount_received":{"type":"integer"},"capture_method":{"type":"string","enum":["automatic","manual"]},"client_secret":{"type":"string"},"confirmation_method":{"type":"string","enum":["automatic","manual"]},"created":{"type":"integer"},"currency":{"type":"string"},"customer":{"type":["string","null"],"pattern":"^cus_"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["requires_payment_method","requires_confirmation","requires_action","processing","succeeded","canceled"]},"livemode":{"type":"boolean","enum":[false]},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"next_action":{"type":["object","null"],"description":"Present when status=requires_action. Mirrors Stripe's next_action shape (use_stripe_sdk → three_d_secure_redirect).","properties":{"type":{"type":"string"},"use_stripe_sdk":{"type":"object"}}}}},"StripeSubscription":{"type":"object","required":["id","object","status","livemode"],"properties":{"id":{"type":"string","pattern":"^sub_"},"object":{"type":"string","enum":["subscription"]},"billing_cycle_anchor":{"type":"integer"},"cancel_at_period_end":{"type":"boolean"},"canceled_at":{"type":["integer","null"]},"collection_method":{"type":"string","enum":["charge_automatically","send_invoice"]},"created":{"type":"integer"},"currency":{"type":"string"},"current_period_start":{"type":"integer"},"current_period_end":{"type":"integer"},"customer":{"type":"string","pattern":"^cus_"},"description":{"type":["string","null"]},"livemode":{"type":"boolean","enum":[false]},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"status":{"type":"string","enum":["trialing","active","past_due","canceled","unpaid","incomplete","incomplete_expired"]}}},"StripeInvoice":{"type":"object","required":["id","object","status","livemode"],"properties":{"id":{"type":"string","pattern":"^in_"},"object":{"type":"string","enum":["invoice"]},"account_country":{"type":"string"},"account_name":{"type":"string"},"amount_due":{"type":"integer"},"amount_paid":{"type":"integer"},"amount_remaining":{"type":"integer"},"created":{"type":"integer"},"currency":{"type":"string"},"customer":{"type":"string","pattern":"^cus_"},"subscription":{"type":["string","null"],"pattern":"^sub_"},"status":{"type":"string","enum":["draft","open","paid","uncollectible","void"]},"livemode":{"type":"boolean","enum":[false]},"metadata":{"type":"object","additionalProperties":{"type":"string"}}}},"StripeError":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["type","message"],"properties":{"type":{"type":"string","description":"One of: invalid_request_error, card_error, rate_limit_error, api_error."},"code":{"type":"string"},"decline_code":{"type":"string","description":"Present on card_error responses (e.g. generic_decline, insufficient_funds)."},"message":{"type":"string"},"param":{"type":"string"}}}}},"StripeListCustomers":{"type":"object","required":["object","data","has_more","url"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/StripeCustomer"}},"has_more":{"type":"boolean"},"url":{"type":"string","description":"Stripe's path for this resource (e.g. /v1/customers)."}}},"StripeListCharges":{"type":"object","required":["object","data","has_more","url"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/StripeCharge"}},"has_more":{"type":"boolean"},"url":{"type":"string","description":"Stripe's path for this resource (e.g. /v1/customers)."}}},"StripeListPaymentIntents":{"type":"object","required":["object","data","has_more","url"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/StripePaymentIntent"}},"has_more":{"type":"boolean"},"url":{"type":"string","description":"Stripe's path for this resource (e.g. /v1/customers)."}}},"StripeListSubscriptions":{"type":"object","required":["object","data","has_more","url"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/StripeSubscription"}},"has_more":{"type":"boolean"},"url":{"type":"string","description":"Stripe's path for this resource (e.g. /v1/customers)."}}},"StripeListInvoices":{"type":"object","required":["object","data","has_more","url"],"properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/StripeInvoice"}},"has_more":{"type":"boolean"},"url":{"type":"string","description":"Stripe's path for this resource (e.g. /v1/customers)."}}},"StripeWebhookTriggerRequest":{"type":"object","required":["event_type","target_url"],"properties":{"event_type":{"type":"string","enum":["charge.succeeded","charge.failed","customer.created","payment_intent.succeeded","payment_intent.payment_failed","invoice.paid","customer.subscription.created","customer.subscription.deleted"]},"target_url":{"type":"string","format":"uri","description":"Public HTTPS URL to POST the signed event to. Private/internal addresses are refused at request time AND after DNS resolution."},"count":{"type":"integer","minimum":1,"maximum":5,"default":1,"description":"Events to send. Clamped to 5."}}},"StripeWebhookTriggerResponse":{"type":"object","required":["object","event_type","delivered","results","signature"],"properties":{"object":{"type":"string","enum":["webhook.trigger_result"]},"event_type":{"type":"string"},"delivered":{"type":"integer"},"results":{"type":"array","items":{"type":"object","required":["event_id","delivery_status","duration_ms"],"properties":{"event_id":{"type":"string","pattern":"^evt_"},"delivery_status":{"type":"integer","description":"HTTP status returned by the target. 0 if the request errored before a response."},"duration_ms":{"type":"integer"},"error":{"type":"string"}}}},"signature":{"type":"object","properties":{"algorithm":{"type":"string","enum":["HMAC-SHA256"]},"header_format":{"type":"string"},"secret_hint":{"type":"string"},"test_secret":{"type":"string"},"example_header":{"type":"string"}}}}}},"parameters":{"StripeLimit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":10},"description":"Page size, capped at 100. Matches Stripe's pagination."},"StripeStartingAfter":{"name":"starting_after","in":"query","schema":{"type":"string"},"description":"Cursor: ID of the last item from a previous page. Returns items strictly AFTER this id."},"StripeEndingBefore":{"name":"ending_before","in":"query","schema":{"type":"string"},"description":"Cursor: ID of the first item from a previous page. Returns items strictly BEFORE this id."},"ChaosSimulate":{"name":"simulate","in":"query","schema":{"type":"string","enum":["card_declined","insufficient_funds","rate_limited","timeout","requires_action","succeeded"]},"description":"Failure-simulation hook.\n- card_declined → 402 card_error / generic_decline\n- insufficient_funds → 402 card_error / insufficient_funds\n- rate_limited → 429 rate_limit_error\n- timeout → 504 api_error after a 5s delay\n\nOn /payment_intents/{id}/confirm, the non-chaos values requires_action / succeeded steer the confirm result."},"ChaosDelay":{"name":"delay","in":"query","schema":{"type":"integer","minimum":0,"maximum":10000},"description":"Sleep this many ms before responding. Clamped to 10000."},"ChaosFailRate":{"name":"fail_rate","in":"query","schema":{"type":"number","minimum":0,"maximum":1},"description":"Probability (0..1) of returning a 500 api_error. Useful for testing retry logic."}}}}