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 200 with "status": "test". A live key returns 202 with "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 200 with a warning saying 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 /contacts with a test key returns the result but saves nothing. The opted_out path 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, using windowOpenUntil to explain when it closed.
  • missing_variables and template_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 accepted means Meta took the message; delivered and read come later, by webhook or status check.
  • Meta rejections. whatsapp_rejected carries 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

  1. Swap in a live key (sfc_live_…) from the Developers page, again in an environment variable, server side.
  2. Send one template to your own number.
  3. Watch it go sent, delivered, read (webhook or GET /messages/{id}).
  4. 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 free