Webhook Security: Protecting Your Endpoints from the Wild West

Webhooks are HTTP endpoints that receive data from external services. Anyone who discovers your webhook URL can send requests to it. That’s a problem. Here’s how to secure them properly. The Threat Model Your webhook endpoint faces several threats: Spoofing: Attacker sends fake payloads pretending to be Stripe/GitHub/etc. Replay attacks: Attacker captures a legitimate request and resends it Tampering: Attacker intercepts and modifies payloads in transit Enumeration: Attacker discovers your webhook URLs through guessing Denial of service: Attacker floods your endpoint with requests Signature Verification Most webhook providers sign their payloads. Always verify: ...

March 1, 2026 Â· 5 min Â· 968 words Â· Rob Washington

LLM API Integration Patterns for Production Applications

Integrating LLMs into production applications is deceptively simple. Call an API, get text back. But building reliable, cost-effective systems requires more thought. Here are patterns that work at scale. The Basic Call Every LLM integration starts here: 1 2 3 4 5 6 7 8 import openai def complete(prompt: str) -> str: response = openai.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}] ) return response.choices[0].message.content This works for prototypes. Production needs more. Retry with Exponential Backoff LLM APIs have rate limits and occasional failures: ...

March 1, 2026 Â· 5 min Â· 1002 words Â· Rob Washington

Idempotency: Making APIs Safe to Retry

Networks fail. Clients timeout. Users double-click. If your API creates duplicate orders or charges cards twice when this happens, you have a problem. Idempotency is the solution—making operations safe to retry without side effects. What Is Idempotency? An operation is idempotent if performing it multiple times has the same effect as performing it once. # G P D # P P E U E O O I T T L N S S d E O T T e / / T T m u u E p s s i u o e e / d r s t r r u e d e e s s s m e r n / / e p r s t 1 1 r o s / : 2 2 s t 1 3 3 / e 2 S 1 n 3 a { 2 t / m . 3 : c e . h . D a r } i r e f g s f e u e l r t # # # e # # n e A A U t C C v l l s r h e w w e r e a r a a r e a r y y y s t g s s 1 u e e t 2 l s s i r s 3 t m e e a t e t t i e h u s s a N e r c E n u g h W c s s o a e n t o r u r e i r d s m d e 1 ( e e a r 2 a r g 3 l a 1 r e i 2 t e a n 3 o a c d h e t y a h t c i g i h s o m n e t s e i t m a = e t e s t i l l g o n e ) The Problem Client sends request → Server processes it → Response lost in transit: ...

March 1, 2026 Â· 7 min Â· 1451 words Â· Rob Washington

API Pagination Patterns: Offset, Cursor, and Keyset

Every API that returns lists needs pagination. Without it, a request for “all users” could return millions of rows, crushing your database and timing out the client. But pagination has tradeoffs—and choosing wrong can hurt performance or cause data inconsistencies. Offset Pagination The classic approach. Simple to implement, simple to understand: G G G E E E T T T / / / u u u s s s e e e r r r s s s ? ? ? l l l i i i m m m i i i t t t = = = 2 2 2 0 0 0 & & & o o o f f f f f f s s s e e e t t t = = = 0 2 4 0 0 # # # F S T i e h r c i s o r t n d d p p a p a g a g e g e e 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @app.get("/users") def list_users(limit: int = 20, offset: int = 0): users = db.query( "SELECT * FROM users ORDER BY id LIMIT %s OFFSET %s", (limit, offset) ) total = db.query("SELECT COUNT(*) FROM users")[0][0] return { "data": users, "pagination": { "limit": limit, "offset": offset, "total": total } } Pros: ...

March 1, 2026 Â· 7 min Â· 1286 words Â· Rob Washington

API Error Handling That Helps Instead of Frustrates

Bad error handling wastes everyone’s time. A cryptic “Error 500” sends developers on a debugging odyssey. A well-designed error response tells them exactly what went wrong and how to fix it. Here’s how to build the latter. The Anatomy of a Good Error Every error response should answer three questions: What happened? (error code/type) Why? (human-readable message) How do I fix it? (actionable guidance) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": [ { "field": "email", "message": "Invalid email format", "received": "not-an-email" }, { "field": "age", "message": "Must be a positive integer", "received": "-5" } ], "documentation_url": "https://api.example.com/docs/errors#VALIDATION_ERROR" }, "request_id": "req_abc123" } Always include: ...

