SMS Status Webhooks
Overview
SMS Status Webhooks let you receive real-time notifications when outbound SMS messages change status — for example when a message is Sent, Delivered, or Failed. Instead of polling the API, you register a webhook URL and the platform pushes status updates to you automatically.
This is a v2-only feature available at api/v2/status-webhooks.
Prerequisites
- An active Tech231 Platform account with API v2 access
- A publicly accessible HTTP or HTTPS endpoint to receive callbacks
- Valid authentication token (JWT) with your tenant
Quick Start
1. Register a Webhook
Code
2. Receive Status Updates
The platform will call your endpoint whenever a message reaches Sent, Delivered, or Failed status:
Code
3. Verify the Signature
If you provided a secret, validate the X-Hub-Signature-256 header:
Code
Managing Subscriptions
You can create multiple webhook subscriptions per tenant — each with its own URL, HTTP method, and template configuration.
Create Subscription
Code
Request Body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Yes | — | The HTTP/HTTPS URL to receive callbacks |
secret | string | No | — | HMAC secret for payload signing via X-Hub-Signature-256 |
name | string | No | — | Human-readable label for this subscription |
httpMethod | string | No | "POST" | HTTP method: POST, GET, PUT, or PATCH |
bodyTemplate | string | No | — | Custom body template with placeholder variables |
queryParamTemplate | string | No | — | Custom query parameter template with placeholder variables |
statusMappings | object | No | — | Dictionary mapping status values to custom strings (e.g., {"Sent": "pending"}) |
Response: 201 Created
Code
Update Subscription
Code
Request Body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Yes | — | The HTTP/HTTPS URL to receive callbacks |
secret | string | No | — | HMAC secret (set to null to remove) |
isActive | boolean | Yes | — | Enable or disable the subscription |
name | string | No | — | Human-readable label |
httpMethod | string | No | "POST" | HTTP method: POST, GET, PUT, or PATCH |
bodyTemplate | string | No | — | Custom body template |
queryParamTemplate | string | No | — | Custom query parameter template |
statusMappings | object | No | — | Dictionary mapping status values to custom strings |
Get Subscription
Code
List All Subscriptions
Code
Delete Subscription
Code
Default Payload
When no custom templates are configured, the platform sends a JSON payload via POST:
Code
Payload Fields
| Field | Type | Description |
|---|---|---|
messageId | string (UUID) | Unique identifier of the SMS message |
tenantId | string | Your tenant / organization identifier |
status | string | The new status: Sent, Delivered, or Failed |
senderId | string | The sender ID used for the message |
recipient | string | The recipient phone number |
timestamp | string | ISO 8601 timestamp of the status change |
providerMessageId | string | null | Provider-assigned message ID (if available) |
failureReason | string | null | Reason for failure (only present for Failed status) |
Template Placeholders
You can customize how the webhook request is formatted using body templates and query parameter templates. Templates use double-brace {{Variable}} placeholders that are replaced with actual values at delivery time.
Available Placeholders
| Placeholder | Description | Example Value |
|---|---|---|
{{MessageId}} | Unique SMS message identifier (UUID) | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
{{TenantId}} | Your tenant / organization identifier | org_abc123 |
{{Status}} | The new message status | Sent, Delivered, or Failed |
{{SenderId}} | Sender ID used for the message | MyApp |
{{Recipient}} | Recipient phone number | +231770001234 |
{{Timestamp}} | ISO 8601 timestamp of the status change | 2026-02-23T14:30:00.0000000Z |
{{ProviderMessageId}} | Provider-assigned message ID (empty string if unavailable) | prov-msg-001 |
{{FailureReason}} | Reason for failure (empty string if not failed) | Number unreachable |
Placeholder names are case-insensitive — {{messageid}}, {{MessageId}}, and {{MESSAGEID}} are all equivalent.
Status Mappings
By default, the {{Status}} placeholder resolves to the platform's internal status values: Sent, Delivered, or Failed. If your system uses different status terminology, you can provide a statusMappings dictionary to automatically translate these values.
How It Works
When a subscription has statusMappings configured:
- The platform looks up the original status (e.g.,
Delivered) in your mapping dictionary - If a mapping exists, the mapped value replaces
{{Status}}in templates and the default JSON payload - If no mapping exists for the status, the original value is used as fallback
Mapping keys are case-insensitive — "sent", "Sent", and "SENT" all match the Sent status.
Example: Map to Your Internal Status Codes
Code
When a message is delivered, the default payload will contain "status": "success" instead of "status": "Delivered". Templates also see the mapped value:
Code
Produces: {"state":"completed"}
Example: Numeric Status Codes
Code
Partial Mappings
You don't need to map every status — only the ones you want to rename. Unmapped statuses pass through unchanged:
Code
Here, Sent would remain as "Sent" in the payload, while Delivered becomes "OK" and Failed becomes "ERR".
Custom HTTP Methods
By default, webhooks are delivered via POST with a JSON body. You can change this per subscription using the httpMethod field.
Supported Methods
| Method | Body Sent? | Query Params? | Use Case |
|---|---|---|---|
POST | Yes (default JSON or bodyTemplate) | Optional via queryParamTemplate | Standard webhook delivery |
PUT | Yes (default JSON or bodyTemplate) | Optional via queryParamTemplate | Upsert-style endpoints |
PATCH | Yes (default JSON or bodyTemplate) | Optional via queryParamTemplate | Partial update endpoints |
GET | No | Yes (auto-generated or queryParamTemplate) | Simple status polling endpoints |
Custom Body Templates
Use bodyTemplate to control the exact JSON (or any text format) sent in the request body for POST, PUT, and PATCH methods.
Example: Custom JSON Structure
Code
The platform will deliver:
Code
Example: XML Body
Code
Example: Form-Encoded Body
Code
When bodyTemplate is omitted, the default JSON payload is used. This means existing subscriptions continue to work without changes.
Custom Query Parameter Templates
Use queryParamTemplate to append query parameters to the webhook URL. This is especially useful for GET requests or when your endpoint expects data in the URL.
Example: GET with Query Parameters
Code
The platform will call:
Code
Example: POST with Query Params and Body
You can combine both queryParamTemplate and bodyTemplate on body methods:
Code
GET Fallback (No Template)
When httpMethod is GET and no queryParamTemplate is provided, all non-empty variables are automatically appended as query parameters:
Code
Fallback Behavior Summary
| Configuration | Behavior |
|---|---|
No httpMethod | Defaults to POST |
POST/PUT/PATCH without bodyTemplate | Sends default JSON payload as request body |
POST/PUT/PATCH with bodyTemplate | Sends interpolated template as request body |
GET without queryParamTemplate | All variables auto-appended as query params |
GET with queryParamTemplate | Interpolated template appended as query params |
Any method with queryParamTemplate | Query params appended to URL (combined with body for non-GET) |
Status Values
Webhooks are triggered for the following status transitions:
| Status | Description |
|---|---|
Sent | Message was successfully submitted to the carrier |
Delivered | Message was confirmed delivered to the recipient's handset |
Failed | Message delivery failed (see failureReason for details) |
Failure Scenarios
The Failed status is triggered in these situations:
- Delivery receipt indicates failure — carrier reports the message as failed or expired
- Delivery receipt indicates undelivered — carrier could not deliver the message
- Permanent send failure — the platform exhausted all retry attempts
When a message fails, the failureReason field contains details about why.
Security
HMAC Signature Verification
If you provide a secret when creating a subscription, every webhook delivery includes an X-Hub-Signature-256 header containing an HMAC-SHA256 signature of the payload.
What gets signed:
- For
POST/PUT/PATCH: the request body content (either default JSON or interpolated template) - For
GET: the query parameter string
Verification examples (select a language):
Java
Code
C#
Code
Node.js
Code
Python
Code
Go
Code
PHP
Code
Timeout
All webhook deliveries have a 5-second timeout. Ensure your endpoint responds promptly. If it times out, the delivery is considered failed (but does not affect SMS processing).
Complete Examples
Example 1: Standard POST Webhook (Default)
Code
Receives the default JSON payload via POST.
Example 2: GET Callback to Legacy System
Code
Calls: GET https://legacy.yourapp.com/callback?id=abc123&st=Delivered&phone=%2B231770001234
Example 3: PUT Upsert with Custom Body
Code
Example 4: POST with Query Routing and Minimal Body
Code
Example 5: Custom Status Values with Body Template
Code
When a message is delivered, the webhook sends:
Code