This is a genre of bug, not a single incident. I have hit it enough times that I now treat “but it works in Postman” as a smell, not as evidence.
The pattern:
- Provider documentation.
- Postman collection, often from the provider.
- Green 200.
- Same URL from Laravel
Http::or Guzzle. 401, 400, 421, or a body that looks like XML pretending to be JSON.
Then the meeting where somebody pastes the Postman screenshot as if the application is being difficult on purpose.
Postman is also a client
Postman is not a transparent window onto the network. It has opinions:
- it may send
Content-TypeandAcceptyou did not notice - it may follow redirects
- it may decompress
- it may persist cookies
- it may apply a collection-level header
- it may send a bearer token from an environment you forgot was selected
Laravel will send what you wrote. That is the whole difference.
The list I walk before inventing a conspiracy
Content-Type. Postman said JSON. The server wanted application/x-www-form-urlencoded. Or the opposite.
Accept. You asked for JSON in Postman. Laravel sent */* or nothing, and the server answered with HTML. Related: Laravel returning HTML instead of JSON.
Authorization. “Bearer” with a space. A token with a newline copied from Slack.
Host. You should almost never set this by hand. Related: HTTP 421.
JSON encoding of empty objects and []. PHP’s json_encode and JavaScript’s JSON.stringify are not spiritually aligned.
A method that has saved me hours
Do not compare screenshots. Compare raw requests.
- Postman: Console, look at the actual request.
- Laravel:
Http::dd()/ Guzzle middleware / a local proxy.
If you cannot diff the bytes, you are arguing with a feeling.
The runbook: API works in Postman but fails in Laravel.
The lesson I repeat to myself
APIs do not care that your Postman collection is the official one. They care about the request that arrived. Laravel is not broken because it is more literal than a GUI. Literal is the job.