Test the WhatsApp API without sending a real message
How to use a SendFromChat test key to check your WhatsApp send code (template, variables, 24-hour window, errors) without messaging a customer. Node test file, what a test key checks, what it cannot prove.
November 6, 2026 · 6 min read
The scary part of wiring WhatsApp into an app is the first real send: a wrong phone number, a template that isn't approved, a loop that fires twice, and a customer gets a message from you. A test key lets you run your send code against the real API and stop before anything goes out.
SendFromChat is an independent product and is not affiliated with Meta or WhatsApp. What a test key does comes from our REST API docs and our code. It is our own check, not a Meta sandbox.
The test file below was not run against a live account. We ran it only against a mock fetch to check the request shapes and the assertions, so try it with your own key first.
The short version
- A test key (
sfc_test_…) is created on the dashboard's Developers page. It runs the checks a real send would, then stops. Nothing is sent, saved or counted toward your plan. - You still need a connected number. A test-key send with no connected number returns
409 no_number, so connect first, then build without messaging anyone. - A passing test returns
200with"status": "test". A live key returns202with"status": "accepted". - Test keys work with the REST API only. They are rejected on the MCP connection, so your AI assistant needs a live key or a sign-in.
- A pass means our checks passed. It does not prove Meta will deliver the message.
1. Create a test key
Open Developers on the dashboard and create a test key. Keep it on your server, like any key. Our docs say never to put one in browser or mobile-app code.
export SENDFROMCHAT_TEST_KEY=sfc_test_...
Check the key with GET /api/v1. It returns the workspace, whether the key is live or test, and the numbers it can send from. If that list is empty, connect a number first.
2. What a test key checks
From the docs and the send code, a test-key POST /messages goes through these checks and then stops:
| Check | What you get when it fails |
|---|---|
| Plan: trial ended or allowance used | 402 sending_paused |
| Template exists in your workspace | 404 template_not_found |
| Template is approved | 422 template_not_approved |
Every {{n}} variable is filled |
400 missing_variables |
| An image-header template has its picture | 422 header_image_missing |
| Free-form text or a file, when the person's last message to that number is more than 24 hours old | 422 window_closed, with windowOpenUntil |
| The person has opted out (template sends) | 422 opted_out |
| A new number's daily warm-up cap for templates | 429 warmup_limit |
Two details that surprise people:
- A free-form message to a number that has never written to you does not fail. We have no record of a window either way, so the test returns
200with awarningsaying WhatsApp will silently drop the message if they haven't written in. Treat that warning as a failure in your own test. - A test key cannot create an opted-out contact.
PUT /contactswith a test key returns the result but saves nothing. Theopted_outpath needs a real person replying STOP to your number, so test that one by hand.
3. A test file you can keep
This uses Node's built-in test runner and the global fetch, so it needs Node 20 or newer. It refuses to run with a live key, so it can't message anyone by mistake.
// sfc.test.mjs — run with: node --test sfc.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
const BASE = "https://sendfromchat.com/api/v1";
const KEY = process.env.SENDFROMCHAT_TEST_KEY ?? "";
const TO = process.env.SFC_TEST_TO; // a number you control, e.g. +4915123456789
const TEMPLATE = process.env.SFC_TEMPLATE; // an approved template name
const VARS = JSON.parse(process.env.SFC_TEMPLATE_VARS ?? "[]"); // e.g. ["Ana","#4821"]
if (!KEY.startsWith("sfc_test_")) {
throw new Error("Set SENDFROMCHAT_TEST_KEY to a test key (sfc_test_...). Live keys are refused here.");
}
async function send(body) {
const res = await fetch(`${BASE}/messages`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return { status: res.status, body: await res.json() };
}
test("an approved template passes every check", async () => {
const r = await send({ to: TO, template: { name: TEMPLATE, variables: VARS } });
assert.equal(r.status, 200, JSON.stringify(r.body));
assert.equal(r.body.status, "test");
});
test("a missing variable is caught before sending", async () => {
const r = await send({ to: TO, template: { name: TEMPLATE, variables: [] } });
assert.equal(r.status, 400);
assert.equal(r.body.error.type, "missing_variables");
});
test("an unknown template is caught", async () => {
const r = await send({ to: TO, template: { name: "no_such_template_xyz" } });
assert.equal(r.status, 404);
assert.equal(r.body.error.type, "template_not_found");
});
test("free-form text is not silently dropped", async () => {
const r = await send({ to: TO, text: "Thanks, we're on it." });
if (r.status === 422) {
assert.equal(r.body.error.type, "window_closed"); // your code should offer a template here
return;
}
assert.equal(r.status, 200);
// 200 without a warning means the window is open. A warning means we have no record of one.
assert.equal(r.body.warning, undefined, r.body.warning);
});
Use a template that has at least one variable (a {{1}} placeholder): the second test expects the missing-variable error.
Run it:
SFC_TEST_TO=+4915123456789 SFC_TEMPLATE=order_shipped SFC_TEMPLATE_VARS='["Ana","#4821"]' \
node --test sfc.test.mjs
The fourth test fails for a number that has never written to you. That is the point: it tells you your code would have sent free-form text that WhatsApp may drop. Write to your own number from your phone first, or send a template instead.
4. Test your error handling too
Your app should branch on error.type, not on message text. The docs list the types. Put one handler in your send function and test it with the same calls:
window_closed: show the person a template option, usingwindowOpenUntilto explain when it closed.missing_variablesandtemplate_not_approved: these are bugs in your code or your template, not retries.warmup_limit(429): retry later.sending_paused(402): stop and alert someone; the trial ended or the plan limit was reached.
Also send an Idempotency-Key header derived from the event (for example order-4821-shipped). With a live key, a retry with the same key and body returns the first response with Idempotent-Replayed: true instead of sending again. We did not test how idempotency behaves with a test key, so check that yourself before you rely on it.
5. What a test key cannot tell you
- Delivery. A test never reaches Meta. A live
202 acceptedmeans Meta took the message;deliveredandreadcome later, by webhook or status check. - Meta rejections.
whatsapp_rejectedcarries Meta's own reason, and only a real send can produce it. - Your assistant. Test keys don't work on the MCP connection.
So the last step is always one real message to your own number with a live key. Then ship.
6. Switch to live
- Swap in a live key (
sfc_live_…) from the Developers page, again in an environment variable, server side. - Send one template to your own number.
- Watch it go
sent,delivered,read(webhook orGET /messages/{id}). - Keep the test file in your repo and run it before each deploy with a test key.
Data and your customers
SendFromChat stores data in Mumbai, India. Transfers from the EU and UK are covered by the Standard Contractual Clauses and the UK Addendum in our Data Processing Agreement, and the sub-processors are listed publicly. Read it before you decide. This is not legal advice.
Try it
Connect a number, create a test key and run the file above. The 24-hour rule behind window_closed is explained in the 24-hour window guide.
Connect your number (30-day free trial, no card).
Do this from your AI assistant
SendFromChat connects Claude, ChatGPT, Cursor or any MCP client to the WhatsApp Business API — templates, bulk sends, inbox, chatbots and sequences, just by asking. 30 days free, no card.
Start freeMore guides
WhatsApp template image header — format, size and how to add one
What a WhatsApp message template needs for a picture header (JPG or PNG, size limits), why Google Drive and Dropbox links fail, how to upload the image from your own code, and what header_image_missing means.
WhatsApp Business API in Europe — a country-by-country guide
Where WhatsApp actually dominates in Europe, what it costs to send in each market, and the rules that differ by country before you launch a campaign.
WhatsApp marketing and the GDPR
What European law actually requires before you message someone on WhatsApp — lawful basis, opt-in, opt-out, retention, transfers and the DPA your customers will ask you for.
How to set up the WhatsApp Cloud API (2026 step-by-step guide)
Create a Meta app, get a WhatsApp Business Account, send your first message from the free test number, then make it permanent with a system-user token — every step, with the parts Meta's docs skip.