🌐 Understanding and Using APIs

REST APIs, Webhooks & Constraints

šŸ“” 2.1 Construct a REST API Request

What is REST API?

REST (Representational State Transfer) is an architectural style for designing networked applications. REST APIs use HTTP requests to perform CRUD operations (Create, Read, Update, Delete) on resources.

Components of a REST API Request

1. HTTP Methods

Method Purpose Example Use Case
GET Retrieve data from server Get list of devices, retrieve user info
POST Create new resource Create new user, add device to network
PUT Update/replace entire resource Replace entire configuration
PATCH Partially update resource Update specific fields of a device
DELETE Remove resource Delete device, remove user account

2. URL/Endpoint Structure

graph LR A["https://api.example.com"] -->|"/"| B["v1"] B -->|"/"| C["devices"] C -->|"/"| D["12345"] A -->|Base URL| A1["Protocol + Domain
https://api.example.com"] B -->|API Version| B1["Version Number
v1, v2, etc."] C -->|Resource Type| C1["Collection Name
devices, users, products"] D -->|Resource ID| D1["Specific Item
UUID or numeric ID"] style A fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff style B fill:#51cf66,stroke:#2f9e44,stroke-width:2px,color:#fff style C fill:#339af0,stroke:#1864ab,stroke-width:2px,color:#fff style D fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,color:#fff style A1 fill:#e7f3ff,stroke:#667eea,stroke-width:2px style B1 fill:#e6fcef,stroke:#51cf66,stroke-width:2px style C1 fill:#d9f1ff,stroke:#339af0,stroke-width:2px style D1 fill:#ffe0e0,stroke:#ff6b6b,stroke-width:2px
šŸ“Œ Complete URL Example:
https://api.example.com/v1/devices/12345

Breaking it down:
🌐 Base URL: https://api.example.com
šŸ“Œ Version: /v1
šŸ“¦ Resource: /devices
šŸ†” ID: /12345

3. Headers (Python Script Format)

Headers provide metadata about the request. In Python, headers are sent as a dictionary:

#!/usr/bin/env python3
"""
API Headers Configuration Example
Demonstrates how to create and use headers in API requests
"""

import requests


# ==============================================================================
# METHOD 1: Bearer Token Authentication (OAuth 2.0)
# ==============================================================================

# Create headers dictionary with Bearer token
headers_bearer = {
    "Content-Type": "application/json",     # Sending JSON data to API
    "Accept": "application/json",           # Expecting JSON response
    "Authorization": "Bearer <YOUR_JWT_TOKEN_HERE>"
    # Replace <YOUR_JWT_TOKEN_HERE> with your actual token
    # Format must be: "Bearer <token>"
}


# ==============================================================================
# METHOD 2: API Key Authentication
# ==============================================================================

headers_api_key = {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-API-Key": "<YOUR_API_KEY>"
    # Replace <YOUR_API_KEY> with your API key
    # Header name varies by API: X-API-Key, Api-Key, apikey, etc.
}


# ==============================================================================
# USAGE EXAMPLE: Making API Request with Headers
# ==============================================================================

def make_api_request():
    """
    Make GET request with authentication headers
    """
    url = "https://api.example.com/v1/devices"   # API endpoint URL

    try:
        # Perform GET request (swap headers if needed)
        response = requests.get(url, headers=headers_bearer)

        # Raises exception if response is 4xx/5xx
        response.raise_for_status()

        # Print response details
        print(f"āœ… Status Code: {response.status_code}")
        print(f"šŸ“Š Response Headers: {dict(response.headers)}")
        print("šŸ“¦ Response Data:")
        print(response.json())

        return response.json()

    except requests.exceptions.HTTPError as err:
        print(f"āŒ HTTP Error: {err}")

    except requests.exceptions.ConnectionError:
        print("āŒ Connection Error: Could not connect to server")

    except requests.exceptions.Timeout:
        print("ā±ļø Timeout Error: Request timed out")

    except Exception as err:
        print(f"āš ļø Unexpected Error: {err}")


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    make_api_request()

4. Request Body (Python Script Format)

Request body contains the data you're sending to the server (for POST, PUT, PATCH):

#!/usr/bin/env python3
"""
Request Body Configuration Example
Demonstrates creating and sending JSON payloads in API requests
"""

import requests


# ==============================================================================
# METHOD 1: Simple Payload (Direct Dictionary)
# ==============================================================================

# Create request body as Python dictionary
payload = {
    "name": "Router-01",                     # Device hostname
    "ip_address": "192.168.1.1",            # Device IP address
    "status": "active",                      # Device status
    "location": "Building A"                 # Physical location
}


# ==============================================================================
# METHOD 2: Nested Payload (Complex Data)
# ==============================================================================

payload_nested = {
    "device": {
        "name": "Switch-05",
        "type": "switch",
        "interfaces": [
            {"name": "GigE0/0", "ip": "10.0.0.1"},
            {"name": "GigE0/1", "ip": "10.0.0.2"}
        ]
    },
    "metadata": {
        "created_by": "admin",
        "tags": ["production", "core"]
    }
}


# ==============================================================================
# USAGE EXAMPLE: Sending Request Body in POST Request
# ==============================================================================

def create_device(payload):
    """
    Send POST request with JSON payload
    """
    url = "https://api.example.com/v1/devices"
    
    headers = {
        "Content-Type": "application/json",
        "Authorization": "Bearer <YOUR_TOKEN>"
    }

    try:
        # Send POST request (json param auto-converts dict to JSON)
        response = requests.post(url, json=payload, headers=headers)

        # Raises exception if response is 4xx/5xx
        response.raise_for_status()

        # Print response
        print(f"āœ… Status Code: {response.status_code}")
        print("šŸ“¦ Created Device:")
        print(response.json())

        return response.json()

    except requests.exceptions.HTTPError as err:
        print(f"āŒ HTTP Error: {err}")

    except requests.exceptions.ConnectionError:
        print("āŒ Connection Error: Could not connect to server")

    except requests.exceptions.Timeout:
        print("ā±ļø Timeout Error: Request timed out")

    except Exception as err:
        print(f"āš ļø Unexpected Error: {err}")


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    create_device(payload)

Practical Examples

Example 1: GET Request (Python Script Format)

#!/usr/bin/env python3
"""
GET Request Example
Retrieves list of devices from REST API
"""

import requests


# ==============================================================================
# CONFIGURATION
# ==============================================================================

API_URL = "https://api.example.com/v1/devices"
API_TOKEN = "<YOUR_JWT_TOKEN_HERE>"


# ==============================================================================
# GET REQUEST FUNCTION
# ==============================================================================

def get_devices():
    """
    Retrieve all devices from API
    
    Returns:
        list: List of device dictionaries or None if failed
    """
    
    # Prepare headers
    headers = {
        "Accept": "application/json",
        "Authorization": f"Bearer {API_TOKEN}"
    }
    
    try:
        # Make GET request (retrieves data, doesn't modify)
        response = requests.get(API_URL, headers=headers, timeout=10)
        
        # Check if successful
        response.raise_for_status()
        
        # Parse and return JSON response
        devices = response.json()
        print(f"āœ… Successfully retrieved {len(devices)} devices")
        return devices

    except requests.exceptions.HTTPError as err:
        print(f"āŒ HTTP Error: {err}")
        
    except requests.exceptions.ConnectionError:
        print("āŒ Connection Error: Check network/URL")
        
    except requests.exceptions.Timeout:
        print("ā±ļø Timeout Error: Request timed out")
        
    except Exception as err:
        print(f"āš ļø Unexpected Error: {err}")
    
    return None


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    print("šŸ” Fetching devices from API...\n")
    
    devices = get_devices()
    
    if devices:
        print(f"\nšŸ“Š Retrieved {len(devices)} devices")
        for device in devices:
            print(f"  - {device.get('name', 'N/A')}: {device.get('ip_address', 'N/A')}")
    else:
        print("āŒ Failed to retrieve devices")

Example 2: POST Request with Python

#!/usr/bin/env python3
"""
POST Request Example - Create New Device
Demonstrates creating a resource via REST API
"""

import requests


# ==============================================================================
# CONFIGURATION
# ==============================================================================

url = "https://api.example.com/v1/devices"      # API endpoint

headers = {
    "Content-Type": "application/json",          # Sending JSON data
    "Authorization": "Bearer <YOUR_TOKEN>"      # Your auth token
}

payload = {
    "name": "Switch-05",                          # Device hostname
    "ip_address": "10.1.1.50",                   # Device IP
    "type": "switch"                             # Device type
}


# ==============================================================================
# MAKE POST REQUEST
# ==============================================================================

# Send POST request (json param converts dict to JSON automatically)
response = requests.post(url, json=payload, headers=headers)

# Check status code
print(f"Status Code: {response.status_code}")  # 201=Created, 200=OK

# Display response body
print(f"Response: {response.json()}")          # Created device details

Example 3: PATCH Request with JavaScript

#!/usr/bin/env node
/**
 * PATCH Request Example - Update Device
 * Demonstrates partial update via REST API using JavaScript fetch
 */


// ==============================================================================
// CONFIGURATION
// ==============================================================================

const API_URL = 'https://api.example.com/v1/devices/12345';  // Device ID in URL
const API_TOKEN = '<YOUR_JWT_TOKEN_HERE>';


// ==============================================================================
// PATCH REQUEST FUNCTION
// ==============================================================================

const updateDevice = async () => {
  
  try {
    // Make PATCH request (partial update - only changed fields)
    const response = await fetch(API_URL, {
      method: 'PATCH',                              // HTTP method for partial update
      
      headers: {
        'Content-Type': 'application/json',      // Sending JSON data
        'Authorization': `Bearer ${API_TOKEN}`   // Auth token
      },
      
      body: JSON.stringify({
        status: 'inactive',                       // Only update status field
        location: 'Building B'                    // Only update location field
      })
    });
    
    // Check if request was successful
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    // Parse JSON response
    const data = await response.json();
    
    // Display results
    console.log(`āœ… Status: ${response.status}`);
    console.log('šŸ“¦ Updated Device:', data);
    
    return data;
    
  } catch (error) {
    console.error('āŒ Error:', error.message);
    throw error;
  }
};


// ==============================================================================
// MAIN EXECUTION
// ==============================================================================

updateDevice()
  .then(device => console.log('✨ Update complete!', device))
  .catch(err => console.error('āš ļø Update failed:', err));

Query Parameters (Python Script Format)

Filter, sort, or paginate results using query parameters:

#!/usr/bin/env python3
"""
Query Parameters Examples
Demonstrates filtering, sorting, and pagination in API requests
"""

import requests


# ==============================================================================
# CONFIGURATION
# ==============================================================================

BASE_URL = "https://api.example.com/v1/devices"
HEADERS = {
    "Authorization": "Bearer <YOUR_TOKEN>",
    "Accept": "application/json"
}


# ==============================================================================
# Example 1: Filter by Status
# ==============================================================================

params = {"status": "active"}                       # Only active devices
response = requests.get(BASE_URL, headers=HEADERS, params=params)
print(f"URL: {response.url}")                      # Shows: ?status=active
print(f"Found: {len(response.json())} devices\n")


# ==============================================================================
# Example 2: Pagination
# ==============================================================================

params = {
    "page": 2,                                   # Get page 2
    "limit": 50                                  # 50 items per page
}
response = requests.get(BASE_URL, headers=HEADERS, params=params)
print(f"URL: {response.url}")                      # Shows: ?page=2&limit=50
print(f"Page 2: {len(response.json())} devices\n")


# ==============================================================================
# Example 3: Sorting
# ==============================================================================

params = {
    "sort": "name",                              # Sort by name field
    "order": "asc"                               # Ascending order
}
response = requests.get(BASE_URL, headers=HEADERS, params=params)
print(f"URL: {response.url}")                      # Shows: ?sort=name&order=asc
print(f"Sorted: {len(response.json())} devices\n")


# ==============================================================================
# Example 4: Multiple Filters Combined
# ==============================================================================

params = {
    "status": "active",                          # Only active
    "location": "Building A",                    # In Building A
    "type": "router"                             # Router type only
}
response = requests.get(BASE_URL, headers=HEADERS, params=params)
print(f"URL: {response.url}")
# Shows: ?status=active&location=Building+A&type=router
print(f"Filtered: {len(response.json())} devices")

HTTP Status Codes

Code Meaning Description
200 OK Request succeeded
201 Created Resource successfully created
204 No Content Success but no content to return
400 Bad Request Invalid request syntax or parameters
401 Unauthorized Authentication required or failed
403 Forbidden Authenticated but not authorized
404 Not Found Resource doesn't exist
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server-side error occurred
šŸ’” Best Practice: Always check the API documentation for the exact endpoint structure, required headers, authentication method, and expected request/response format.

šŸ”” 2.2 Common Usage Patterns Related to Webhooks

What are Webhooks?

Webhooks are HTTP callbacks that allow applications to send real-time data to other applications when specific events occur. Instead of continuously polling for data (asking "did something happen?"), webhooks push data to your application when events happen.

šŸ“Œ Key Concept: Webhooks follow an event-driven architecture pattern. You register a URL endpoint, and the service calls that URL when an event occurs.

Polling vs Webhooks

Aspect Polling Webhooks
Communication Pull - Client requests data Push - Server sends data
Frequency Periodic requests Event-triggered
Efficiency Less efficient (many empty responses) More efficient (only when needed)
Latency Higher (depends on polling interval) Lower (real-time notification)
Resource Usage Higher (continuous requests) Lower (event-based)

Common Webhook Usage Patterns

1. Event Notification Pattern

Receive notifications when specific events occur:

  • GitHub: Notify when code is pushed, PR is created, issue is opened
  • Stripe: Alert on payment success, failure, or refund
  • Meraki: Network device status changes
  • Webex: New message in room, user joins meeting

2. Data Synchronization Pattern

Keep systems in sync by receiving updates:

  • Update local database when remote data changes
  • Sync user information across multiple systems
  • Maintain inventory consistency across platforms

3. Workflow Automation Pattern

Trigger automated workflows based on events:

  • Deploy application when code is merged
  • Send notifications to Slack/Teams when alert occurs
  • Create ticket in ITSM when device goes down
  • Run backup when configuration changes

Webhook Implementation

Setting Up a Webhook Endpoint (Python/Flask Script)

#!/usr/bin/env python3
"""
Webhook Endpoint with Security
Flask application that receives and validates webhook POST requests
"""

from flask import Flask, request, jsonify
import hashlib
import hmac
import logging


# ==============================================================================
# CONFIGURATION
# ==============================================================================

WEBHOOK_SECRET = "your_secret_key_here"      # Shared secret (keep secure!)

app = Flask(__name__)

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


# ==============================================================================
# SECURITY FUNCTIONS
# ==============================================================================

def verify_signature(payload, signature):
    """
    Verify webhook signature using HMAC-SHA256
    
    Args:
        payload (bytes): Raw request body
        signature (str): Signature from request header
    
    Returns:
        bool: True if signature is valid
    """
    if not signature:
        return False
    
    # Calculate expected signature
    expected = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    # Securely compare (prevents timing attacks)
    return hmac.compare_digest(signature, expected)


# ==============================================================================
# EVENT HANDLERS
# ==============================================================================

def handle_device_status(data):
    """Handle device status change event"""
    device_name = data.get('device_name')
    new_status = data.get('status')
    logger.info(f"Device {device_name} status changed to {new_status}")
    # Add your business logic here


def handle_alert(data):
    """Handle alert triggered event"""
    alert_type = data.get('alert_type')
    severity = data.get('severity')
    logger.warning(f"Alert: {alert_type} (Severity: {severity})")
    # Add your notification logic here


# ==============================================================================
# WEBHOOK ENDPOINT
# ==============================================================================

@app.route('/webhook', methods=['POST'])
def webhook_handler():
    """
    Main webhook endpoint that receives POST requests
    
    Returns:
        JSON response with status code
    """
    
    # Get signature from header
    signature = request.headers.get('X-Hub-Signature')
    
    # Verify webhook authenticity
    if not verify_signature(request.data, signature):
        logger.warning("Invalid signature")
        return jsonify({'error': 'Invalid signature'}), 401
    
    # Parse JSON payload
    try:
        data = request.json
    except Exception as err:
        logger.error(f"Invalid JSON: {err}")
        return jsonify({'error': 'Invalid JSON'}), 400
    
    # Get event type
    event_type = request.headers.get('X-Event-Type', 'unknown')
    logger.info(f"Received: {event_type}")
    
    # Route to appropriate handler
    if event_type == 'device.status.changed':
        handle_device_status(data)
    elif event_type == 'alert.triggered':
        handle_alert(data)
    else:
        logger.warning(f"Unknown event: {event_type}")
    
    # Return success immediately
    return jsonify({'status': 'success', 'event_type': event_type}), 200


# ==============================================================================
# HEALTH CHECK ENDPOINT
# ==============================================================================

@app.route('/health', methods=['GET'])
def health_check():
    """Health check endpoint"""
    return jsonify({'status': 'healthy'}), 200


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    print("šŸš€ Starting webhook server...")
    print("šŸ“ Webhook: http://localhost:5000/webhook")
    print("šŸ’š Health: http://localhost:5000/health")
    
    app.run(host='0.0.0.0', port=5000, debug=True)

Registering a Webhook (Python Script)

#!/usr/bin/env python3
"""
Register Webhook with API Service
Subscribe to webhook events by registering your endpoint URL
"""

import requests
import json


# ==============================================================================
# CONFIGURATION
# ==============================================================================

WEBHOOK_API_URL = "https://api.service.com/v1/webhooks"
YOUR_API_TOKEN = "<YOUR_BEARER_TOKEN>"
YOUR_WEBHOOK_URL = "https://your-domain.com/webhook"     # Must be publicly accessible
WEBHOOK_SECRET = "your_webhook_secret_here"


# ==============================================================================
# WEBHOOK REGISTRATION FUNCTION
# ==============================================================================

def register_webhook(webhook_url, events, secret):
    """
    Register webhook with API service
    
    Args:
        webhook_url (str): Your endpoint URL
        events (list): Event types to subscribe to
        secret (str): Shared secret for verification
    
    Returns:
        dict: Registration response or None
    """
    
    headers = {
        "Authorization": f"Bearer {YOUR_API_TOKEN}",
        "Content-Type": "application/json"
    }
    
    payload = {
        "url": webhook_url,
        "events": events,
        "secret": secret
    }
    
    try:
        print(f"šŸ“¤ Registering webhook...")
        print(f"šŸŽÆ URL: {webhook_url}")
        print(f"šŸ“‹ Events: {', '.join(events)}")
        
        response = requests.post(
            WEBHOOK_API_URL,
            headers=headers,
            json=payload,
            timeout=10
        )
        
        response.raise_for_status()
        
        webhook_data = response.json()
        print(f"\nāœ… Webhook registered!")
        print(f"šŸ†” ID: {webhook_data.get('id', 'N/A')}")
        print(json.dumps(webhook_data, indent=2))
        
        return webhook_data
        
    except requests.exceptions.HTTPError as err:
        print(f"āŒ HTTP Error: {err}")
        
    except requests.exceptions.RequestException as err:
        print(f"āš ļø Request Error: {err}")
        
    except Exception as err:
        print(f"āŒ Error: {err}")
    
    return None


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    events = ["device.created", "device.updated", "device.deleted"]
    
    result = register_webhook(YOUR_WEBHOOK_URL, events, WEBHOOK_SECRET)
    
    if result:
        print("\n✨ Webhook is now active!")
    else:
        print("\nāš ļø Registration failed!")

Webhook Best Practices

āœ… Security Best Practices:
  • Verify signatures: Always validate webhook signatures using HMAC
  • Use HTTPS: Ensure webhook endpoints use secure connections
  • Validate payload: Check data structure and types
  • IP whitelisting: Restrict access to known IP addresses
⚔ Performance Best Practices:
  • Respond quickly: Return 200 OK immediately, process asynchronously
  • Implement retry logic: Handle failed webhook deliveries
  • Use queues: Queue webhook processing for heavy operations
  • Idempotency: Handle duplicate webhook deliveries gracefully

Example: Handling Retries and Idempotency (Python Script)

#!/usr/bin/env python3
"""
Idempotent Webhook Handler with Redis
Prevents duplicate processing of webhook events
"""

from flask import Flask, request, jsonify
import redis
import logging
from datetime import datetime


# ==============================================================================
# CONFIGURATION
# ==============================================================================

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Redis connection for event tracking
redis_client = redis.Redis(
    host='localhost',
    port=6379,
    db=0,
    decode_responses=True
)

EVENT_TTL = 86400  # 24 hours in seconds


# ==============================================================================
# IDEMPOTENCY FUNCTIONS
# ==============================================================================

def is_event_processed(event_id):
    """Check if event was already processed"""
    key = f'webhook:{event_id}'
    return redis_client.exists(key) > 0


def mark_event_processed(event_id):
    """Mark event as processed in Redis with TTL"""
    key = f'webhook:{event_id}'
    value = datetime.now().isoformat()
    redis_client.setex(key, EVENT_TTL, value)
    logger.info(f"Marked event {event_id} as processed")


# ==============================================================================
# WEBHOOK ENDPOINT
# ==============================================================================

@app.route('/webhook', methods=['POST'])
def webhook():
    """Idempotent webhook handler - ensures events processed only once"""
    
    # Get unique event ID
    event_id = request.headers.get('X-Event-ID')
    
    if not event_id:
        return jsonify({'error': 'Missing X-Event-ID header'}), 400
    
    # Check if already processed
    if is_event_processed(event_id):
        logger.info(f"Event {event_id} already processed")
        return jsonify({'status': 'already_processed'}), 200
    
    # Mark as processed
    mark_event_processed(event_id)
    
    # Process webhook data
    try:
        data = request.json
        logger.info(f"Processing: {data}")
        # TODO: Add to message queue here
    except Exception as err:
        logger.error(f"Error: {err}")
    
    # Return success immediately
    return jsonify({'status': 'received', 'event_id': event_id}), 200


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    print("šŸ”” Webhook Server with Idempotency")
    print("šŸ“ Endpoint: http://localhost:5000/webhook")
    print("šŸ”„ Redis-based idempotency enabled\n")
    
    app.run(host='0.0.0.0', port=5000, debug=True)

Testing Webhooks

Tools for testing webhook implementations:

  • ngrok: Expose local server to internet for testing
  • webhook.site: Inspect webhook payloads
  • RequestBin: Collect and inspect HTTP requests
  • Postman: Send test webhook payloads
#!/usr/bin/env python3
"""
Testing Webhooks Locally with ngrok
Step-by-step guide and Python code for webhook testing
"""

import requests


# ==============================================================================
# STEP 1: Install ngrok
# ==============================================================================
# Download: https://ngrok.com/download
# Or use package manager:
#   Windows: choco install ngrok
#   Mac: brew install ngrok
#   Linux: snap install ngrok


# ==============================================================================
# STEP 2: Run local webhook server (Terminal 1)
# ==============================================================================
# python webhook_server.py
# Server runs on: http://localhost:5000


# ==============================================================================
# STEP 3: Start ngrok tunnel (Terminal 2)
# ==============================================================================
# Command: ngrok http 5000
# Creates tunnel: public URL -> localhost:5000


# ==============================================================================
# STEP 4: Get public URL from ngrok output
# ==============================================================================
# ngrok displays:
# Forwarding: https://abc123.ngrok.io -> http://localhost:5000
# Use: https://abc123.ngrok.io/webhook


# ==============================================================================
# STEP 5: Register webhook using ngrok URL
# ==============================================================================

def register_ngrok_webhook(ngrok_url):
    """Register ngrok URL as webhook endpoint"""
    
    webhook_config = {
        "url": f"{ngrok_url}/webhook",
        "events": ["device.created", "device.updated"],
        "secret": "test-secret-123"
    }
    
    response = requests.post(
        "https://api.service.com/v1/webhooks",
        headers={"Authorization": "Bearer <YOUR_TOKEN>"},
        json=webhook_config
    )
    
    return response.json()


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    # Replace with your actual ngrok URL
    ngrok_url = "https://abc123def456.ngrok.io"
    
    print(f"🌐 Registering webhook: {ngrok_url}")
    
    result = register_ngrok_webhook(ngrok_url)
    
    print(f"āœ… Registered!")
    print(f"šŸ“ Receiving at: {ngrok_url}/webhook")
    print(f"šŸ’” Keep ngrok running!")

āš ļø 2.3 Identify Constraints When Consuming APIs

Overview

When working with APIs, various constraints can affect how you design and implement your applications. Understanding these limitations is crucial for building reliable and efficient systems.

1. Rate Limiting

Rate limiting restricts the number of API calls a client can make within a specific time period.

Common Rate Limit Types:

Type Description Example
Per Second Limits requests per second 10 requests/second
Per Minute Limits requests per minute 100 requests/minute
Per Hour Limits requests per hour 1,000 requests/hour
Per Day Daily request quota 10,000 requests/day
Concurrent Simultaneous connections 5 concurrent requests

Rate Limit Headers:

# Common rate limit response headers returned by API

X-RateLimit-Limit: 100
# Total number of requests allowed in the current time window

X-RateLimit-Remaining: 75
# Number of requests remaining in current time window

X-RateLimit-Reset: 1640995200
# Unix timestamp when the rate limit will reset

Retry-After: 60
# Number of seconds to wait before making another request

Handling Rate Limits in Code:

#!/usr/bin/env python3
"""
Rate Limit Handler with Auto-Retry
Handles 429 errors and respects Retry-After headers
"""

import time
import requests
from requests.exceptions import HTTPError


# ==============================================================================
# API CALL WITH RETRY LOGIC
# ==============================================================================

def api_call_with_retry(url, headers, max_retries=3):
    """Make API call with automatic retry on rate limit"""
    
    for attempt in range(max_retries):
        try:
            # Make GET request
            response = requests.get(url, headers=headers)
            response.raise_for_status()              # Raise error if 4xx/5xx
            
            # Check remaining rate limit
            remaining = int(response.headers.get('X-RateLimit-Remaining', 1))
            
            if remaining < 10:
                print(f"āš ļø Warning: Only {remaining} requests remaining")
            
            return response.json()          # Return parsed response
            
        except HTTPError as err:
            if err.response.status_code == 429:         # Rate limited
                retry_after = int(err.response.headers.get('Retry-After', 60))
                print(f"ā±ļø Rate limited. Waiting {retry_after}s (Attempt {attempt+1}/{max_retries})")
                time.sleep(retry_after)
            else:
                raise                           # Different error, re-raise
    
    raise Exception("Max retries exceeded")


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    url = "https://api.example.com/v1/devices"
    headers = {"Authorization": "Bearer <YOUR_TOKEN>"}
    
    try:
        data = api_call_with_retry(url, headers)
        print(f"āœ… Success! Retrieved {len(data)} devices")
    except Exception as err:
        print(f"āŒ Failed: {err}")

2. Authentication and Authorization

Common Authentication Methods:

Method How It Works Pros/Cons
API Keys Static key in header/query āœ… Simple
āŒ Less secure, no expiration
OAuth 2.0 Token-based with refresh āœ… Secure, scoped access
āŒ Complex setup
JWT JSON Web Tokens āœ… Stateless, portable
āŒ Can't revoke easily
Basic Auth Base64 encoded username:password āœ… Simple
āŒ Insecure without HTTPS

Token Management Example:

#!/usr/bin/env python3
"""
OAuth Token Management with Auto-Refresh
Automatically refreshes expired tokens
"""

import requests
import time


# ==============================================================================
# API CLIENT WITH TOKEN MANAGEMENT
# ==============================================================================

class APIClient:
    """API Client with automatic token refresh"""
    
    def __init__(self, client_id, client_secret):
        self.client_id = client_id
        self.client_secret = client_secret
        self.access_token = None                # Will be fetched on first use
        self.token_expiry = 0                   # Unix timestamp
    
    
    def get_token(self):
        """Get valid token (fetches new one if expired)"""
        
        # Check if current token is still valid
        if self.access_token and time.time() < self.token_expiry:
            return self.access_token          # Use existing token
        
        # Request new token from OAuth endpoint
        response = requests.post(
            'https://api.example.com/oauth/token',
            data={
                'grant_type': 'client_credentials',
                'client_id': self.client_id,
                'client_secret': self.client_secret
            }
        )
        
        data = response.json()
        
        # Save new token and expiry
        self.access_token = data['access_token']
        self.token_expiry = time.time() + data['expires_in'] - 60  # 60s buffer
        
        print(f"šŸ”‘ New token acquired (expires in {data['expires_in']}s)")
        return self.access_token


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    # Create client
    client = APIClient(
        client_id="your_client_id",
        client_secret="your_client_secret"
    )
    
    # Get token (will fetch new one)
    token = client.get_token()
    print(f"Token: {token[:20]}...")
    
    # Get token again (will reuse existing)
    token2 = client.get_token()
    print(f"Same token reused: {token == token2}")

3. Payload Size Limits

APIs often impose limits on request and response sizes:

āš ļø Common Size Constraints:
  • Request body: Often 1MB - 10MB maximum
  • URL length: Typically 2048 characters for GET requests
  • Response size: May be limited to prevent memory issues
  • File uploads: Separate limits for media/attachments

Handling Large Datasets with Pagination:

#!/usr/bin/env python3
"""
Pagination Handler
Retrieves all data from paginated API endpoints
"""

import requests


# ==============================================================================
# PAGINATION FUNCTION
# ==============================================================================

def get_all_devices(base_url, headers):
    """Fetch all devices across all pages"""
    
    all_devices = []                         # Accumulator for all devices
    page = 1                                    # Start at page 1
    per_page = 100                              # Max items per page
    
    while True:
        # Request current page
        response = requests.get(
            f"{base_url}/devices",
            params={'page': page, 'per_page': per_page},
            headers=headers
        )
        
        data = response.json()                 # Parse response
        devices = data['devices']              # Extract devices array
        
        all_devices.extend(devices)           # Add to collection
        print(f"šŸ“„ Page {page}: {len(devices)} devices")
        
        # Check if last page
        if len(devices) < per_page:           # Fewer items = last page
            break
        
        page += 1                               # Next page
    
    return all_devices


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    base_url = "https://api.example.com"
    headers = {"Authorization": "Bearer <YOUR_TOKEN>"}
    
    print("šŸ”„ Fetching all devices with pagination...\n")
    devices = get_all_devices(base_url, headers)
    
    print(f"\nāœ… Total devices retrieved: {len(devices)}")

4. Timeout Constraints

APIs may terminate long-running requests:

  • Connection timeout: Time to establish connection (typically 10-30s)
  • Read timeout: Time to receive response (typically 30-120s)
  • Total timeout: Overall request duration limit
#!/usr/bin/env python3
"""
Timeout Configuration Example
Demonstrates setting connection and read timeouts
"""

import requests


# ==============================================================================
# CONFIGURATION
# ==============================================================================

url = "https://api.example.com/v1/devices"
headers = {"Authorization": "Bearer <YOUR_TOKEN>"}


# ==============================================================================
# REQUEST WITH TIMEOUT
# ==============================================================================

try:
    response = requests.get(
        url,
        headers=headers,
        timeout=(10, 30)                       # (connect_timeout, read_timeout)
        # 10s to establish connection, 30s to receive response
    )
    
    print(f"āœ… Status: {response.status_code}")
    print(response.json())
    
except requests.exceptions.Timeout:
    print("ā±ļø Timeout: Server took too long to respond")
    
except requests.exceptions.ConnectionError:
    print("āŒ Connection Error: Check network/URL")
    
except requests.exceptions.RequestException as err:
    print(f"āš ļø Error: {err}")

5. API Versioning

APIs evolve over time, requiring version management:

Versioning Strategies:

Method Example Description
URL Path /api/v1/devices Most common, explicit
Header API-Version: 2.0 Clean URLs, flexible
Query Param /api/devices?version=1 Simple but clutters URL
Content Type Accept: application/vnd.api.v1+json RESTful but complex

Best Practices for Handling Constraints

āœ… Recommended Practices:
  1. Implement caching: Reduce API calls with local caching
  2. Use pagination: Request data in manageable chunks
  3. Implement retry logic: Handle transient failures gracefully
  4. Monitor usage: Track API consumption and limits
  5. Implement circuit breakers: Prevent cascading failures
  6. Use webhooks: Replace polling where possible
  7. Batch requests: Combine multiple operations when supported
  8. Handle errors gracefully: Provide meaningful error messages

Complete Example: Resilient API Client (Python Script)

#!/usr/bin/env python3
"""
Resilient API Client with Retry, Caching, and Rate Limiting
Production-ready API client with all best practices
"""

import requests
import time
import logging
from cachetools import TTLCache


# ==============================================================================
# CONFIGURATION
# ==============================================================================

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


# ==============================================================================
# RESILIENT API CLIENT CLASS
# ==============================================================================

class ResilientAPIClient:
    """API Client with retry, caching, and rate limiting"""
    
    def __init__(self, base_url, api_key, cache_ttl=300, max_retries=3):
        """Initialize API client with configuration"""
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.max_retries = max_retries
        self.cache = TTLCache(maxsize=100, ttl=cache_ttl)
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        })
    
    
    def _make_request(self, method, endpoint, **kwargs):
        """Make HTTP request with retry and rate limit handling"""
        url = f"{self.base_url}{endpoint}"
        backoff = 1
        
        for attempt in range(1, self.max_retries + 1):
            try:
                response = self.session.request(method, url, timeout=(10, 30), **kwargs)
                
                # Handle rate limiting
                if response.status_code == 429:
                    retry_after = int(response.headers.get('Retry-After', backoff))
                    logger.warning(f"Rate limited. Waiting {retry_after}s...")
                    time.sleep(retry_after)
                    backoff *= 2
                    continue
                
                response.raise_for_status()
                return response.json()
                
            except requests.exceptions.Timeout:
                if attempt == self.max_retries:
                    raise
                time.sleep(backoff)
                backoff *= 2
                
            except requests.exceptions.RequestException as err:
                logger.error(f"Request failed: {err}")
                raise
        
        raise Exception(f"Failed after {self.max_retries} attempts")
    
    
    def get(self, endpoint, use_cache=True, **kwargs):
        """Make GET request with optional caching"""
        if use_cache and endpoint in self.cache:
            logger.debug(f"Cache hit: {endpoint}")
            return self.cache[endpoint]
        
        result = self._make_request('GET', endpoint, **kwargs)
        
        if use_cache:
            self.cache[endpoint] = result
        
        return result
    
    
    def post(self, endpoint, data=None, **kwargs):
        """Make POST request"""
        return self._make_request('POST', endpoint, json=data, **kwargs)
    
    
    def patch(self, endpoint, data=None, **kwargs):
        """Make PATCH request"""
        return self._make_request('PATCH', endpoint, json=data, **kwargs)
    
    
    def delete(self, endpoint, **kwargs):
        """Make DELETE request"""
        return self._make_request('DELETE', endpoint, **kwargs)


