Mental model
Cisco objective 2.5 says “Troubleshoot a problem given the HTTP response code, request and API documentation”. There is a repeatable 4-step decision tree.
Status code first digit.
- 2xx → it worked. Not a bug.
- 3xx → follow Location (curl -L does this).
- 4xx → you sent something wrong. Read the body.
- 5xx → server failed. Retry; if it persists, open a support ticket.
Read the error body. Modern APIs return JSON with an
error,errors,message, ordetailfield. The text usually points directly at the problem.Isolate. If a Python script fails, redo the same call in curl. If curl works, the bug is in your Python. If curl also fails, the bug is in the request.
Compare working vs broken. Diff a request you know works against the one that fails. Nine times out of ten it is one header, one typo in a key, or wrong URL.
Status-code-by-status-code checklist
400 Bad Request
- Malformed JSON (missing closing brace, trailing comma).
- Required field missing.
- Field type wrong (string instead of int).
- Field value out of range.
Fix: read the body; it names the field. If you cannot see the body, retry with curl -i to show it.
401 Unauthorized
- No Authorization header.
- Wrong credentials.
- Token expired.
- Spelling:
Authorizationnotauthorization(most servers do not care; some do).
Fix: add / correct auth. For Bearer tokens, refresh if expired. See REST API auth.
403 Forbidden
- Auth was accepted but your account has no permission for this resource.
- Common on multi-tenant APIs: you authenticated as org A but asked about org B.
Fix: ask the admin for a wider scope, or use the right tenant / org id.
404 Not Found
- Typo in the URL (devices vs device, trailing slash difference).
- Resource id does not exist (deleted, or wrong id).
- Some APIs return 404 instead of 403 for security — “we are not telling you if it exists”.
Fix: double-check URL. List the parent resource (GET /devices) to confirm the id you are asking about actually exists.
405 Method Not Allowed
- You used GET on an endpoint that only supports POST, or vice versa.
Fix: check the docs for the endpoint and change -X.
409 Conflict
- You tried to create something that already exists.
- You tried to update a resource that has changed since you fetched it (optimistic concurrency).
Fix: GET first to see current state; use PUT or PATCH; include If-Match if the API supports ETags.
413 Payload Too Large
- Request body exceeds the server’s limit (usually 1 to 10 MB).
Fix: split the work. Upload files in chunks. Batch creates smaller.
422 Unprocessable Entity
- JSON parsed but failed validation (e.g., email field has no @, VLAN id is 5000).
Fix: read the body; adjust the field value.
429 Too Many Requests
- Rate limit.
Fix: honor Retry-After. See API constraints.
500 Internal Server Error
- Server bug or transient failure.
Fix: retry once after a few seconds. If persistent, open a vendor support ticket with the request id if the response headers include one.
502 Bad Gateway / 503 Service Unavailable / 504 Gateway Timeout
- Problem between you and the application: a proxy, a load balancer, or a downstream the server depends on.
Fix: retry with exponential backoff.
Working methodology
Step 1 — reproduce in curl
If your Python script fails, build the exact same request in curl. Any random editor, no code, just:
curl -i -v -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"sw3","ip":"10.0.0.3"}' \
https://api.example.com/devices
Step 2 — read everything
- Status line.
- All response headers (
-ior-v). - Body.
- If verbose, your request headers (prefixed
>in curl -v).
Step 3 — binary-chop the request
Comment out one header, retry. Change one field, retry. Narrow until you find the single change that flips 4xx into 2xx.
Step 4 — fix in Python
Transliterate the working curl back to requests. The gotchas:
data=vsjson=. Usejson=for a dict;data=for a string.- Headers Python adds automatically (User-Agent, Content-Length).
- Trailing slash on the URL — some APIs care.
- URL encoding of query params — use
params=dict, not string concatenation.
Step 5 — add resilience
Once the request works, add:
r.raise_for_status()so you fail loud on 4xx/5xx.timeout=5so hangs become errors.- Retry on 429 and 5xx with exponential backoff.
- Log the response id (if the API returns one) so vendor support can trace.
Common gotchas
- Content-Type: application/json missing. Python
requests.post(..., json=...)sets it automatically; manual-dwith curl does not. - Base URL has a region. Meraki, Catalyst Center and Webex have per-region API hostnames. Wrong region → 401 or 404.
- Trailing slash.
/devicesvs/devices/vary by API. - Capitalisation of JSON keys.
deviceIdvsdeviceidmatters. - Dates and timezones. API often wants ISO 8601 UTC (
2026-10-03T12:34:56Z), not a local-time string.
FAQ
My script works in dev but fails in CI. Compare env vars. CI often has a different API key, a different token, or missing certificates.
I get 200 but the body is empty. Check Content-Length. If it is 0, the API did succeed but has nothing to return (common for DELETE and no-op PATCH). If the body should have content, check the Accept header you sent.
502 Bad Gateway immediately on every call. Usually a load balancer / proxy issue, not the API. Check the vendor status page.
Why does my 401 become 200 after a few minutes? Clock skew on your machine. TLS cert validation and OAuth tokens can fail if your clock is off by more than a few minutes.
