We integrated Zalo Notification Service for OTP and transactional messages. It worked in production on the first try. In staging it failed for two days.
Same code. Same credentials in the config. Different result.
The failure was invisible
The API returned success. The logs were clean. Nothing threw. The message simply never arrived, and there was no signal anywhere in our system that anything had gone wrong.
That's the worst shape a bug can take: a system that reports health while doing nothing. Every debugging instinct sends you looking at your own code first, and your own code is fine.
Environments are not configuration
The assumption underneath the bug was that staging and production were the same system with different values plugged in — swap the keys, point at a different database, everything else behaves identically.
That holds right up until an external service has its own idea of what your environments are. Third-party platforms often have their own approval states, sender registrations, and template allowlists, and those live on their side. None of it appears in your config file, so none of it shows up when you diff your environments.
What actually fixed it
Two things.
First, stop trusting a 200. An accepted request means the provider took the message, not that anyone received it. If the provider offers delivery callbacks, wire them up — otherwise your only feedback loop is a user telling you their code never came.
Second, verify the integration end to end in every environment before assuming it carries over. "It works in prod" is not evidence that it works in staging, and the reverse bites harder: an integration that quietly works only in staging will break on launch day.
The general lesson
When something works in one environment and not another, the difference is rarely in the code. It's in the assumptions the code makes about the world around it — and those assumptions are usually invisible until one of them is wrong.