5 things I check before trusting an API
Before I integrate with any API — official or reverse-engineered — I run through this checklist to avoid surprises later.
Whether I'm building an integration for a client or automating something for myself, I never trust an API at face value. Docs lie. Behavior changes. Undocumented rate limits kill your bot at 3am.
These 5 checks take about 10 minutes. They've saved me weeks of debugging production failures.
1. Does it actually rate limit?
The docs say "100 requests per minute." But is that enforced? I test it directly:
$ time for i in $(seq 1 150); do
curl -so /dev/null -w "%{http_code}\n" \
https://api.target.com/v1/users \
-H "Authorization: Bearer $TOKEN"
done | sort | uniq -c
# Possible outcomes:
150 200 ← No rate limiting at all
100 200, 50 429 ← Rate limiting works as documented
148 200, 2 500 ← Rate limiting exists but is buggyThe third outcome is the most dangerous for automation. It means the API has rate limiting but it's implemented poorly — sometimes it triggers, sometimes it doesn't, and when it does, it might crash instead of returning a clean 429.
💡 What I look for in the response
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1704067260
Retry-After: 30If these headers exist, I can build adaptive throttling. If they don't, I'm flying blind and need to implement my own backoff logic.
2. What happens when the token expires?
I intentionally let my token expire and hit the API. The response tells me everything about error handling quality:
# Good: Clean 401 with machine-readable error
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "token_expired",
"message": "Access token has expired",
"expired_at": "2024-01-15T10:30:00Z"
}
# Bad: Generic 500 with stack trace
HTTP/1.1 500 Internal Server Error
Content-Type: text/html
<h1>Error</h1>
<pre>JsonWebTokenError: jwt expired
at /app/node_modules/jsonwebtoken/verify.js:152:21
at /app/middleware/auth.js:34:5</pre>
# Worst: Silent 200 with empty/wrong data
HTTP/1.1 200 OK
{"users": []}⚠️ Why this matters for automation
If expired tokens cause 500s, the API is fragile. Your automation needs aggressive retry logic and token refresh before every batch. If it returns empty data silently, your bot might run for days thinking everything is fine while actually getting nothing.
3. Are IDs sequential or random?
I create two resources and compare their IDs:
# Sequential (bad for security, good for enumeration)
POST /api/documents → {"id": 4521}
POST /api/documents → {"id": 4522}
# I now know: there are ~4522 documents total.
# I can enumerate: /api/documents/1, /api/documents/2, ...
# UUID (better)
POST /api/documents → {"id": "7f3a2b1c-9d4e-4f5a-8b6c-1d2e3f4a5b6c"}
POST /api/documents → {"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}
# Can't enumerate. Can't guess total count.Sequential IDs tell me three things:
- Total record count is guessable (competitive intelligence)
- Enumeration attacks are trivial (IDOR testing)
- The API owner can detect bulk sequential access (my bot might get flagged)
If I'm building automation against sequential IDs, I randomize access order and add jitter to avoid detection patterns.
4. Does it version properly?
# Versioned (safe to build against)
GET /api/v2/users HTTP/1.1
# → v1 still works, v2 has new fields, v3 coming next quarter
# Unversioned (dangerous)
GET /api/users HTTP/1.1
# → Response format can change any time without warningI've had automations die overnight because an unversioned API changed its response format. The field was user_name on Monday and username on Tuesday. No deprecation notice, no changelog, no version bump.
💡 My defensive pattern
# I always validate response shape before processing
def validate_user(data: dict) -> bool:
required = ['id', 'email', 'created_at']
return all(k in data for k in required)
# If validation fails → alert, don't silently break
users = api.get('/users')
for user in users:
if not validate_user(user):
alert(f"Schema changed! Missing keys in: {user.keys()}")
break5. What does the error format look like?
I intentionally send broken requests to map the error surface:
# Test 1: Missing required field
POST /api/users -d '{"name": "test"}'
# Expected: 422 with field-level errors
# Test 2: Wrong type
POST /api/users -d '{"email": 12345}'
# Expected: 422 with type mismatch error
# Test 3: Invalid JSON
POST /api/users -d 'not json at all'
# Expected: 400 with parse error
# Test 4: Oversized payload
POST /api/users -d '{"bio": "<100KB string>"}'
# Expected: 413 with size limit info| Error quality | What it means | Trust level |
|---|---|---|
| Structured JSON errors | Team cares about DX | High |
| Generic "Bad Request" | Minimal effort | Medium |
| HTML error pages | Framework defaults, no customization | Low |
| 500 with stack trace | Debug mode in production | Very low |
The meta-lesson
┌─ API TRUST ASSESSMENT ───────────────────────────┐
│ │
│ Rate limiting → How much can I push? │
│ Error handling → How fragile is it? │
│ ID format → How detectable am I? │
│ Versioning → How stable is the contract? │
│ Error format → How much does the team care? │
│ │
│ Time to assess: ~10 minutes │
│ Time saved: weeks of 3am debugging │
│ │
└──────────────────────────────────────────────────┘An API that fails these basic checks will cause problems later — it's just a matter of when. Better to know upfront and build defensively than to discover it when your automation breaks in production.
Related posts
- API Reverse EngineeringMay 18, 20268 min read
What your JWT tokens reveal about your backend
JWTs are meant to be opaque to users. They're not. Here's what I learn about your architecture just by decoding one.
- API Reverse EngineeringApr 22, 202612 min read
How I reverse-engineered an HR attendance API in 3 days
A practical walkthrough of the methodology I use when there's no documentation: capturing traffic, mapping endpoints, and validating assumptions before writing a single line of automation.
- API Reverse EngineeringApr 8, 20268 min read
Reading the network tab without losing your mind
DevTools shows you everything, which is the problem. Here's how I filter signal from noise when reverse-engineering a web application's API.