# ==============================================================================
# MAIN EXECUTION
# ==============================================================================

if __name__ == "__main__":
    # Initialize client
    client = ResilientAPIClient(
        base_url="https://api.example.com",
        api_key="<YOUR_API_KEY>",
        cache_ttl=300,
        max_retries=3
    )
    
    try:
        # GET request with caching
        print("šŸ“” Making GET request...")
        devices = client.get("/v1/devices")
        print(f"āœ… Retrieved {len(devices)} devices")
        
        # Same request (from cache)
        devices_cached = client.get("/v1/devices")
        print(f"⚔ Cached: {len(devices_cached)} devices")
        
        # POST request
        new_device = client.post("/v1/devices", data={
            "name": "Router-10",
            "ip": "10.0.0.10"
        })
        print(f"āœ… Created: {new_device.get('name')}")
        
    except Exception as err:
        print(f"āŒ Error: {err}")
    
    print("\n✨ Demo completed!")

šŸ Interactive Python Interpreter

šŸ’» Practice API Concepts: Use this Python interpreter to practice REST API calls, JSON parsing, and other API-related concepts!

Sample Code Templates

āœļø Python Code Editor
šŸ“¤ Output
ā³ Loading Python interpreter...
✨ Features:
  • Practice JSON data manipulation
  • Test Python dictionaries and lists
  • Simulate API response handling
  • Learn Python basics for DevNet
āš ļø Note: This interpreter runs in the browser and cannot make actual HTTP requests. Use it to practice data manipulation and Python syntax.
← Back to Topics