Dead Ringer
For developers, on localhost and in CI

A dead ringer for the real thing.

A desktop app that is a local stand-in for the APIs your app sends through: the WhatsApp Cloud API, Postmark or SMTP for mail, and SMSPortal for SMS. Point your app at it instead of the live endpoints, play the customer yourself, and get webhooks back in the same shape. Nothing leaves your machine except a media link your app sends, which it downloads as Meta does.

14 days, full features, no card. Mac today; a Windows build is in development.

Three problems with the free test number

Meta gives you one. In order of how much it costs you.

01

It doesn't shard

One test number, one callback URL, one developer at a time. Parallel workspaces, git worktrees and multiple agents all collide. Working around it means building a fan-out router.

02

Setup costs hours

Business verification, a WABA, tokens, webhook config, template review queues. Repeated for every client.

03

It is networked and non-deterministic

Async webhooks with real latency, rate limits, shared state. Unusable in CI. Unusable offline.

Thirty seconds from new project to sending messages

Your app talks to it exactly as it would talk to the live API.

1Create a project

One project per client integration. Each gets its own phone number, tokens and history, isolated from the others.

curl -X POST http://127.0.0.1:3010/_api/projects \
  -H 'Content-Type: application/json' \
  -d '{"name":"Acme Corp"}'
2Point your app at it

Swap the base URL and credentials in your config. Every version prefix is accepted. Docker works through host.docker.internal.

META_GRAPH_URL=http://127.0.0.1:3010
META_WHATSAPP_PHONE_NUMBER_ID=100000000000002
META_WHATSAPP_ACCESS_TOKEN=EAA…
META_APP_SECRET=f409…
3Play the customer

Reply from the chat UI, from curl, or let an agent do it over the built-in MCP server. Delivery statuses and read receipts come back as webhooks, signed the way Meta signs them.

POST /_api/projects/acme-corp/inbound
{"from":"12025550142","text":"thanks, what time?"}

What it looks like

Screenshots of the Mac app, cut down to the part you work in. They follow the page's theme.

Your app sends a template. You tap one of its buttons in the window, and the reply lands on your webhook with its payload.

One conversation in Dead Ringer: an appointment reminder template with three buttons, the customer's Confirm tap shown with its payload, and the replies that followed. One conversation in Dead Ringer: an appointment reminder template with three buttons, the customer's Confirm tap shown with its payload, and the replies that followed.

Pick a status or an error code for any message and your webhook gets exactly that. Every failure path is one click away.

The status menu open on a sent message: send sent, delivered or read to your app, or fail the message with one of four error codes. The status menu open on a sent message: send sent, delivered or read to your app, or fail the message with one of four error codes.

Mail your app sends over SMTP or the Postmark API is caught and rendered. Deliver it, open it, bounce it, or reply as the customer.

The email tab: a list of caught messages beside one receipt rendered as HTML, with buttons to deliver, open, click, bounce, mark as spam or reply as the customer. The email tab: a list of caught messages beside one receipt rendered as HTML, with buttons to deliver, open, click, bounce, mark as spam or reply as the customer.

Sent messages with their parts, credits and opt-outs, and a button for each delivery receipt your app should handle.

The SMS tab: sent and received messages with their delivery states, one message with its receipts, and a row of receipts to send to your app. The SMS tab: sent and received messages with their delivery states, one message with its receipts, and a row of receipts to send to your app.

An MCP server on the same port. Add it to your coding agent once and it can set up projects, play the customer and read back what your app sent.

The Connect an agent page: the command that adds the Dead Ringer MCP server to Claude Code, Cursor, VS Code or Codex, and the server URL for any other client. The Connect an agent page: the command that adds the Dead Ringer MCP server to Claude Code, Cursor, VS Code or Codex, and the server URL for any other client.

What it speaks

Four surfaces working today, one project behind all of them. A message, the SMS that chases it and the mail it triggers land in the same history.

WhatsApp Cloud API Supported Messages, media, interactive replies and templates. Webhooks come back signed, in the same format, with the same error shapes. META_GRAPH_URL=http://127.0.0.1:3010
SMSPortal Supported Bulk sends, replies and all eight delivery statuses, with receipts signed the way SMSPortal signs them. Credits fall as you send, and a bulk send answers 200 with the dropped destinations in the body. SMS_PORTAL_URL=http://127.0.0.1:3010
Postmark API Supported Sends over the HTTP API, routed to a project by server token. Inbound mail and delivery events POST back in Postmark's shape. POST /email
SMTP Supported Anything that can send mail, routed by the AUTH username. The listener catches; it never relays. MAIL_HOST=127.0.0.1 MAIL_PORT=3025

An MCP server ships alongside them, so a coding agent can drive the customer side. SendGrid, Mailgun and Resend are a day of work each and get built in the order people ask. See the full list, or ask for the one you need.

Laravel's own Postmark mailer hard-codes https, so a Laravel app sends over SMTP: five env lines, no code. SDKs that let you move the base URL, like postmark-php and postmark.js, can use the API path instead.

What it does

The same paths and payloads: text, media, location, contacts, reactions, interactive messages and templates. Webhooks in the same format, signed with X-Hub-Signature-256, including the hub.challenge handshake. Webhooks go straight to localhost. No tunnel, and no public URL to verify or keep alive. Template approval, delivery status, read receipts, the 24-hour window and a chaos mode, each switchable per project. Test numbers that trigger a specific error shape on demand, so failure paths are one send away. Inbound email too: Postmark and SMTP today, so the mail your app sends lands in the same project as its message traffic. An MCP server, so a coding agent can drive the customer side of a conversation. As many projects as you like, in parallel. Runs offline and in CI. No Meta account, app or test number needed.

What it does not do

Talk to Meta. It holds no connection to WhatsApp. The one thing it fetches is a media link your app sends, as Meta would, and you can turn that off. Run in the cloud. It has to POST webhooks into apps on localhost; a hosted version brings back the tunnel it exists to remove. Relay mail anywhere. The SMTP listener catches; it never sends. Care what language you use. It is a black box over HTTP: PHP, Node, Python, Ruby and .NET all look the same to it. Speak every platform. WhatsApp, SMS and email today — see the list, or ask for yours. Replace one real end-to-end pass before go-live.

One licence. One price. Yours to keep.

An annual licence per developer, two activations each. It renews through Paddle until you cancel; if it lapses, the version you have keeps working and only updates stop. It works offline; the only call after activation is a once-a-day check for updates.

Single developer US$89per year, excl. VAT
Every feature, unlimited projects Two activations A year of updates, then a perpetual fallback VAT invoice from Paddle
Start free trial 14 days, no card.
Full pricing and team licences
Dead Ringer for Mac Version 0.6.11 · macOS 11 or later
Windows build in development: ask Have a key? Log in to download Start a trial to download