API bug reports: request, response, and a curl anyone can run
A backend developer opens a ticket that says "invoice API is failing". No URL. No status code. No body. They reply "which endpoint?" and the ticket sits for two days.
An API bug report has one job: let another person reproduce the failure on their own machine in under a minute. If they have to guess anything, you have not finished the report.
Short version
- Record the full request: method, URL, headers, and body.
- Record the full response: status code, key headers, and body.
- Paste a runnable
curlcommand so nobody retypes anything. - Include the environment, the timestamp, and the request ID.
- Replace secrets with
REDACTED, but keep the shape of the value. - Never paste a real token, password, or customer email into a ticket.
What an API bug report must contain
An API, or Application Programming Interface, is the set of URLs your app calls to read and write data. A bug in an API is almost always one of three things: the wrong status code, the wrong response body, or no response at all.
To prove which one it is, your report needs five parts:
- The request you sent.
- The response you got.
- The response you expected.
- A
curlcommand that repeats step 1. - Environment and timing details.
That is it. Everything else is optional.
Capture the full request
Open your browser DevTools, go to the Network tab, right-click the failing call, and copy it. In most APIs there are four things you need.
Method and URL. Write them on one line, together:
POST https://api.staging.example.com/v1/invoices
Do not write "the invoices endpoint". There may be four of them.
Headers. You only need the ones that change behaviour: Authorization, Content-Type, Accept, Idempotency-Key, and any custom X- header your app sends.
Body. Paste the exact JSON you sent, not a description of it. This is the difference between a fix and a guessing game:
Bad: sent an invoice with a numeric ID
Good:
{"invoice_id": 4021, "amount": 4999, "currency": "usd"}
Query parameters. Keep them in the URL. ?limit=50&status=open is part of the request.
Capture the full response
The response is where the answer usually hides, and it is the part people skip most often.
Record three things:
- Status code, such as
401 Unauthorized. Write the number and the text. - Response headers that help triage:
x-request-id,retry-after,content-type. - Response body, copied exactly, including the quotes and braces.
A real example:
401 Unauthorized
x-request-id: req_8f2a91c4
content-type: application/json
{"error":"invoice_id must be a string"}
That body is interesting on its own. A 401 normally means an auth problem, but the message talks about a field type. That mismatch is often the real bug, and a developer can only see it if you paste both parts.
Then add one line for what you expected:
Expected:
201 Createdwith the new invoice object.
Turn it into a curl command anyone can run
curl is a command line tool that sends HTTP requests. A curl command is the most portable bug repro there is. It works on any machine, needs no setup, and cannot be misread.
Most browsers build it for you:
- Open DevTools and select the Network tab.
- Reproduce the bug so the failing call appears.
- Right-click the call and choose Copy then Copy as cURL.
- Paste it into a text editor.
- Remove cookies and browser noise, then redact secrets.
- Test the cleaned command yourself before you file the ticket.
Step 6 matters. A curl command that does not run is worse than none, because it sends someone down a false path.
Here is a clean one:
curl -i -X POST https://api.staging.example.com/v1/invoices \
-H "Authorization: Bearer sk_test_REDACTED" \
-H "Content-Type: application/json" \
-d '{"invoice_id": 4021, "amount": 4999, "currency": "usd"}'
The -i flag prints the response headers, so whoever runs it sees the status code and the request ID without extra work. Keep the line breaks with \ so the command stays readable in the ticket.
What the status code tells your team
The status code is a three-digit number the server returns. It decides who should look at the bug first, so put it in your ticket title.
| Status code | What it usually means | Who should look first |
|---|---|---|
| 400 Bad Request | The request body or parameters are wrong | Whoever built the caller (often frontend) |
| 401 Unauthorized | Missing, expired, or invalid credentials | The person testing, then auth owner |
| 403 Forbidden | Credentials are valid but lack permission | Backend or permissions owner |
| 404 Not Found | Wrong URL, or the record does not exist | Reporter checks the URL, then backend |
| 409 Conflict | Duplicate or state clash, such as a repeated invoice | Backend |
| 422 Unprocessable | Body parses, but a value fails validation | Backend and frontend together |
| 429 Too Many Requests | Rate limit hit | Backend or infrastructure |
| 500 Internal Server Error | The server crashed on this request | Backend, high priority |
| 502 / 503 / 504 | Gateway, deploy, or timeout problem | Infrastructure or on-call |
Two pairs cause most confusion.
401 vs 403. 401 means the server does not know who you are. 403 means it knows and still says no. If you see 403, mention the account and role you used: [email protected] with the Viewer role.
400 vs 500. A 400 is usually the caller's mistake. A 500 is always a server bug, even when the request was wrong, because a server should reject bad input politely instead of crashing. Never close a 500 as "invalid input".
Redact secrets without breaking the repro
Bug trackers are searchable, exported, and often shared with contractors. Treat every ticket as public.
The rule: keep the shape, remove the value. Shape tells the developer whether you used the right kind of key. The value is what leaks.
| Do not paste | Paste this instead |
|---|---|
Authorization: Bearer sk_live_51Hq... | Authorization: Bearer sk_test_REDACTED |
X-Api-Key: 8f14e45fceea167a | X-Api-Key: REDACTED |
"email":"[email protected]" | "email":"[email protected]" |
"card_number":"4242424242424242" | "card_number":"REDACTED" |
| A full session cookie header | Remove the line and add a note |
Three more habits worth keeping:
- Keep the prefix.
sk_test_REDACTEDsays you used a test key. PlainREDACTEDdoes not, and the reviewer may waste time asking. - Use staging accounts. Test with [email protected] on
https://staging.example.comso there is nothing sensitive to hide. - Rotate anything that slips. If a live token reaches a ticket, revoke it. Editing the comment does not undo the exposure.
When your bug starts in the browser rather than in a terminal, Crosscheck captures the failing network requests, console errors, and environment details with the report, so you have the request and response side by side before you write the curl.
Environment and timing details
Backend engineers search logs. Logs need anchors. Give them four:
- Environment: staging, production, or a preview URL. Name it exactly:
api.staging.example.com. - Timestamp in UTC:
2026-08-06 14:32:10 UTC. Local time costs someone a conversion. - Request ID or trace ID:
req_8f2a91c4, taken from thex-request-idresponse header. This is the single most valuable line in an API bug report. It takes a developer straight to the exact log entry. - Frequency: every time, or 3 of 10 attempts. Intermittent bugs are triaged differently.
If your API does not return a request ID header, ask for one. It is a small change that saves hours on every future report.
A copy-paste API bug report template
Title: POST /v1/invoices returns 401 with a validation message on staging
Environment: staging (api.staging.example.com)
Time: 2026-08-06 14:32:10 UTC
Account: [email protected] (Admin)
Request ID: req_8f2a91c4
Frequency: every attempt (5 of 5)
Request
POST https://api.staging.example.com/v1/invoices
Content-Type: application/json
Authorization: Bearer sk_test_REDACTED
{"invoice_id": 4021, "amount": 4999, "currency": "usd"}
Response
401 Unauthorized
x-request-id: req_8f2a91c4
{"error":"invoice_id must be a string"}
Expected
201 Created with the new invoice object.
Repro
curl -i -X POST https://api.staging.example.com/v1/invoices \
-H "Authorization: Bearer sk_test_REDACTED" \
-H "Content-Type: application/json" \
-d '{"invoice_id": 4021, "amount": 4999, "currency": "usd"}'
Notes
Sending "invoice_id": "4021" as a string returns 201. The status code
does not match the error message.
That last note is the kind of detail that closes a bug the same day. You narrowed the trigger to one field, and you said which status code looks wrong.
Frequently asked questions
What if the API returns no response at all?
Say so, and include how long you waited before it timed out. A 30 second hang with no status code points at a different problem than a fast 500.
Should I include the response time?
Include it when speed is the bug, or when the call is unusually slow. 4.2 seconds is a fact; "slow" is an opinion.
Can I attach a Postman collection instead of curl?
You can attach it as well, but keep the curl in the ticket body. Not everyone has Postman open, and text is searchable years later.
How do I report a bug I can only reproduce while logged in? Use a shared test account such as [email protected], note the role, and redact the token. Say which login step produces the credentials.
Is the request ID really that important? Yes. Without it, a developer scans thousands of log lines by timestamp. With it, they find the exact request and its stack trace in seconds.




