How to Build a WhatsApp Opt-In and Opt-Out System That Keeps Your API Account Safe

If you run an e-commerce store, a logistics service, or a software platform in Pakistan or India, you already know that SMS open rates are terrible. WhatsApp is where your customers actually read messages. But there is a catch. If you send automated messages to people who did not explicitly ask for them, they will block you.
On WhatsApp, reporting a business takes exactly two taps. If your spam report rate spikes, Meta’s automated systems will flag your phone number. Your quality rating will drop from Green (High) to Yellow (Medium) or Red (Low). If it stays Red, Meta will restrict your messaging limits—dropping you from 100,000 messages a day to 10,000, then 1,000, and eventually disabling your API account entirely.
To prevent this, you must build an explicit opt-in and opt-out system. This guide walks you through the exact technical steps, database schemas, and webhook handlers required to set this up from scratch.
What You Need Before Writing Any Code
Do not start coding until you have these four components ready:
- A WhatsApp Business API Account: You can set this up directly via the Meta Developer Portal or through a Business Solution Provider (BSP) like WA Link. While WA Link simplifies template approval and API routing, you still need to build the database logic to respect user choices.
- A Webhook Server: A running backend service (Node.js, Python, PHP, or Go) accessible via an HTTPS URL. Meta requires SSL; you cannot use a plain HTTP endpoint for production webhooks. For local testing, use a tool like ngrok to expose your local port.
- A Database: You need a persistent store (like PostgreSQL, MySQL, or MongoDB) to track the subscription state of every phone number.
- A Verified Meta Business Manager: While you can test in sandbox mode without verification, you cannot scale your messaging tiers without it.
The Rules of Consent: What Meta Actually Demands
Meta updated its policies to allow both on-WhatsApp and off-WhatsApp opt-ins. However, the core rule remains: you must clearly state what the user is signing up to receive. You cannot bundle WhatsApp consent inside a generic "Terms and Conditions" checkbox.
Your opt-in flow must show:
- The specific business name the user is subscribing to.
- That the messages will be sent via WhatsApp.
- The type of information you will send (e.g., order updates, shipping alerts, or promotional offers).
Conversely, the opt-out must be immediate. If a user says "stop," "unsubscribe," or clicks an opt-out button, you must update your database instantly. Sending even one single promotional message after a user has opted out violates Meta’s policy and guarantees a spam report.
Step 1: Designing the Database Schema
Do not rely on Meta to remember who opted out. Your database must be the single source of truth. Before sending any message, your system must query this database.
Here is a simple SQL schema to handle this. We track the phone number, the opt-in status, the channel they opted in through, and the timestamp of the last change.
CREATE TABLE whatsapp_subscribers (
phone_number VARCHAR(20) PRIMARY KEY,
is_subscribed BOOLEAN DEFAULT FALSE,
opt_in_source VARCHAR(50), -- 'website_checkout', 'whatsapp_chat', 'sms'
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
If you use a non-relational database like MongoDB, your schema should look like this:
{
"_id": "923001234567",
"is_subscribed": true,
"opt_in_source": "website_checkout",
"updated_at": "2026-03-30T10:14:00Z"
}
Always store phone numbers in full international format without spaces, dashes, or leading zeros (for example, 923001234567 for Pakistan or 919876543210 for India). This matches the format Meta uses in webhook payloads.
Step 2: Implementing the Opt-In Flow
There are two practical ways to collect opt-ins. You can collect them on your website (off-WhatsApp) or directly inside a chat thread (on-WhatsApp).
Method A: The Website Checkout Checkbox
If you run an e-commerce store, add a checkbox at the checkout page near the phone number field. It should look like this:
[ ] Send me order updates and exclusive offers via WhatsApp
Do not pre-check this box. Pre-checked boxes are a bad idea because users do not read them, receive messages they did not expect, and immediately report your number as spam. When the user checks the box and submits the form, send an API request to your backend to update your database:
// Node.js Express endpoint example
app.post('/api/checkout', async (req, res) => {
const { phoneNumber, optIn } = req.body;
if (optIn) {
await db.query(
`INSERT INTO whatsapp_subscribers (phone_number, is_subscribed, opt_in_source)
VALUES ($1, true, 'website_checkout')
ON CONFLICT (phone_number)
DO UPDATE SET is_subscribed = true, opt_in_source = 'website_checkout'`,
[phoneNumber]
);
}
// Proceed with order processing
});
Method B: Conversational Opt-In
If a customer initiates a chat with you, you do not automatically have the right to send them promotional broadcasts. You can, however, ask them for consent during the conversation. Use a Quick Reply button template to make this easy.
Your template text should say: "Would you like to receive updates about your shipment on WhatsApp?" with two buttons: "Yes, sign me up" and "No, thanks".
Step 3: Handling Opt-Outs via Webhooks
You must give users a clear way to stop receiving messages. The most reliable way to do this is by adding a "Marketing Opt-Out" button to your marketing templates. Meta actually enforces this for utility and marketing templates in many regions.
When a user clicks your "Opt Out" button or types "STOP", Meta sends a POST request to your configured webhook URL. Your server must process this payload, identify the user's phone number, and set is_subscribed to false.
Here is what the incoming JSON payload from Meta looks like when a user clicks an "Opt Out" quick-reply button:
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "109983728372",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "923007654321",
"phone_number_id": "12098374823"
},
"contacts": [
{
"profile": {
"name": "Zubair Khan"
},
"wa_id": "923001234567"
}
],
"messages": [
{
"from": "923001234567",
"id": "wamid.HBgLOTIzMDAxMjM0NTY3FQIAERgSQjM0RDY4REU1RTdDQTMyM0FCAA==",
"timestamp": "1774865640",
"type": "button",
"button": {
"payload": "opt_out_marketing",
"text": "Opt Out"
}
}
]
},
"field": "messages"
}
]
}
]
}
Your webhook code must parse this payload. Here is how to handle it in Node.js:
app.post('/webhook', async (req, res) => {
const body = req.body;
// Validate that this is a WhatsApp webhook payload
if (body.object === 'whatsapp_business_account') {
const entry = body.entry?.[0];
const change = entry?.changes?.[0]?.value;
const message = change?.messages?.[0];
if (message) {
const senderPhone = message.from; // e.g., "923001234567"
// Check if user clicked the opt-out button
const isOptOutButton = message.type === 'button' && message.button?.payload === 'opt_out_marketing';
// Also check if they typed "STOP" or "UNSUBSCRIBE" as a text message
const isOptOutText = message.type === 'text' &&
['stop', 'unsubscribe', 'block'].includes(message.text.body.trim().toLowerCase());
if (isOptOutButton || isOptOutText) {
// Update database
await db.query(
`UPDATE whatsapp_subscribers
SET is_subscribed = false
WHERE phone_number = $1`,
[senderPhone]
);
console.log(`Successfully unsubscribed: ${senderPhone}`);
// Optional: Send a single confirmation message back to the user
await sendOptOutConfirmation(senderPhone);
}
}
// Always return a 200 OK to Meta quickly to prevent webhook retries
return res.status(200).send('EVENT_RECEIVED');
}
return res.sendStatus(404);
});
Step 4: Confirming Your System Works
Do not assume your code works because it compiled. You must test both the positive and negative pathways.
How to Test the Opt-In
- Go to your website checkout or registration page.
- Enter your personal phone number but leave the WhatsApp checkbox unchecked. Complete the registration.
- Check your database. Your phone number should either not exist in the
whatsapp_subscriberstable or haveis_subscribedset tofalse. - Run your automated notification script. Your script must skip sending the WhatsApp message to your number.
- Repeat the process, this time checking the box. Verify that the database updates to
trueand that your script successfully triggers the WhatsApp API.
How to Test the Opt-Out
- Send a template message containing your "Opt Out" button to your personal phone number.
- Tap the "Opt Out" button on your phone.
- Monitor your server logs. You should see the incoming POST request from Meta with the payload type
buttonand payload valueopt_out_marketing. - Query your database:
SELECT is_subscribed FROM whatsapp_subscribers WHERE phone_number = 'your_number';. It must showfalse. - Try to trigger another notification to your number. Your backend logic should block the API call before it ever reaches Meta.
What to Do When the System Fails
Even with clean code, things will occasionally break. Here are the most common failure points and how to fix them.
The Webhook Timeout Error
Meta requires your webhook server to respond with an HTTP 200 OK status code within 5 seconds. If your database query is slow or your server tries to send the confirmation message before responding to Meta, the connection will time out. Meta will retry sending the webhook, creating a loop that can crash your server.
The Fix: Process webhooks asynchronously. Read the incoming payload, immediately send the 200 OK response to Meta, and then process the database update and confirmation message in the background.
The "User Opted Out But Received Message Anyway" Bug
This happens when you have race conditions. For example, a bulk marketing campaign starts running at 10:00 AM. It queries the database, gets 5,000 active numbers, and starts looping through them. At 10:01 AM, a user on that list clicks "Opt Out." Your webhook updates the database. However, your sending loop already loaded the user into memory, so it sends the message anyway at 10:03 AM.
The Fix: If you are running large campaigns, do not load the entire list into memory at once. Query your database in small batches (e.g., 100 users at a time) and verify the is_subscribed flag immediately before calling the WhatsApp API for each batch.
Your Quality Rating Drops to "Red"
If you see your Quality Rating drop in the Meta Business Suite, it means users are flagging your messages. This usually means your opt-in was unclear, or you are sending messages too frequently.
The Fix: Immediately pause all active campaigns. Review your opt-in sources. If you are using a BSP like WA Link, check their template analytics to see which specific template has the highest block rate. Redesign or delete that template.
What to Do Next
Now that your core opt-in and opt-out database logic is active, you need to configure your templates in the Meta Business Manager. Make sure every marketing template you submit for approval includes a Quick Reply button with the custom payload opt_out_marketing.
You should also set up an automated script that runs weekly to clean your database. If a phone number returns an API error code 131030 (Recipient phone number not associated with a WhatsApp account) multiple times, mark that user as inactive to save on API costs.
Frequently Asked Questions
Can I pre-check the WhatsApp opt-in checkbox on my registration form?
No. While Meta's automated systems cannot physically check your website code, doing this violates their policy. More importantly, it leads to high spam report rates from users who did not realize they signed up. This will quickly destroy your phone number's quality rating.
What happens if I keep messaging a user who opted out?
If a user reports you to Meta after opting out, and Meta detects that you continued messaging them after they sent an opt-out keyword or clicked an opt-out button, Meta can suspend your WhatsApp Business API account permanently. There is no warning period for clear policy violations of this type.
Do I have to pay Meta for opt-out messages?
Yes. If a user replies to you to opt out, that message is free (incoming). However, if you send them an automated confirmation message back (e.g., "You have been unsubscribed"), that message initiates a user-initiated conversation, which will be charged according to Meta's standard conversation-based pricing. To avoid this cost, you can simply update your database without sending a confirmation message.
Can I use WA Link to automatically manage my opt-outs?
While WA Link helps you register templates with opt-out buttons and routes the incoming webhook data to your server, it cannot modify your internal database. You must still write the backend webhook listener that receives the payload from WA Link and updates your system's user records.