March 1, 2026 Â· 6 min Â· 1214 words Â· Rob Washington

Rate Limiting Strategies That Protect Without Frustrating

Rate limiting is the bouncer at your API’s door. Too strict, and legitimate users get frustrated. Too loose, and one bad actor can take down your service. Here’s how to find the balance. Why Rate Limit? Without limits, a single client can: Exhaust your database connections Burn through your third-party API quotas Inflate your cloud bill Deny service to everyone else Rate limiting isn’t about being restrictive—it’s about being fair. ...

March 1, 2026 Â· 5 min Â· 1047 words Â· Rob Washington

Webhook Patterns: Receiving Events Reliably

Webhooks flip the API model: instead of polling, the service calls you. Here’s how to handle them without losing events. Basic Webhook Handler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from fastapi import FastAPI, Request, HTTPException import hmac import hashlib app = FastAPI() @app.post("/webhooks/stripe") async def stripe_webhook(request: Request): payload = await request.body() sig_header = request.headers.get("Stripe-Signature") # Verify signature first if not verify_stripe_signature(payload, sig_header): raise HTTPException(status_code=400, detail="Invalid signature") event = json.loads(payload) # Process event if event["type"] == "payment_intent.succeeded": handle_payment_success(event["data"]["object"]) # Always return 200 quickly return {"received": True} Signature Verification Never trust unverified webhooks. ...

February 28, 2026 Â· 5 min Â· 889 words Â· Rob Washington

API Versioning Strategies That Don't Hurt

Your API will change. How you handle that change determines whether clients curse your name or barely notice. The Three Approaches 1. URL Path Versioning G G E E T T v 1 2 / / u u s s e e r r s s / / 1 1 2 2 3 3 Pros: ...

February 28, 2026 Â· 4 min Â· 791 words Â· Rob Washington

curl Patterns for API Development and Testing

