Document rate limit headers in OpenAPI spec
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
- Add reusable header components (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After) - Reference headers in 200 responses on all conversion and demo endpoints - Add Retry-After header to 429 responses - Update Rate Limits section in API description to mention response headers - Add comprehensive tests for header documentation (21 new tests) - All 809 tests passing
This commit is contained in:
parent
a3bba8f0d5
commit
70eb6908e3
18 changed files with 801 additions and 821 deletions
|
|
@ -131,6 +131,59 @@
|
|||
}
|
||||
},
|
||||
"paths": {
|
||||
"/v1/usage/me": {
|
||||
"get": {
|
||||
"summary": "Get your usage stats",
|
||||
"tags": [
|
||||
"Account"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyHeader": []
|
||||
},
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Usage statistics for the authenticated user",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"used": {
|
||||
"type": "integer",
|
||||
"description": "PDFs generated this month"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Monthly PDF limit for your plan"
|
||||
},
|
||||
"plan": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"demo",
|
||||
"pro"
|
||||
],
|
||||
"description": "Current plan"
|
||||
},
|
||||
"month": {
|
||||
"type": "string",
|
||||
"description": "Current billing month (YYYY-MM)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid API key"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/billing/checkout": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
|
@ -168,102 +221,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v1/billing/success": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Billing"
|
||||
],
|
||||
"summary": "Checkout success page",
|
||||
"description": "Provisions a Pro API key after successful Stripe checkout and displays it in an HTML page.\nCalled by Stripe redirect after payment completion.\n",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "session_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Stripe Checkout session ID"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "HTML page displaying the new API key",
|
||||
"content": {
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing session_id or no customer found"
|
||||
},
|
||||
"409": {
|
||||
"description": "Checkout session already used"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to retrieve session"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/billing/webhook": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Billing"
|
||||
],
|
||||
"summary": "Stripe webhook endpoint",
|
||||
"description": "Receives Stripe webhook events for subscription lifecycle management.\nRequires the raw request body and a valid Stripe-Signature header for verification.\nHandles checkout.session.completed, customer.subscription.updated,\ncustomer.subscription.deleted, and customer.updated events.\n",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "Stripe-Signature",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Stripe webhook signature for payload verification"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"description": "Raw Stripe event payload"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Webhook received",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"received": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing Stripe-Signature header or invalid signature"
|
||||
},
|
||||
"500": {
|
||||
"description": "Webhook secret not configured"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/convert/html": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
|
@ -314,6 +271,17 @@
|
|||
"responses": {
|
||||
"200": {
|
||||
"description": "PDF document",
|
||||
"headers": {
|
||||
"X-RateLimit-Limit": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Limit"
|
||||
},
|
||||
"X-RateLimit-Remaining": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Remaining"
|
||||
},
|
||||
"X-RateLimit-Reset": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Reset"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
|
|
@ -336,7 +304,12 @@
|
|||
"description": "Unsupported Content-Type (must be application/json)"
|
||||
},
|
||||
"429": {
|
||||
"description": "Rate limit or usage limit exceeded"
|
||||
"description": "Rate limit or usage limit exceeded",
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"$ref": "#/components/headers/Retry-After"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "PDF generation failed"
|
||||
|
|
@ -393,6 +366,17 @@
|
|||
"responses": {
|
||||
"200": {
|
||||
"description": "PDF document",
|
||||
"headers": {
|
||||
"X-RateLimit-Limit": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Limit"
|
||||
},
|
||||
"X-RateLimit-Remaining": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Remaining"
|
||||
},
|
||||
"X-RateLimit-Reset": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Reset"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
|
|
@ -415,7 +399,12 @@
|
|||
"description": "Unsupported Content-Type"
|
||||
},
|
||||
"429": {
|
||||
"description": "Rate limit or usage limit exceeded"
|
||||
"description": "Rate limit or usage limit exceeded",
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"$ref": "#/components/headers/Retry-After"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "PDF generation failed"
|
||||
|
|
@ -480,6 +469,17 @@
|
|||
"responses": {
|
||||
"200": {
|
||||
"description": "PDF document",
|
||||
"headers": {
|
||||
"X-RateLimit-Limit": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Limit"
|
||||
},
|
||||
"X-RateLimit-Remaining": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Remaining"
|
||||
},
|
||||
"X-RateLimit-Reset": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Reset"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
|
|
@ -502,7 +502,12 @@
|
|||
"description": "Unsupported Content-Type"
|
||||
},
|
||||
"429": {
|
||||
"description": "Rate limit or usage limit exceeded"
|
||||
"description": "Rate limit or usage limit exceeded",
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"$ref": "#/components/headers/Retry-After"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "PDF generation failed"
|
||||
|
|
@ -551,6 +556,17 @@
|
|||
"responses": {
|
||||
"200": {
|
||||
"description": "Watermarked PDF document",
|
||||
"headers": {
|
||||
"X-RateLimit-Limit": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Limit"
|
||||
},
|
||||
"X-RateLimit-Remaining": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Remaining"
|
||||
},
|
||||
"X-RateLimit-Reset": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Reset"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
|
|
@ -575,6 +591,11 @@
|
|||
},
|
||||
"429": {
|
||||
"description": "Demo rate limit exceeded (5/hour)",
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"$ref": "#/components/headers/Retry-After"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
|
|
@ -633,6 +654,17 @@
|
|||
"responses": {
|
||||
"200": {
|
||||
"description": "Watermarked PDF document",
|
||||
"headers": {
|
||||
"X-RateLimit-Limit": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Limit"
|
||||
},
|
||||
"X-RateLimit-Remaining": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Remaining"
|
||||
},
|
||||
"X-RateLimit-Reset": {
|
||||
"$ref": "#/components/headers/X-RateLimit-Reset"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
|
|
@ -649,7 +681,12 @@
|
|||
"description": "Unsupported Content-Type"
|
||||
},
|
||||
"429": {
|
||||
"description": "Demo rate limit exceeded (5/hour)"
|
||||
"description": "Demo rate limit exceeded (5/hour)",
|
||||
"headers": {
|
||||
"Retry-After": {
|
||||
"$ref": "#/components/headers/Retry-After"
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Server busy"
|
||||
|
|
@ -660,6 +697,141 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v1/email-change": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Account"
|
||||
],
|
||||
"summary": "Request email change",
|
||||
"description": "Sends a 6-digit verification code to the new email address.\nRate limited to 3 requests per hour per API key.\n",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"apiKey",
|
||||
"newEmail"
|
||||
],
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"newEmail": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Verification code sent",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "verification_sent"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing or invalid fields"
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid API key"
|
||||
},
|
||||
"409": {
|
||||
"description": "Email already taken"
|
||||
},
|
||||
"429": {
|
||||
"description": "Too many attempts"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/email-change/verify": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Account"
|
||||
],
|
||||
"summary": "Verify email change code",
|
||||
"description": "Verifies the 6-digit code and updates the account email.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"apiKey",
|
||||
"newEmail",
|
||||
"code"
|
||||
],
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"newEmail": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{6}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Email updated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ok"
|
||||
},
|
||||
"newEmail": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing fields or invalid code"
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid API key"
|
||||
},
|
||||
"410": {
|
||||
"description": "Code expired"
|
||||
},
|
||||
"429": {
|
||||
"description": "Too many failed attempts"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -867,83 +1039,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v1/signup/verify": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Account"
|
||||
],
|
||||
"summary": "Verify email and get API key",
|
||||
"description": "Verifies the 6-digit code sent to the user's email and provisions a free API key.\nRate limited to 15 attempts per 15 minutes.\n",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"code"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Email address used during signup",
|
||||
"example": "user@example.com"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "6-digit verification code from email",
|
||||
"example": "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Email verified, API key issued",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "verified"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"description": "The provisioned API key"
|
||||
},
|
||||
"tier": {
|
||||
"type": "string",
|
||||
"example": "free"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing fields or invalid verification code"
|
||||
},
|
||||
"409": {
|
||||
"description": "Email already verified"
|
||||
},
|
||||
"410": {
|
||||
"description": "Verification code expired"
|
||||
},
|
||||
"429": {
|
||||
"description": "Too many failed attempts"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/templates": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue