WhatsApp API & Developers

WhatsApp API Security: Tokens, Webhooks and a Checklist

Somewhere in your company there is a token that can send WhatsApp messages as your business to every customer you have. It might be in a secret manager. It might also be in a .env file on a contractor's laptop, a Postman collection or a Slack thread from launch week. WhatsApp API security mostly comes down to that token, one app secret and one public webhook endpoint.

The stakes are specific. A leaked token lets someone message your customers under your verified name, which is an ideal phishing setup, and run up per-message charges on your account. A webhook that doesn't check signatures will accept fake "customer messages" from anyone who finds the URL.

This guide covers tokens, secrets, webhooks and logging, then ends with a checklist you can work through in an afternoon.

What you are protecting

AssetIf it leaks or is abusedMain control
Access tokenMessages sent as your business; templates and numbers managed; charges billed to youSystem users, least privilege, server-only storage, rotation
App secretForged webhook signatures; valid appsecret_proof valuesSecret manager, rotation, never client-side
Webhook endpointFake inbound messages and statuses injected into your systemsSignature checks on the raw body, mTLS
Verify tokenLets someone re-verify your endpoint during setupRandom value, constant-time comparison
Message data and logsA privacy breach involving customer conversationsRedaction, retention limits, access control

Tokens: system users and least privilege

Use system user tokens for anything automated. Meta's access token guide recommends System User tokens for direct developers and Business Integration System User tokens for Tech Providers and partners. User tokens from the App Dashboard expire within hours and belong to a person, which is fine for a first test message and wrong for production.

Prefer employee system users with partial access. Admin system users get full access to every WhatsApp account and Messaging account your portfolio owns or has been shared, by default. Employee system users only reach the accounts you grant, at Partial or Full level, and Meta notes you must grant both the WhatsApp account and the Messaging account. Give each service its own system user, scoped to the accounts it needs, so a leak in one doesn't expose the rest.

Grant only the permissions a service uses. Meta's setup walkthrough assigns business_management, whatsapp_business_management and whatsapp_business_messaging. A worker that only sends messages typically needs the messaging permission, not template or account management. A token debugger check before go-live shows what a token can do. Our guide to the WhatsApp API permanent access token walks through generating one.

Choose expiry deliberately. Meta lets you pick a token expiration preference when you generate a system token. Non-expiring tokens are convenient and live forever if they leak; expiring ones force a rotation process you should have anyway. Treat tokens as opaque strings of variable length, as Meta advises, and never parse them.

Bind tokens to your app secret. Graph API supports appsecret_proof, an HMAC-SHA256 of the access token keyed with your app secret, sent with each call. Turning on Require App Secret in the App Dashboard (App Settings, Advanced, Security) makes calls without a valid proof fail, so a stolen token alone isn't enough. Test it on a staging app first:

import hashlib
import hmac
import os
import requests

TOKEN = os.environ["WHATSAPP_TOKEN"]
APP_SECRET = os.environ["META_APP_SECRET"]
proof = hmac.new(APP_SECRET.encode(), TOKEN.encode(), hashlib.sha256).hexdigest()

resp = requests.post(
    "https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/messages",  # use the current version
    params={"appsecret_proof": proof},
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={
        "messaging_product": "whatsapp",
        "to": "919811122233",
        "type": "text",
        "text": {"body": "Your order 4821 has shipped."},
    },
    timeout=10,
)
resp.raise_for_status()

Use the current Graph API version from Meta's changelog in place of v23.0. Meta also offers a server allow list that restricts some API calls to servers you approve.

Secrets out of code, and a rotation plan

Keep the token, app secret and verify token in a secret manager (AWS Secrets Manager, Google Secret Manager, Vault or similar) and inject them at runtime. Keep .env files out of git, run a secret scanner in CI, and never ship either secret in a mobile app or browser bundle. If a client needs to send, it calls your backend.

Write the rotation runbook before you need it:

  1. Generate a new token for the system user and store it as a new secret version.
  2. Roll your services so every instance picks it up, and confirm sends succeed.
  3. Revoke the old token. In an incident, also remove the compromised system user's asset access, which makes its tokens fail with error 200.
  4. For the app secret, update your webhook verifier to accept signatures from both old and new secrets, reset the secret, then drop the old one. Otherwise valid webhooks fail mid-rotation.
  5. Review the App Dashboard and Business Settings for system users, admins and apps you no longer recognise.

Alert on authentication errors so you notice a dead or revoked token quickly: 190 (token expired), 0 (unable to authenticate the app user), 10 (permission missing) and 200 (no access to the resource).

Webhook security

Meta's webhooks reach a public URL, so treat every request as hostile until proven otherwise.

  • Valid TLS. Meta requires a correctly installed certificate; self-signed certificates aren't supported.
  • Verify the GET handshake. Compare hub.verify_token to your stored value and echo hub.challenge only on a match.
  • Verify every POST. Meta signs each payload in X-Hub-Signature-256 as sha256= plus an HMAC-SHA256 of the request body, keyed with your app secret. Compute it over the raw bytes before any JSON parsing, and compare in constant time.
  • Don't rely on IP allowlists. Meta publishes how to list its webhook IPs, then notes they change periodically and suggests mutual TLS instead. mTLS is enabled per app, not per number.
  • Make replays harmless. The signature covers only the body, with no timestamp, and Meta retries failed deliveries for up to 7 days. Deduplicate on message ID plus status so a repeated payload does nothing.
  • Size and speed. Payloads can reach 3 MB. Acknowledge with 200 quickly and process from a queue.

A minimal Express handler:

import crypto from "node:crypto";
import express from "express";

