Opt-in webhooks send a message to your server each time someone opts in to one of your campaigns. Your system hears about new subscribers right away. You don't need to keep asking the API for them.
This article is for the developer who builds the endpoint that receives these messages. It covers what mCommons sends, how to check that a message is real, and how to handle repeats.
Note: Mobile Commons turns on opt-in webhooks one account at a time. To request access, contact your Customer Success Manager or Technical Support.
- What Triggers a Webhook
- Before You Start
- What mCommons Sends
- Verifying the Signature
- Responding to a Webhook
- Handling Retries and Repeat Opt-ins
- Test Webhooks
- Troubleshooting
What triggers a webhook
Each webhook covers either all opt-ins in your account, all opt-ins to one campaign, or opt-ins through one opt-in path. Within that scope, mCommons sends a webhook when someone:
- Opts in by keyword, QR code, web form or API call
- Confirms a double opt-in (mCommons sends the webhook after they confirm, not when it first asks them)
- Opts back in after opting out
- Opts in again while already subscribed, for example by texting the keyword a second time
- Joins a campaign through a bulk add, for example from a group
- Joins through a data sync from your CRM
mCommons doesn't send a webhook for numbers on your account's block list.
Note: A webhook that covers all opt-ins sends one message per person when a group is added to a campaign. A large group can send thousands of messages in a short time, so make sure your endpoint can handle the volume.
Before you start
You need:
-
An HTTPS endpoint on a public domain. mCommons doesn't send to
http://addresses or to private or internal network addresses. - The signing secret for the webhook. mCommons shows it only once, when someone creates the webhook or rotates its secret. Get it from the person who set up the webhook, and store it somewhere secure, such as your environment variables or a secrets manager.
What mCommons sends
Each webhook is an HTTPS POST with a JSON body.
Headers
| Header | Value |
|---|---|
Content-Type |
application/json |
X-MC-Webhook-Event |
The event type. Always opt_in today. |
X-MC-Webhook-Delivery |
A unique ID for this delivery. Retries of the same delivery use the same ID. |
X-MC-Webhook-Signature |
sha256= followed by the signature of the body. See Verifying the Signature. |
Body
{
"event": "opt_in",
"phone": "15555550100",
"campaign_id": 12345,
"campaign_name": "Voter Registration 2026",
"opt_in_path_id": 678,
"opt_in_path": "VOTE Keyword Opt-In",
"source": "OptInPath",
"activated_at": "2026-10-08T14:30:00Z",
"delivery_id": "3f1c2a9e-5b7d-4e8f-a1c2-9d0e6b4f7a21"
}
| Field | Type | Description |
|---|---|---|
event |
string | The event type. Always opt_in today. Check it so your code still works if new types appear later. |
phone |
string | The person's mobile number: digits only, starting with the country code and with no +. For example, 15555550100. |
campaign_id |
integer | The ID of the campaign the person opted in to. |
campaign_name |
string | The name of that campaign. |
opt_in_path_id |
integer or null | The ID of the opt-in path the person used. Empty if the opt-in didn't come through an opt-in path. |
opt_in_path |
string or null | The name of that opt-in path. Empty if the opt-in didn't come through an opt-in path. |
source |
string | What created the subscription, for example OptInPath or Group. |
activated_at |
string | When the person first opted in to this campaign, in ISO 8601 format (UTC). If they opt in again later, this stays the original time. |
delivery_id |
string | The same value as the X-MC-Webhook-Delivery header. |
Tip: If your system stores numbers with a leading +, add it to phone before you look up the contact.
Verifying the signature
Check the signature on every webhook before you trust it. A valid signature shows the message came from mCommons and wasn't changed on the way.
- Read the
X-MC-Webhook-Signatureheader and remove thesha256=prefix. - Compute an HMAC-SHA256 of the raw request body, using the signing secret as the key. Use the body exactly as received, before any JSON parsing.
- Convert the result to lowercase hexadecimal.
- Compare it to the value from the header with a constant-time comparison. If they don't match, reject the request.
Python example:
import hashlib
import hmac
def is_valid(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
received = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
JavaScript (Node) example:
const crypto = require("crypto");
function isValid(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const received = (signatureHeader || "").replace(/^sha256=/, "");
if (expected.length !== received.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
When the secret changes: If someone rotates the signing secret in mCommons, the old secret stops working right away. mCommons signs every webhook after that with the new one. Update your endpoint as soon as you receive the new secret. Plan any rotation together with whoever manages the webhook in mCommons.
Responding to a webhook
Return any 2xx status code to confirm you received the webhook. mCommons treats anything else as a failure, including:
- Any
3xx,4xxor5xxstatus (mCommons doesn't follow redirects) - No connection within 5 seconds
- No response within 10 seconds
Respond within a few seconds. If your processing takes longer, return 200 first and do the work afterward.
Retries
If a webhook fails, mCommons tries again:
| Attempt | When |
|---|---|
| 1 | Right away |
| 2 | 1 minute after the first attempt fails |
| 3 | 5 minutes after the second attempt fails |
After 3 failed attempts, mCommons stops trying and doesn't send that opt-in again. If someone turns off or deletes the webhook in mCommons, mCommons doesn't send the opt-ins from that time later either. To catch up after an outage, use the API to list campaign subscribers for the time you missed.
Handling retries and repeat opt-ins
Your endpoint can receive more than one message about the same person. There are two cases.
The same delivery, sent again. A retry can arrive even if your server processed the first attempt, for example when your response was slow. Retries share a delivery_id. Store each delivery_id you process and skip any you've already seen.
A new opt-in from someone already subscribed. If a person opts in again, for example by texting the keyword twice, mCommons sends a new webhook with a new delivery_id. If you only need to know that someone is subscribed, treat phone plus campaign_id as the key, and update the existing record instead of creating a new one.
Test webhooks
Whoever manages the webhook in mCommons can send a test at any time. A test:
- Goes out once, with no retries
- Carries a signature made with the real signing secret, so you can check your signature code
- Includes
"test": truein the body - Uses sample values, including the phone number
+15555550100and the campaign nameTest Campaign - Has no
delivery_idin the body
Test webhooks use a + in the phone number, but real opt-ins don't. Don't use a test to check your phone-number matching.
{
"event": "opt_in",
"test": true,
"phone": "+15555550100",
"campaign_id": null,
"campaign_name": "Test Campaign",
"opt_in_path_id": null,
"opt_in_path": null,
"source": "OptInPath",
"activated_at": "2026-10-08T14:30:00Z"
}
If the webhook covers one campaign or opt-in path, campaign_id, opt_in_path_id and opt_in_path show those values instead of being empty.
Skip or flag messages with "test": true so they don't create real contacts in your system.
Troubleshooting
| Problem | What to check |
|---|---|
| Every signature check fails | Make sure you sign the raw body, not parsed and re-serialized JSON. Check that you removed the sha256= prefix and that the secret has no extra spaces. If someone rotated the secret recently, update it. |
| The test arrives but real opt-ins don't | Check that the webhook is on in mCommons and that its campaign and opt-in path match the opt-ins you expect. |
| No webhooks arrive at all | Confirm your endpoint is reachable from the internet over HTTPS and that a firewall isn't blocking it. Ask whoever manages the webhook to send a test and check its result. |
| The same person arrives more than once | See Handling Retries and Repeat Opt-ins. |
| Contacts don't match | The phone field has no +. Format it the same way as the numbers in your system before matching. |
For more help, contact Technical Support.