REST APIs
REST API interfaces for product access
REST APIs
REST APIs (Representational State Transfer) are the primary programmatic interface for products. Every product - whether SaaS, API, Data, or Headless - exposes a REST API for developers to integrate programmatically.
Overview
REST APIs provide:
- Resource-based URLs - Collections and individual resources
- HTTP methods - GET, POST, PATCH, DELETE for CRUD operations
- JSON payloads - Standard request/response format
- Stateless - Each request contains all necessary information
- Versioned -
/v1/,/v2/for backward compatibility
URL Structure
Resource Pattern
https://api.{product}.com/{version}/{resource}
https://api.{product}.com/{version}/{resource}/{id}
https://api.{product}.com/{version}/{resource}/{id}/{sub-resource}Examples
# Products
GET /v1/products
GET /v1/products/:id
POST /v1/products
PATCH /v1/products/:id
DELETE /v1/products/:id
# Nested resources
GET /v1/products/:id/features
GET /v1/products/:id/prices
# Actions (non-CRUD)
POST /v1/products/:id/activate
POST /v1/products/:id/deactivateHTTP Methods
GET - Retrieve Resources
# List collection
GET /v1/products
Response: { data: Product[], pagination: {...} }
# Get single resource
GET /v1/products/prod_123
Response: { data: Product }
# Query parameters for filtering
GET /v1/products?type=API&isActive=true&limit=20POST - Create Resources
POST /v1/products
Content-Type: application/json
{
"name": "Acme API",
"type": "API",
"business": "biz_456",
"description": "Data enrichment API"
}
Response:
{
"data": {
"id": "prod_789",
"name": "Acme API",
...
}
}PATCH - Update Resources
PATCH /v1/products/prod_123
Content-Type: application/json
{
"description": "Updated description",
"isActive": false
}
Response:
{
"data": {
"id": "prod_123",
"description": "Updated description",
"isActive": false,
...
}
}DELETE - Remove Resources
DELETE /v1/products/prod_123
Response:
{
"data": {
"id": "prod_123",
"deleted": true
}
}Authentication
API Keys
# Authorization header (recommended)
GET /v1/products
Authorization: Bearer sk_live_1234567890
# Query parameter (less secure)
GET /v1/products?api_key=sk_live_1234567890API Key Types
interface APIKey {
id: string
key: string // sk_test_... or sk_live_...
type: 'test' | 'live' // Test vs production
name: string // User-provided label
scopes: string[] // Permissions
rateLimit: number // Requests per minute
createdAt: Date
lastUsed?: Date
}OAuth 2.0
# Authorization Code Flow
GET /oauth/authorize?client_id=...&redirect_uri=...&scope=...
# Token Exchange
POST /oauth/token
{
"grant_type": "authorization_code",
"code": "...",
"client_id": "...",
"client_secret": "..."
}
# Use access token
GET /v1/products
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...Query Parameters
Filtering
# Exact match
GET /v1/products?type=API
# Multiple values (OR)
GET /v1/products?type=API,SaaS
# Comparison operators
GET /v1/products?createdAt[gte]=2024-01-01
GET /v1/products?price[lt]=100
# Nested fields
GET /v1/products?business.name=Acme
# Full-text search
GET /v1/products?q=data+enrichmentPagination
# Limit and offset
GET /v1/products?limit=20&offset=40
# Cursor-based (recommended for large datasets)
GET /v1/products?limit=20&cursor=eyJpZCI6InByb2RfMTIzIn0
Response:
{
"data": [...],
"pagination": {
"hasMore": true,
"nextCursor": "eyJpZCI6InByb2RfNDU2In0"
}
}Sorting
# Single field
GET /v1/products?sort=name
# Descending
GET /v1/products?sort=-createdAt
# Multiple fields
GET /v1/products?sort=type,-createdAtField Selection
# Include specific fields only
GET /v1/products?fields=id,name,type
# Exclude fields
GET /v1/products?fields=-description,-features
# Expand relations
GET /v1/products?expand=business,featuresResponse Format
Success Response
{
"data": {
"id": "prod_123",
"name": "Acme API",
"type": "API",
"business": {
"id": "biz_456",
"name": "Acme Corp"
}
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req_abc123"
}
}Collection Response
{
"data": [
{ "id": "prod_1", ... },
{ "id": "prod_2", ... }
],
"pagination": {
"total": 147,
"limit": 20,
"offset": 0,
"hasMore": true,
"nextCursor": "..."
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req_abc123"
}
}Error Response
{
"error": {
"type": "validation_error",
"message": "Invalid product type",
"code": "INVALID_TYPE",
"field": "type",
"suggestion": "Must be one of: SaaS, API, Data, Headless"
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req_abc123"
}
}Error Handling
HTTP Status Codes
| Code | Meaning | Usage |
|---|---|---|
| 200 | OK | Successful GET, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input, validation error |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | Valid auth but insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource already exists, state conflict |
| 422 | Unprocessable Entity | Semantic error (valid syntax, invalid data) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error |
| 503 | Service Unavailable | Temporary downtime |
Error Types
interface APIError {
type: 'validation_error' | 'authentication_error' | 'authorization_error' |
'not_found_error' | 'rate_limit_error' | 'server_error'
message: string
code: string // Machine-readable error code
field?: string // Field that caused error (if applicable)
suggestion?: string // How to fix the error
documentation?: string // Link to relevant docs
}Error Examples
// Validation error
{
"error": {
"type": "validation_error",
"message": "Product name is required",
"code": "MISSING_REQUIRED_FIELD",
"field": "name"
}
}
// Authentication error
{
"error": {
"type": "authentication_error",
"message": "Invalid API key",
"code": "INVALID_API_KEY",
"documentation": "https://docs.acme.com/authentication"
}
}
// Rate limit error
{
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 60,
"suggestion": "Wait 60 seconds or upgrade your plan"
}
}Rate Limiting
Rate Limit Headers
X-RateLimit-Limit: 1000 # Requests allowed per window
X-RateLimit-Remaining: 847 # Requests remaining
X-RateLimit-Reset: 1674567890 # Unix timestamp when limit resetsRate Limit Response
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1674567890
Retry-After: 60
{
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded",
"retryAfter": 60
}
}Rate Limit Tiers
interface RateLimitTier {
plan: string
requestsPerMinute: number
requestsPerHour: number
requestsPerDay: number
burstLimit: number // Short burst allowance
}
const rateLimits = {
free: { rpm: 10, rph: 100, rpd: 1000, burst: 20 },
starter: { rpm: 100, rph: 5000, rpd: 50000, burst: 200 },
pro: { rpm: 1000, rph: 50000, rpd: 1000000, burst: 2000 },
enterprise: { rpm: 10000, rph: Infinity, rpd: Infinity, burst: 20000 }
}Webhooks
Webhook Events
interface Webhook {
id: string
url: string // Customer's endpoint
events: string[] // Events to subscribe to
secret: string // For signature verification
isActive: boolean
}
// Available events
const events = [
'product.created',
'product.updated',
'product.deleted',
'subscription.created',
'subscription.updated',
'subscription.canceled',
'usage.threshold_reached'
]Webhook Payload
{
"id": "evt_123",
"type": "product.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": "prod_456",
"name": "New Product",
"type": "API",
...
}
}Signature Verification
// Verify webhook signature
import { createHmac } from 'crypto'
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = createHmac('sha256', secret)
.update(payload)
.digest('hex')
return signature === expectedSignature
}
// Usage
const signature = request.headers['x-webhook-signature']
const isValid = verifyWebhookSignature(
request.body,
signature,
webhook.secret
)Versioning
URL Versioning
# Version 1
GET https://api.acme.com/v1/products
# Version 2 (with breaking changes)
GET https://api.acme.com/v2/productsHeader Versioning
GET https://api.acme.com/products
Accept: application/vnd.acme.v2+jsonDeprecation
HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 01 Jan 2025 00:00:00 GMT
Link: <https://api.acme.com/v2/products>; rel="alternate"
{
"data": {...},
"meta": {
"warning": "This API version is deprecated and will be removed on 2025-01-01. Please migrate to v2."
}
}Idempotency
Idempotency Keys
POST /v1/products
Idempotency-Key: unique-key-123
Content-Type: application/json
{
"name": "New Product",
...
}Retry with same key returns original result:
# Same request, same key
POST /v1/products
Idempotency-Key: unique-key-123
# Returns cached response (doesn't create duplicate)
HTTP/1.1 200 OK
Idempotent-Replayed: true
{
"data": { "id": "prod_789", ... }
}Best Practices
API Design
-
Use nouns for resources, not verbs
- Good:
GET /products - Bad:
GET /getProducts
- Good:
-
Use plural nouns for collections
- Good:
/products,/users - Bad:
/product,/user
- Good:
-
Use nested routes for relationships
- Good:
/products/:id/features - Bad:
/product-features?productId=:id
- Good:
-
Use HTTP methods for actions
- GET = retrieve
- POST = create
- PATCH = update
- DELETE = remove
-
Return appropriate status codes
- 2xx = success
- 4xx = client error
- 5xx = server error
Performance
- Support field selection to reduce payload size
- Use cursor pagination for large datasets
- Implement ETags for caching
- Support compression (gzip, brotli)
- Use CDN for static responses
Security
- Always use HTTPS
- Validate all inputs
- Implement rate limiting
- Use API keys or OAuth
- Sanitize error messages (no sensitive data)
Code Examples
cURL
# List products
curl https://api.acme.com/v1/products \
-H "Authorization: Bearer sk_live_..."
# Create product
curl https://api.acme.com/v1/products \
-X POST \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "New Product",
"type": "API",
"business": "biz_123"
}'JavaScript
const client = {
baseURL: 'https://api.acme.com/v1',
apiKey: 'sk_live_...',
async request(method, path, data) {
const response = await fetch(`${this.baseURL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error.message)
}
return response.json()
},
// Convenience methods
get(path) { return this.request('GET', path) },
post(path, data) { return this.request('POST', path, data) },
patch(path, data) { return this.request('PATCH', path, data) },
delete(path) { return this.request('DELETE', path) },
}
// Usage
const products = await client.get('/products?type=API')
const product = await client.post('/products', {
name: 'New Product',
type: 'API'
})Python
import requests
class AcmeClient:
def __init__(self, api_key):
self.base_url = 'https://api.acme.com/v1'
self.api_key = api_key
def request(self, method, path, data=None):
response = requests.request(
method=method,
url=f'{self.base_url}{path}',
headers={
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json',
},
json=data
)
response.raise_for_status()
return response.json()
def get(self, path):
return self.request('GET', path)
def post(self, path, data):
return self.request('POST', path, data)
# Usage
client = AcmeClient('sk_live_...')
products = client.get('/products?type=API')
product = client.post('/products', {
'name': 'New Product',
'type': 'API'
})Next Steps
- API Products - Usage-based API products
- MCPs - Model Context Protocol for agents
- SDKs - Client libraries wrapping REST APIs