const app = express();
const APP_SECRET = process.env.META_APP_SECRET;
const VERIFY_TOKEN = process.env.WA_VERIFY_TOKEN;

const safeEqual = (a, b) => {
  const x = Buffer.from(String(a));
  const y = Buffer.from(String(b));
  return x.length === y.length && crypto.timingSafeEqual(x, y);
};

app.get("/webhooks/whatsapp", (req, res) => {
  const ok = req.query["hub.mode"] === "subscribe" &&
    safeEqual(req.query["hub.verify_token"] ?? "", VERIFY_TOKEN);
  return ok ? res.status(200).send(String(req.query["hub.challenge"])) : res.sendStatus(403);
});

app.post(
  "/webhooks/whatsapp",
  express.raw({ type: "application/json", limit: "3mb" }),  // keep the raw bytes
  (req, res) => {
    const expected = "sha256=" +
      crypto.createHmac("sha256", APP_SECRET).update(req.body).digest("hex");
    if (!safeEqual(req.get("X-Hub-Signature-256") ?? "", expected)) {
      return res.sendStatus(401);
    }
    res.sendStatus(200);
    queue.publish(JSON.parse(req.body.toString("utf8")));  // dedupe downstream
  }
);

Our dedicated guide to how to verify a WhatsApp webhook signature covers Python, PHP and common mistakes, and WhatsApp API webhooks covers the payloads.

Logging, media and personal data

Logs are where most WhatsApp data leaks actually happen, because nobody thinks of them as a database.

  • Scrub the Authorization header in HTTP client and proxy logs. One debug line can print a production token.
  • Log identifiers, not content. Message ID, status, error code and phone number ID are enough to debug. Keep message bodies out of application logs.
  • Treat phone numbers, BSUIDs and usernames as personal data. Hash or truncate them in logs and analytics.
  • Handle media server-side. Media download URLs expire after 5 minutes and require your access token, so never hand one to a browser or third party. Download on your server, store it with access controls, and serve it through your own authenticated route. Media IDs in webhooks expire after 7 days.
  • Set retention on logs. Short-lived debug logs are a security control. Our guide to WhatsApp API data privacy covers retention and deletion more broadly.

The checklist

AreaCheck
TokensOne system user per service, employee role, Partial access to only the accounts it needs
TokensOnly the permissions each service uses; appsecret_proof required
TokensExpiry chosen on purpose; rotation runbook written and rehearsed
SecretsSecret manager in every environment; nothing in git, apps or browsers; CI secret scanning
WebhooksValid TLS; GET verify token checked; POST signature checked on raw body in constant time
WebhooksmTLS considered; no IP-only trust; dedupe on message ID and status; 3 MB body limit
DataAuthorization headers scrubbed; no message bodies in logs; identifiers hashed in analytics
DataMedia downloaded server-side; retention set for logs, media and exports
MonitoringAlerts on errors 190, 0, 10, 200; account_update webhooks; unusual send volume
PeopleDeparted staff removed from Business Settings; system users and apps reviewed quarterly

For the rest of the platform, see the complete guide to the WhatsApp API.

Where Tenashi fits

If you're connecting your own systems to WhatsApp through Tenashi rather than Meta's API, the same principles apply, and the Tenashi API builds several in. Keys are created by workspace admins in Settings, shown once and revocable. Each key acts with the permissions of the teammate who created it and stops working if that person leaves the workspace (401 key_owner_removed), so access ends with employment. Keep keys server-side like any secret.

Tenashi webhooks are signed with a per-endpoint whsec_ secret: X-Tenashi-Signature is an HMAC-SHA256 of the timestamp, a period and the raw body, and you reject timestamps older than 5 minutes, which gives you replay protection Meta's body-only signature doesn't. The honest scope: Tenashi links your existing number by QR scan, isn't Meta's official API, and is built for team conversations, not OTPs or high-volume alerts. Plans start from $18/mo.

Read the API docs · Get started

FAQ

How do I secure a WhatsApp API access token?

Use a system user token rather than a personal user token, scoped to only the accounts and permissions the service needs. Store it in a secret manager, never in code, mobile apps or browsers. Require appsecret_proof so a stolen token alone fails, choose an expiry on purpose and rehearse a rotation runbook.

How do I verify WhatsApp webhook requests?

Compute an HMAC-SHA256 of the raw request body using your app secret, prefix it with sha256= and compare it in constant time with the X-Hub-Signature-256 header. Do it before parsing the JSON. Reject mismatches with a 4xx status. For the setup handshake, check hub.verify_token and echo hub.challenge.

Should I allowlist Meta's IP addresses for webhooks?

Not as your only control. Meta publishes how to look up its webhook IP ranges but says they change periodically and suggests mutual TLS instead. An IP allowlist that falls out of date silently drops real webhooks. Signature verification plus mTLS protects you without depending on an IP list.

Can someone replay an old WhatsApp webhook?

Potentially, because Meta's signature covers only the request body and carries no timestamp, so a captured payload still verifies. Meta also retries failed deliveries for up to 7 days, producing genuine duplicates. Make processing idempotent by deduplicating on message ID plus status, so a repeated payload changes nothing.

Are WhatsApp media URLs safe to share?

No. Media download URLs from Cloud API require your access token and expire after 5 minutes, so sharing one means either exposing your token or sending a link that soon breaks. Download media on your server, store it with proper access controls and serve it to your team through an authenticated route.


Most WhatsApp API incidents start with a token in the wrong place or a webhook that trusts anyone. Fix those two, work down the checklist, and you're ahead of most integrations. If you're wiring WhatsApp into your own systems from your existing number, read the Tenashi API docs.