Exhibit A
Follow a stop event from payload to suppression list
{ "object": "whatsapp_business_account", "entry": [{ "id": "102290129340398", "changes": [{ "field": "user_preferences", "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "919876543210", "phone_number_id": "106540352242922" }, "contacts": [{ "wa_id": "919812345678" }], "user_preferences": [{ "wa_id": "919812345678", "user_id": "IN.40731866125599102847", "detail": "User requested to stop marketing messages", "category": "marketing_messages", "value": "stop", "timestamp": 1792387800 }] } }] }]}Step 5 of 5 · line 20
timestamp is an integer of Unix seconds, not a string as in message webhooks.
Webhook payload, step 5 of 5, line 20: timestamp is an integer of Unix seconds, not a string as in message webhooks.
What is the user preferences webhook?
The user preferences webhook is one of the WhatsApp Business Account webhook fields, user_preferences, that Meta posts when a person changes their marketing preference for your business inside WhatsApp. Each item in the value.user_preferences array says who changed it (user_id, the person's business-scoped user ID, and normally wa_id, which is left out for username users who have not shared their number), what it applies to (category, documented as marketing_messages), the new state (value is stop or resume), a readable detail such as "User requested to stop marketing messages", and a timestamp in Unix seconds — an integer here, unlike the string timestamps in message webhooks.
It exists because customers can switch off a business's marketing using WhatsApp's own controls, without typing anything your bot could parse. Once a person has stopped marketing from you, Meta does not deliver it: any marketing template you send them fails with error 131050, "This recipient has chosen to stop receiving marketing messages on WhatsApp from your business", and Meta's guidance for that error is not to retry. The event lets you record the change the moment it happens instead of discovering it through failed campaigns.
It is not the same as keyword opt-out handling. When a customer replies "STOP" in a chat, that arrives as an ordinary text message in the messages field and your own code decides it means an opt-out. user_preferences is WhatsApp telling you about a choice made through the app, with no message text involved. It concerns marketing messages only, it is not a block (a customer who blocks your number is a separate case), and Meta notes it does not fire for "Interested" or "Not interested" feedback given through the Offers and announcements setting.
Key takeaways
- The user preferences webhook (user_preferences) fires when a WhatsApp user stops or resumes marketing messages from your business.
- Each item carries the BSUID in user_id, usually wa_id, category marketing_messages, value stop or resume, and an integer Unix timestamp.
- After a stop, marketing templates to that user fail with error 131050, and Meta advises not to retry them.
- It covers marketing only and never replaces your own STOP keyword handling, which arrives as an ordinary inbound message.
- The field must be subscribed explicitly and needs the whatsapp_business_management permission.
The user preferences webhook in code
from datetime import datetime, timezonefrom flask import Flask, requestapp = Flask(__name__)@app.post("/webhooks/whatsapp")def whatsapp_webhook():verify_signature(request) # X-Hub-Signature-256 over the raw bodybody = request.get_json()for entry in body.get("entry", []):for change in entry.get("changes", []):if change.get("field") != "user_preferences":continue # other fields are handled elsewherefor pref in change["value"].get("user_preferences", []):if pref.get("category") != "marketing_messages":continuejobs.enqueue("sync_marketing_preference", {"user_id": pref.get("user_id"), # BSUID"wa_id": pref.get("wa_id"), # omitted for some username users"value": pref["value"], # "stop" or "resume""changed_at": datetime.fromtimestamp(pref["timestamp"], timezone.utc).isoformat(),})return "", 200 # always acknowledge
Pick out user_preferences changes, queue them for the consent sync and acknowledge straight away.
Why does the user preferences webhook matter?
Consent has to match what the customer actually did. India's DPDP Act expects withdrawing consent to be as easy as giving it, and Meta's policy expects businesses to honour opt-outs. If your CRM still marks someone as opted in after they stopped your marketing inside WhatsApp, every campaign wastes an attempt that fails with 131050, delivery reports look worse than they are, and your consent evidence contradicts the customer's own action. Syncing this one webhook into your suppression list keeps campaign audiences, consent records and reporting honest — and the resume event tells you when someone wants to hear from you again.
How does the user preferences webhook work?
Subscribe to the field
Add
user_preferencesto your app's WhatsApp Business Account webhook fields. It needs thewhatsapp_business_managementpermission (themessagesfield needswhatsapp_business_messaging), and it does not arrive unless you subscribe to it explicitly.Read every preference item
The notification has
field: "user_preferences"and avaluewithmetadata,contactsand auser_preferencesarray. Loop over every item rather than reading only the first.Turn it into a consent event
Write an append-only consent record: the user (BSUID and
wa_id), the new state (stoporresume), the event time and the source (whatsapp_user_preferences), so you can later show when and how the preference changed.Update the suppression list
A stop adds the user to the marketing suppression list that every sending tool checks — broadcasts, journeys, the CRM's campaign module. A resume lifts only the block this source created; a keyword STOP from the same customer stays in force.
Keep the latest choice
Webhooks can arrive late or twice. Compare the event's
timestampwith the last stored change and ignore anything older, so a delayed stop cannot undo a newer resume.
Common mistakes with the user preferences webhook
Treating it as your only opt-out signal
Customers also type STOP, tap an opt-out quick reply in your template or tell an agent. Keep keyword and button handling, and feed every source into the same suppression list.
Suppressing every message type
The preference covers marketing. Blocking order updates, OTPs and support replies as well punishes the customer for muting promotions. Suppress marketing only — unless the customer asked you to stop everything, which you then honour from your own records.
Treating resume as a brand-new opt-in
Resume lifts the stop made in WhatsApp. It does not replace the opt-in evidence your policy needs, so keep the original consent record and collect a fresh opt-in if you never had one.
Letting a late event win
A retried stop that arrives after a newer resume would wrongly suppress the customer. Compare timestamps and keep only the latest change per user.
Example: A Surat saree shop's Diwali list
A customer of a Surat saree shop stops the shop's marketing messages in WhatsApp on a Sunday evening. The user_preferences webhook arrives seconds later with value: "stop"; the handler logs a consent event and adds her to the marketing suppression list. Monday's Diwali broadcast excludes her automatically, so no send fails with 131050. Her order confirmations — utility templates — keep arriving as normal. Three weeks later she resumes marketing from the shop, the resume event lifts the block, and she is back in the next campaign audience because her original opt-in record still stands.
User Preferences Webhook FAQs
Does a WhatsApp marketing stop block utility or authentication templates?+
Can I still send marketing to someone who stopped?+
Do I still need to handle STOP replies if I use this webhook?+
How is a stop different from the per-user marketing limit?+
Is a user_preferences stop the same as the customer blocking my number?+
Best practices for the user preferences webhook
- Check the suppression list at send time as well as when the audience is built, so a stop that lands mid-campaign is honoured.
- Chart stops per campaign: a spike after one send is the clearest sign of message fatigue.
- Store the BSUID (
user_id) from each preference item, so records keep matching as usernames roll out and numbers change. - Treat a 131050 failure as a missed or unsynced stop and backfill the suppression list from it.