mdxui

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/deactivate

HTTP 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=20

POST - 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_1234567890

API 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+enrichment

Pagination

# 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,-createdAt

Field 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,features

Response 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

CodeMeaningUsage
200OKSuccessful GET, PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestInvalid input, validation error
401UnauthorizedMissing or invalid API key
403ForbiddenValid auth but insufficient permissions
404Not FoundResource doesn't exist
409ConflictResource already exists, state conflict
422Unprocessable EntitySemantic error (valid syntax, invalid data)
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer error
503Service UnavailableTemporary 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 resets

Rate 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/products

Header Versioning

GET https://api.acme.com/products
Accept: application/vnd.acme.v2+json

Deprecation

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

  1. Use nouns for resources, not verbs

    • Good: GET /products
    • Bad: GET /getProducts
  2. Use plural nouns for collections

    • Good: /products, /users
    • Bad: /product, /user
  3. Use nested routes for relationships

    • Good: /products/:id/features
    • Bad: /product-features?productId=:id
  4. Use HTTP methods for actions

    • GET = retrieve
    • POST = create
    • PATCH = update
    • DELETE = remove
  5. Return appropriate status codes

    • 2xx = success
    • 4xx = client error
    • 5xx = server error

Performance

  1. Support field selection to reduce payload size
  2. Use cursor pagination for large datasets
  3. Implement ETags for caching
  4. Support compression (gzip, brotli)
  5. Use CDN for static responses

Security

  1. Always use HTTPS
  2. Validate all inputs
  3. Implement rate limiting
  4. Use API keys or OAuth
  5. 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

On this page