TXTImpact can send your system an HTTPS request when something happens on your account: a text arrives, a message you sent is delivered or fails, a contact opts in or out, someone taps a tracked link, or a workflow finishes. This article covers every webhook TXTImpact sends and the format of each one.
How webhooks work
Every webhook is an HTTP POST with a JSON body. Every body starts with the same three fields, so one endpoint can receive all of them and branch on event:
| Field | Type | Description |
|---|---|---|
event |
string | What happened, for example message.received. See Events. |
id |
number | The id of this event. |
occurredAt |
string | When it happened, ISO-8601 UTC (YYYY-MM-DDTHH:MM:SSZ). |
The rest of the body depends on the event: a message, a contact, a workflow, or several of these together.
- All date-times are UTC.
- Phone numbers are digits only, country code first, no
+(for example12125550123). - A field with no value may be
nullor left out. Handle both. - New fields may be added at any time. Ignore fields you don't recognize.
Ways to receive webhooks
| Where you set it up | What it sends |
|---|---|
| Event webhooks: Integrations → Delivery Reports & Webhooks → Event webhooks, or the Hooks API | Any of the events below, for the whole account. One address can subscribe to several events. |
| Workflow Webhook action: a Webhook action inside a workflow |
workflow.webhook when the contact reaches that step. |
Use event webhooks for everything else: one subscription covers every number and every user on the account.
Set up an event webhook
- Go to Integrations → Delivery Reports & Webhooks.
- Under Event webhooks, click Add webhook.
- Enter your webhook address (it must start with
https://) and an optional label. - Choose the events to send, then save.
Events start flowing right away. Each subscription receives every event of its type on the account. Filter on your side if you only need some of them, for example by message.to or list.listId.
Hooks API
You can manage the same subscriptions from code. Send your API key in the ApiKey header. Create a key under Integrations → API Keys.
| Request | Purpose |
|---|---|
GET https://api.txtimpact.com/api/v2/Integration/API/HookEventTypes |
The events you can subscribe to |
GET https://api.txtimpact.com/api/v2/Integration/API/Hooks |
Your subscriptions |
POST https://api.txtimpact.com/api/v2/Integration/API/Hook |
Subscribe: {"eventType": "message.received", "endpoint": "https://example.com/hooks", "label": "CRM sync"}. The response includes the subscriptionId. |
PATCH https://api.txtimpact.com/api/v2/Integration/API/Hook/{subscriptionId}/Enabled/{true|false} |
Pause or resume a subscription |
DELETE https://api.txtimpact.com/api/v2/Integration/API/Hook/{subscriptionId} |
Unsubscribe |
The TXTImpact app for Zapier uses this API. When you turn a Zap on, its subscription appears under Event webhooks with a Zapier badge.
Security: the X-W2A-Token header
Set a webhook token under Integrations → Webhook Settings. Enter your own value or click Generate a token, then click Save. The token can be up to 50 characters: letters, numbers and . _ ~ + / = -.
It is one token for the whole account. TXTImpact sends it as a header on every webhook, of every kind:
X-W2A-Token: <your-token>
Reject any request whose X-W2A-Token does not match your stored copy. If no token is set, the header is not sent.
# Python / Flask
if request.headers.get("X-W2A-Token") != STORED_TOKEN:
return Response(status=401)Delivery, retries and duplicates
Respond with any 2xx status within 30 seconds. A non-2xx response, a timeout or a connection error is retried on this schedule:
| Attempt | Wait before the next attempt |
|---|---|
| 1 | 30 seconds |
| 2 | 2 minutes |
| 3 | 10 minutes |
| 4 | 30 minutes |
| 5 | 1 hour |
| 6 | 3 hours |
| 7 | Last attempt. No further retries. |
-
Answer fast. Return
200first, then do the work in the background, so you never reach the 30-second limit. -
Expect duplicates. A retry can deliver the same event again. Deduplicate on the record id inside the body:
message.smsInboxIdfor texts you receive,message.messageIdfor delivery reports,session.sessionIdfor completed workflows. -
Don't rely on order. Events can arrive out of order. Use
occurredAtto sequence them. -
410 Gone ends a subscription. If an event-webhook address answers
410, that subscription is switched off and receives nothing more. - Failing addresses are paused. An address that fails every attempt for a long period is paused to protect your account. Fix the endpoint, then turn it back on.
Events
| Event | Sent when |
|---|---|
message.received |
A text, MMS, RCS or WhatsApp message arrives on one of your numbers |
message.status |
A message you sent reaches its final status: delivered or failed |
link.clicked |
A contact taps a tracked link you sent |
contact.created |
A new contact is added |
contact.subscribed |
A contact joins a list, or opts back in (UNSTOP, START) |
contact.updated |
A contact's details change |
contact.unsubscribed |
A contact leaves a list or opts out (STOP) |
workflow.completed |
A contact reaches the End step of a workflow |
workflow.webhook |
A contact reaches a workflow's Webhook action (set up in the workflow, not under Event webhooks) |
message.received
{
"event": "message.received",
"id": 14637743,
"occurredAt": "2026-10-16T19:39:59Z",
"message": {
"smsInboxId": 14637743,
"text": "Yes, see you at 3",
"originalText": "Yes, see you at 3",
"from": "12125550123",
"to": "18445550100",
"messageType": "SMS",
"attachments": [],
"receivedAt": "2026-10-16T19:39:59Z"
},
"contact": {
"mobileNumber": "12125550123",
"name": "Jane Smith"
}
}| Field | Description |
|---|---|
message.smsInboxId |
TXTImpact's id for this inbound message. Use it to deduplicate. |
message.text |
The message text |
message.originalText |
The text as it was originally received |
message.from |
The sender's number |
message.to |
Your TXTImpact number that received it |
message.messageType |
The kind of message, for example SMS or MMS
|
message.attachments |
A list of full download URLs, one per photo, video or file. An empty list means no media. Each URL can be fetched as is; there is nothing to combine. |
message.receivedAt |
When the message arrived |
contact.name |
The contact's name, if you have one saved |
An MMS with two photos:
"message": {
"smsInboxId": 14637790,
"text": "Here are the photos",
"messageType": "MMS",
"attachments": [
"https://cloud.wire2air.com/mms/Inbound/9f2c41d7e0a64b52/photo1.jpg",
"https://cloud.wire2air.com/mms/Inbound/9f2c41d7e0a64b52/photo2.jpg"
],
...
}message.status
One per message, when the carrier reports the final outcome. Intermediate states (queued, sent) are not sent.
{
"event": "message.status",
"id": 52817734,
"occurredAt": "2026-10-16T15:40:08Z",
"message": {
"messageId": "af8d84df7e1d436d89e00a47815410c2",
"from": "18445550100",
"to": "12125550123",
"status": "delivered",
"statusCode": "11",
"statusMessage": "Message delivered",
"timestamp": "2026-10-16T15:40:08Z"
}
}| Field | Description |
|---|---|
message.messageId |
The id returned when you sent the message. Use it to match the report to your send, and to deduplicate. |
message.from |
The number you sent from |
message.to |
The recipient |
message.status |
delivered or failed
|
message.statusCode |
TXTImpact status code: 11 delivered, 19 failed, or a more specific failure code when the carrier gives one |
message.statusMessage |
A readable description of the status, for example why it failed |
message.timestamp |
When the carrier reported the status |
The message text is not included. You already have it from your send.
link.clicked
{
"event": "link.clicked",
"id": 52811020,
"occurredAt": "2026-10-16T16:02:06Z",
"originalUrl": "https://example.com/offer",
"shortCode": "aB3dE",
"mobileNumber": "12125550123",
"messageId": "af8d84df7e1d436d89e00a47815410c2",
"clickedAt": "2026-10-16T16:02:06Z"
}| Field | Description |
|---|---|
originalUrl |
Where the link points (the Destination URL) |
shortCode |
The short-link code that was tapped |
mobileNumber |
The contact who tapped it |
messageId |
The message that carried the link, when known |
clickedAt |
When it was tapped |
This event has no nested objects. Its fields sit at the top level.
contact.created, contact.subscribed, contact.updated, contact.unsubscribed
All four share one body. The event field says what happened.
{
"event": "contact.unsubscribed",
"id": 52813377,
"occurredAt": "2026-10-16T18:38:57Z",
"origin": "keyword",
"contact": {
"subscriberId": 1168587,
"mobileNumber": "12125550123",
"name": "Jane Smith",
"email": "jane@example.com",
"custom1": null,
"custom2": null,
"custom3": null,
"custom4": null,
"dob": "04/12/1990"
},
"list": {
"listId": 102851,
"listName": "Weekly Specials"
},
"membership": {
"serviceSubId": 9876543,
"status": "U",
"subscribedAt": "2026-03-02T14:10:00Z",
"unsubscribedAt": "2026-10-16T18:38:57Z"
},
"changedFields": null
}| Field | Description |
|---|---|
origin |
Where the change came from: api, ui (someone in the app), keyword (a text such as STOP or a join keyword) or workflow
|
contact.subscriberId |
TXTImpact's id for the contact |
contact.* |
The contact's saved details. dob is MM/DD/YYYY. |
list |
The list involved, or null when the change is not about a list (for example a name edit) |
membership.status |
S subscribed, U unsubscribed, I inactive |
membership.subscribedAt, unsubscribedAt
|
When the contact joined and left that list |
changedFields |
contact.updated only: the fields that changed |
Contact events are sent when a person or a text causes the change. Contacts added in bulk, by a CSV upload or a list import, do not send contact events.
workflow.completed
Sent once per session, when the contact reaches the workflow's End step. A session ended by the Stop or Restart shortcut does not send it.
{
"event": "workflow.completed",
"id": 52819001,
"occurredAt": "2026-10-16T17:20:44Z",
"workflow": {
"serviceId": 103209,
"workflowId": 4471,
"name": "Event RSVP",
"keyword": "RSVP",
"textNumber": "18445550100"
},
"contact": {
"mobileNumber": "12125550123",
"name": "Jane Smith"
},
"session": {
"sessionId": 778812,
"startedAt": "2026-10-16T17:18:02Z",
"completedAt": "2026-10-16T17:20:44Z"
},
"answers": [
{
"questionId": 9012,
"question": "Will you attend?",
"answer": "Yes",
"attachments": [],
"answeredAt": "2026-10-16T17:18:30Z"
},
{
"questionId": 9013,
"question": "How many guests?",
"answer": "2",
"attachments": [],
"answeredAt": "2026-10-16T17:20:40Z"
}
]
}-
answersare in question order. If a question was asked more than once, the last reply is used. -
attachmentsholds the URLs of any photos or files the contact sent as an answer. - To see real data in this shape before you subscribe, call
GET https://api.txtimpact.com/api/v2/Survey/Recent/Completed?take=5with yourApiKeyheader. Add&campaignId={serviceId}for one workflow.
Subscribe to workflow.completed once and it covers every workflow on the account.
workflow.webhook
Sent by a Webhook action you place inside a workflow, at the moment the contact reaches that step. It carries the message that reached the step, plus the workflow it belongs to.
{
"event": "workflow.webhook",
"id": 14637801,
"occurredAt": "2026-10-16T17:19:12Z",
"message": {
"smsInboxId": 14637801,
"text": "Yes",
"originalText": "Yes",
"from": "12125550123",
"to": "18445550100",
"messageType": "SMS",
"attachments": [],
"receivedAt": "2026-10-16T17:19:12Z"
},
"contact": {
"mobileNumber": "12125550123",
"name": "Jane Smith"
},
"workflow": {
"serviceId": 103209,
"sessionId": "a89ee50a65724343b40f032fa8ab2a79",
"moLogId": 14379477
}
}message and contact are the same as in message.received.
Need help?
Contact support with your account name and the address of your endpoint, and we will check what was sent and how your endpoint answered.