
Why your WhatsApp test number never receives a message
The webhook was green. The messages field was subscribed. Meta's dashboard was happy. I sent a message to the number and waited for my assistant to reply.
Nothing arrived. Nothing kept arriving. This is the story of the afternoon I lost to a number that, by design, cannot be spoken to.
I'd been wiring a parked WhatsApp intake scaffold for my personal assistant: environment variables, signed-webhook verification, a seeded channel binding, the full inbound-to-reply loop. Three separate traps sat between me and a working thread. Only one of them was the real villain.
Trap one: the migration that was never applied
The first failure was honest and quick. Seeding the channel binding failed on a check constraint. The reason was that the migration defining it had never reached the hosted database. Weeks earlier, a previous incident had prevented some migrations from being applied; this one was among them, still sitting in the repo, never run.
Fine. Apply it over psql. But that surfaced the second trap.
Trap two: the guardrail that saved the wrong database
The tempting way to apply a migration here is the Supabase MCP integration: it's right there, it can run SQL. I didn't, because it's pointed at a different project.
I checked rather than assumed. The MCP's project resolved to a different application from another project. The one I actually wanted was for the project I was working on. Same account, different database. Running the migration through the MCP would have altered the wrong project's schema, cleanly and confidently, and I'd have spent the next hour wondering why nothing changed in the app.
Guardrail: Verify the target before you run the write. It is dull to follow and very cheap compared to the alternative.
Applying over psql had its own small comedy. I had to probe several connection-pooler regions to find the project's home, spotting it as the one region that returned an authentication error rather than a connection error: an auth failure means the server recognised the project reference, a connection failure means it didn't. And the database password in my local env file was stale, the old local-stack one; the real hosted password was a different, longer string. All annoying, all solvable.
Trap three: the number that cannot receive a message
With the binding seeded and the loop live, everything looked right and still nothing arrived. This was the one that could have eaten a whole day, because there was no error anywhere to chase. Green webhook, subscribed field, silence.
Here's the thing nobody puts in front of you: a Meta WhatsApp test number cannot receive a cold inbound message. It is outbound-only, and only to recipients you've explicitly verified. You cannot start a conversation to it. So a customer messaging your test number does not fail loudly; it simply never happens.
I'd built a perfectly good machine for answering messages that, on that number, could never arrive. The fix was to link a real business number instead, which moves you into WhatsApp's "Coexistence" territory and is its own adventure. But the moment I did, the loop lit up.
What I took away
Two things.
The obvious one: when a system is green everywhere and does nothing, stop looking for a bug and start questioning the assumption underneath. Mine was "a number you can send to is a number you can receive on." For Meta test numbers, that's false, and nothing in the happy path tells you so.
The quieter one: the most valuable step that afternoon was the boring check that the MCP was pointed at the wrong database. No error would have caught that. The habit did.
More war stories as they happen, as I build this in the open.
Found this helpful? Share it!
Enjoyed this post? Subscribe to my newsletter for more insights on web development, career growth, and tech innovations.
Subscribe to NewsletterRelated Posts

The AI I taught to stay silent
This piece explores the counter-intuitive challenge of building an AI assistant designed for silence, rather than constant responsiveness. It details how a WhatsApp-based order system for small food producers was redesigned to be invisible until explicitly triggered, ensuring it supports human interaction without interruption.

When 'add sizes to a product' is secretly a data-model decision
A one-line feature request to add two tub sizes turned out to be a crop-versus-SKU schema decision hiding under a dropdown, exposed by a single GROUP BY.

Making sense of everything: A personal knowledge graph experiment
Exploring the potential of a personal knowledge graph to connect disparate data points across my digital life. This post details the technical stack, schema design, and a three-week proof-of-concept plan to surface hidden patterns and insights.
