WhatsApp Cloud API Setup: Step by Step With curl
Meta's dashboard makes the first message look like one button. Then you try to send from your own code, your token stops working by the afternoon, your webhook never fires, and the real number you added says "not registered". Each of those has a specific cause, and each one is a step the dashboard does not shout about.
This is a hands-on WhatsApp Cloud API setup guide that goes in the order things actually break. You will send the hello_world template from Meta's test number, reply to open the 24-hour window, send free-form text, receive webhooks with a signature check, swap the temporary token for a real one and register your own number. Every step has the curl command or code you need.
Before you begin
You need four things, and none of them cost money:
- A Facebook account registered as a Meta developer (developers.facebook.com).
- A phone with WhatsApp installed, to receive your test messages.
- A terminal with curl.
- A public HTTPS URL for webhooks. In development, a tunnelling tool that exposes your laptop over HTTPS works.
All examples use v23.0 in the URL. Swap in the current Graph API version from Meta's changelog when you build; older versions are retired on a schedule. Replace PHONE_NUMBER_ID with your number's ID and keep your token in $WHATSAPP_TOKEN.
Step 1: Create the app and get a test number
- Open the Meta App Dashboard and click Create App.
- Choose the Connect with customers through WhatsApp use case.
- Select an existing business portfolio or create a new one, then create the app.
- Click Start using the API. On the API Setup page, connect a Messaging account (Meta may have created one with the portfolio).
Meta now creates a test WhatsApp account, test Messaging account and a test business phone number for you. Copy two IDs from the API Setup panel: the test phone number ID and the Messaging account ID. The Messaging account ID is what older guides call the WABA ID; as of Meta's September 2026 account model change, existing WABA IDs simply denote Messaging accounts, and the API calls are unchanged.
Test numbers have relaxed messaging limits and do not need a payment method to send templates, which makes them the right place to build.
Step 2: Send the hello_world template
In API Setup, click Generate access token, pick the test number as From and add your own WhatsApp number as To. Then send from your terminal instead of the button:
curl -X POST "https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/messages" \
-H "Authorization: Bearer $WHATSAPP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "+15551234567",
"type": "template",
"template": {
"name": "hello_world",
"language": { "code": "en_US" }
}
}'
A success returns your recipient's wa_id and a message ID that starts with wamid.. Store that ID; every status webhook for this message will reference it.
Two details save debugging time. Always include the plus sign and country code in to: if you omit the plus, Meta prepends your business number's country code, which can deliver to the wrong person. And hello_world works because it is a sample template Meta provides on the test account. Your own templates need Meta approval before they send.
Step 3: Reply, then send free-form text
Templates are the only way to start a conversation. To send anything else, the user has to message you first. Reply to the hello_world message from your phone. That opens a 24-hour customer service window, and inside it you can send plain text:
curl -X POST "https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/messages" \
-H "Authorization: Bearer $WHATSAPP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "+15551234567",
"type": "text",
"text": { "body": "Got your reply. This one is free-form." }
}'
If you skip the reply, this call fails with error 131047: more than 24 hours have passed since the recipient last replied. That error is the window doing its job, not a bug. The window rules and the October 2026 pricing change for replies are covered in the 24-hour window explained.
Step 4: Receive webhooks and check the signature
Messages from users and delivery statuses (sent, delivered, read, failed) reach you only by webhook. Setup has two halves: a verification handshake when you save the URL, and signed POSTs after that.
import express from "express";
import crypto from "node:crypto";
const app = express();
// Keep the raw body: the signature is computed over the exact bytes Meta sent.
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
// Handshake: Meta sends a GET when you save the callback URL.
app.get("/webhook", (req, res) => {
const ok = req.query["hub.mode"] === "subscribe" &&
req.query["hub.verify_token"] === process.env.VERIFY_TOKEN;
return ok ? res.status(200).send(req.query["hub.challenge"]) : res.sendStatus(403);
});
// Events: messages and statuses arrive as POSTs.
app.post("/webhook", (req, res) => {
const got = req.get("X-Hub-Signature-256") || "";
const want = "sha256=" + crypto.createHmac("sha256", process.env.APP_SECRET)
.update(req.rawBody).digest("hex");
if (got.length !== want.length ||
!crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))) {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge first, then process
const value = req.body.entry?.[0]?.changes?.[0]?.value ?? {};
for (const msg of value.messages ?? []) {
// from can be missing for users with usernames; from_user_id (BSUID) is the stable key
console.log(msg.from_user_id, msg.from, msg.type, msg.text?.body);
}
for (const s of value.statuses ?? []) console.log(s.id, s.status);
});
app.listen(3000);
Then in the dashboard go to WhatsApp > Configuration, enter your callback URL and the same verify token, save it, and subscribe to the messages field. Reply to your test chat again and the payload should land with "object": "whatsapp_business_account".
If webhooks work for the test number but not for a Messaging account you created elsewhere, your app is probably not subscribed to it. Fix that with POST /MESSAGING_ACCOUNT_ID/subscribed_apps using a token with access to that account. Our guide to WhatsApp API webhooks goes deeper on payloads and retries.
Step 5: Replace the temporary token
The token from API Setup is a user access token. Meta says it expires quickly; in practice you regenerate it every few hours, and an expired token returns error 190. For anything that runs unattended, create a system user in Business Settings, assign it your app and your WhatsApp account, and generate a token with business_management, whatsapp_business_messaging and whatsapp_business_management.
Meta now offers both non-expiring system user tokens and 60-day expiring ones, and recommends the expiring kind. The full walkthrough, including refresh and rotation, is in our guide to the WhatsApp API permanent access token.
Step 6: Add, verify and register your real number
Moving from the test number to your own takes three distinct actions, and the dashboard only does the first two.
- Add the number in API Setup or WhatsApp Manager, with a display name and business profile.
- Verify ownership with a code by SMS or voice call.
- Register it for Cloud API with an API call. There is no button for this.
curl -X POST "https://graph.facebook.com/v23.0/PHONE_NUMBER_ID/register" \
-H "Authorization: Bearer $WHATSAPP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "messaging_product": "whatsapp", "pin": "583920" }'
The pin is a 6-digit two-step verification PIN. If the number has none yet, the value you send becomes its PIN. Meta began deprecating this PIN for eligible Cloud API numbers in September 2026, but still asks you to send a valid 6-digit value until it announces an API change.
Then add a payment method to the Messaging account in Business Settings. Without one, a real number can't send paid messages, and since October 1, 2026 service replies also stop once the number's 1,000 free ones for the month are used.
| Error | What it means | Fix |
|---|---|---|
133010 | Number not registered on the platform | Call the register endpoint |
133006 | Number needs verification first | Request and submit the SMS or voice code |
133005 | Two-step PIN incorrect | Reset the PIN in WhatsApp Manager or set a new one by API |
133016 | Too many register or deregister attempts | Wait: 10 attempts per 72 hours, then a 72-hour block |
131045 | Send failed due to a registration error | Register the number, then retry |
The number must not be active on WhatsApp or the WhatsApp Business app while you do this. If it is, read WhatsApp API phone number requirements before deleting anything.
Where Tenashi fits
Cloud API setup is the right investment when you need templates at scale, verified identity or deep automation. If what your team needs is to answer customers from one shared inbox on the number they already use, it's a lot of plumbing for that job. Tenashi links your existing WhatsApp number with a QR scan, with no app review, no number registration and no templates. Your team gets DMs and groups in one inbox with a CRM, notes and assignments, and a developer API on every paid plan, from $18/mo.
It is not Meta's platform: there are no interactive buttons or templates, sends are paced to protect your number, and it is not built for OTPs or high-volume alerts. Many teams run both: the Cloud API for notifications, Tenashi for the conversations people have with a human. For the bigger picture, see the complete guide to the WhatsApp API.
Read the API docs · Get started
FAQ
Is the WhatsApp Cloud API test number free?
Yes. Meta creates a test WhatsApp account, Messaging account and phone number when you set up the app, at no cost. Test numbers have relaxed messaging limits and can send template messages without a payment method on file. They are meant for development, so move to your own registered number before real customers rely on it.
How long does the temporary access token last?
Meta only says the API Setup token expires quickly and is not suitable for real development. In practice you regenerate it every few hours, and an expired token returns error 190. Replace it early with a system user token, either non-expiring or the 60-day expiring kind Meta now recommends, stored on your server.
Why is my webhook not receiving messages?
The usual causes: the GET handshake did not echo hub.challenge, you did not subscribe to the messages field, your app is not subscribed to that Messaging account, or your URL is not public HTTPS. Signature checks that hash a parsed body instead of the raw bytes also reject every request. Check each in that order.
Can I keep using the test number in production?
No. The test number exists for building and testing. Customers will not recognise it, you cannot set it up as your brand, and it has relaxed limits that do not reflect production. Add your own number, verify it, register it with the API, set a display name and add a payment method before going live.
Which Graph API version should I use?
Use the newest version listed in Meta's Graph API changelog when you start, and pin it in your code. This guide uses v23.0 in examples. Meta retires old versions on a schedule, so plan an upgrade check a couple of times a year and read the changelog for WhatsApp-specific changes.
Work through the steps in order and the usual first-day surprises (expired tokens, silent webhooks, an unregistered number) never show up. If your goal is a team inbox on your existing number instead, see every Tenashi feature.