curl is the universal HTTP client. Every developer should know it well—for debugging, testing, and automation. Basic Requests 1 2 3 4 5 6 7 8 9 10 11 # GET (default) curl https://api.example.com/users # POST with data curl -X POST https://api.example.com/users \ -d '{"name":"alice"}' # With headers curl -H "Content-Type: application/json" \ -H "Authorization: Bearer token123" \ https://api.example.com/users JSON APIs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # POST JSON (sets Content-Type automatically) curl -X POST https://api.example.com/users \ --json '{"name":"alice","email":"alice@example.com"}' # Or explicitly curl -X POST https://api.example.com/users \ -H "Content-Type: application/json" \ -d '{"name":"alice"}' # Pretty print response curl -s https://api.example.com/users | jq . # From file curl -X POST https://api.example.com/users \ -H "Content-Type: application/json" \ -d @payload.json HTTP Methods 1 2 3 4 5 6 7 curl -X GET https://api.example.com/users/1 curl -X POST https://api.example.com/users -d '...' curl -X PUT https://api.example.com/users/1 -d '...' curl -X PATCH https://api.example.com/users/1 -d '...' curl -X DELETE https://api.example.com/users/1 curl -X HEAD https://api.example.com/users # Headers only curl -X OPTIONS https://api.example.com/users # CORS preflight Authentication 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Bearer token curl -H "Authorization: Bearer eyJhbGc..." \ https://api.example.com/me # Basic auth curl -u username:password https://api.example.com/users # Or curl -H "Authorization: Basic $(echo -n 'user:pass' | base64)" \ https://api.example.com/users # API key in header curl -H "X-API-Key: secret123" https://api.example.com/data # API key in query curl "https://api.example.com/data?api_key=secret123" Response Inspection 1 2 3 4 5 6 7 8 9 10 11 12 13 # Show headers curl -I https://api.example.com/users # HEAD request curl -i https://api.example.com/users # Include headers with body # Verbose (see full request/response) curl -v https://api.example.com/users # Just status code curl -s -o /dev/null -w "%{http_code}" https://api.example.com/users # Multiple stats curl -s -o /dev/null -w "code: %{http_code}\ntime: %{time_total}s\nsize: %{size_download} bytes\n" \ https://api.example.com/users File Uploads 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # Form upload curl -X POST https://api.example.com/upload \ -F "file=@document.pdf" # Multiple files curl -X POST https://api.example.com/upload \ -F "file1=@doc1.pdf" \ -F "file2=@doc2.pdf" # With additional form fields curl -X POST https://api.example.com/upload \ -F "file=@photo.jpg" \ -F "description=Profile photo" \ -F "public=true" # Binary data curl -X POST https://api.example.com/upload \ -H "Content-Type: application/octet-stream" \ --data-binary @file.bin Handling Redirects 1 2 3 4 5 6 7 8 # Follow redirects curl -L https://example.com/shortened-url # Show redirect chain curl -L -v https://example.com/shortened-url 2>&1 | grep "< location" # Limit redirects curl -L --max-redirs 3 https://example.com/url Timeouts and Retries 1 2 3 4 5 6 7 8 9 10 11 # Connection timeout (seconds) curl --connect-timeout 5 https://api.example.com/ # Max time for entire operation curl --max-time 30 https://api.example.com/slow-endpoint # Retry on failure curl --retry 3 --retry-delay 2 https://api.example.com/ # Retry on specific errors curl --retry 3 --retry-all-errors https://api.example.com/ Saving Output 1 2 3 4 5 6 7 8 9 10 11 # Save to file curl -o response.json https://api.example.com/users # Use remote filename curl -O https://example.com/file.zip # Save headers separately curl -D headers.txt -o body.json https://api.example.com/users # Append to file curl https://api.example.com/users >> all_responses.json Cookie Handling 1 2 3 4 5 6 7 8 9 10 11 12 # Send cookies curl -b "session=abc123; token=xyz" https://api.example.com/ # Save cookies to file curl -c cookies.txt https://api.example.com/login \ -d "user=admin&pass=secret" # Use saved cookies curl -b cookies.txt https://api.example.com/dashboard # Both save and send curl -b cookies.txt -c cookies.txt https://api.example.com/ Testing Webhooks 1 2 3 4 5 6 7 8 9 10 11 12 # Simulate GitHub webhook curl -X POST http://localhost:3000/webhook \ -H "Content-Type: application/json" \ -H "X-GitHub-Event: push" \ -H "X-Hub-Signature-256: sha256=..." \ -d @github-payload.json # Stripe webhook curl -X POST http://localhost:3000/stripe/webhook \ -H "Content-Type: application/json" \ -H "Stripe-Signature: t=...,v1=..." \ -d @stripe-event.json SSL/TLS Options 1 2 3 4 5 6 7 8 # Skip certificate verification (development only!) curl -k https://self-signed.example.com/ # Use specific CA bundle curl --cacert /path/to/ca-bundle.crt https://api.example.com/ # Client certificate curl --cert client.crt --key client.key https://mtls.example.com/ Proxy Configuration 1 2 3 4 5 6 7 8 # HTTP proxy curl -x http://proxy:8080 https://api.example.com/ # SOCKS5 proxy curl --socks5 localhost:1080 https://api.example.com/ # No proxy for specific hosts curl --noproxy "localhost,*.internal" https://api.example.com/ Performance Testing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Time breakdown curl -s -o /dev/null -w "\ namelookup: %{time_namelookup}s\n\ connect: %{time_connect}s\n\ appconnect: %{time_appconnect}s\n\ pretransfer: %{time_pretransfer}s\n\ redirect: %{time_redirect}s\n\ starttransfer: %{time_starttransfer}s\n\ total: %{time_total}s\n" \ https://api.example.com/ # Loop for load testing (basic) for i in {1..100}; do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" \ https://api.example.com/ done Config Files Create ~/.curlrc for defaults: ...

February 28, 2026 Â· 6 min Â· 1116 words Â· Rob Washington

Getting Structured Data from LLMs: JSON Mode and Beyond

The biggest challenge with LLMs in production isn’t getting good responses—it’s getting parseable responses. When you need JSON for your pipeline, “Here’s the data you requested:” followed by markdown-wrapped output breaks everything. Here’s how to reliably extract structured data. The Problem 1 2 3 4 5 6 7 8 response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": "Extract the person's name and age from: 'John Smith is 34 years old'"}] ) print(response.choices[0].message.content) # "The person's name is John Smith and their age is 34." # ... not what we needed You wanted {"name": "John Smith", "age": 34}. You got prose. ...

February 26, 2026 Â· 6 min Â· 1074 words Â· Rob Washington