Document rate limit headers in OpenAPI spec
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:
OpenClaw Subagent 2026-03-18 11:06:22 +01:00
parent a3bba8f0d5
commit 70eb6908e3
18 changed files with 801 additions and 821 deletions

View file

@ -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": [