WhatsApp

Lightweight, dependency-free, in-memory WhatsApp Cloud API fake for testing code that talks to the Meta Graph API with axios.

Default port: 4657

It speaks the exact wire protocol an axios-based WhatsApp Cloud API integration uses: Bearer-token auth (Authorization: Bearer <ACCESS_TOKEN>), JSON request bodies (and multipart/form-data for media upload), and JSON responses with the Cloud API envelopes ({ messaging_product, contacts, messages } for sends, { id } for uploads, Graph-style { error: { … } } on failure). Everything sent is captured in memory for assertions and can be reset. Zero cost, zero side effects.

The Graph version prefix (e.g. v21.0) in the path is accepted but optional — /{version}/{id}/... and /{id}/... both route the same way.

Implemented Operations

Send messages (POST /{PHONE_NUMBER_ID}/messages)

A single endpoint that dispatches on the type field. Always requires messaging_product: "whatsapp". On success returns 200 with { messaging_product, contacts: [{ input, wa_id }], messages: [{ id: "wamid.…", message_status: "accepted" }] }.

Mark as read & typing (POST /{PHONE_NUMBER_ID}/messages)

Media

Phone number & WhatsApp Business Account

Business profile

Message templates (management)

Registration & verification

Webhook verification

Service & inspection operations (parlel-only, not part of the Cloud API)

Quick Start

import { WhatsappServer } from "./services/whatsapp/src/server.js";
import axios from "axios";

const server = new WhatsappServer(4657);
await server.start();

const ACCESS_TOKEN = "parlel-test-access-token";
const PHONE_NUMBER_ID = "100000000000001";

const wa = axios.create({
  baseURL: "http://127.0.0.1:4657/v21.0",
  headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
});

// Send a text message
const { data } = await wa.post(`/${PHONE_NUMBER_ID}/messages`, {
  messaging_product: "whatsapp",
  recipient_type: "individual",
  to: "15551230000",
  type: "text",
  text: { body: "Hello from parlel!" },
});
console.log(data.messages[0].id); // "wamid.…"

// Send an approved template
await wa.post(`/${PHONE_NUMBER_ID}/messages`, {
  messaging_product: "whatsapp",
  to: "15551230000",
  type: "template",
  template: { name: "hello_world", language: { code: "en_US" } },
});

await server.stop();

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureSupported
Send text / template / media / location / contacts / interactive / reaction
Reply context & preview_url
Mark message as read + typing indicator
Media upload / metadata / download / delete
Phone number info & WABA phone_numbers
Business profile get/update
Message template list/create/delete
Number register / deregister / request_code / verify_code
Webhook verification handshake (hub.challenge)
Inbound message & status webhook simulation (control plane)
Bearer-token auth enforcement
Real OTP/SMS delivery (deterministic 123456 instead)✓ By design — Deterministic stub output — repeatable assertions, no API spend
Actual message delivery to a phone✓ By design — Captured in-memory for inspection — no real messages sent
Template approval workflow (created templates stay PENDING)⟳ Roadmap — (simplified)
Webhook delivery to your endpoint (build payloads via /__parlel/inbound)⟳ Roadmap — (inspect-only)
Flows, calling API, billing, analytics, conversation pricing⟳ Roadmap

Error Codes / Shapes

Failures use the Graph API error envelope:

{
  "error": {
    "message": "(#100) The parameter to is required.",
    "type": "GraphMethodException",
    "code": 100,
    "error_subcode": 33,
    "fbtrace_id": "…",
    "error_data": { "messaging_product": "whatsapp", "details": "to is required" }
  }
}
HTTPcodeWhen
401190Missing or invalid Bearer access token (OAuthException).
400100Missing/invalid parameter (messaging_product, to, message body, media id/link, etc.).
404100Unknown object id (phone number, media, WABA) or unknown template on delete.
404132001Sending a template whose name does not exist.
400136024Incorrect verification code in verify_code.
400136025verify_code called before request_code.
403Webhook verification with a non-matching hub.verify_token (text/plain body).
405100Method not allowed on a known resource.

Environment Variables

VarDefault
WHATSAPP_ACCESS_TOKENparlel-test-access-token
WHATSAPP_PHONE_NUMBER_ID100000000000001
WHATSAPP_BUSINESS_ACCOUNT_ID200000000000001
WHATSAPP_API_VERSIONv21.0
WHATSAPP_VERIFY_TOKENparlel-verify-token
WHATSAPP_BASE_URLhttp://127.0.0.1:4657
<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

WHATSAPP_ACCESS_TOKEN=parlel-test-access-token
WHATSAPP_PHONE_NUMBER_ID=100000000000001
WHATSAPP_BUSINESS_ACCOUNT_ID=200000000000001
WHATSAPP_API_VERSION=v21.0
WHATSAPP_VERIFY_TOKEN=parlel-verify-token
WHATSAPP_BASE_URL=http://parlel-bridge:4657
<!-- parlel:testenv:end -->