Webhooks


Webhooks

Receive real-time notifications about message delivery events by configuring webhook endpoints.

Overview

Webhooks let your application receive automatic HTTP POST callbacks whenever messaging events occur — a message is sent, delivered, fails, or a recipient replies. Instead of polling the API for status updates, you configure an endpoint URL and the platform pushes events to you as they happen.

How it works:

  1. Create a webhook via the Dashboard, specifying your endpoint URL and the event types you want to receive
  2. When a matching event occurs, the platform sends a POST request to your endpoint with the event payload
  3. Your server processes the event and returns a 2xx response
  4. If delivery fails, the platform retries with increasing backoff intervals

Event types

Subscribe to specific event types or use wildcard patterns to receive broader categories of events.


Available events

Event typeDescription
messaging.outgoing.message.queuedAn outbound message has been queued for delivery
messaging.outgoing.message.sentAn outbound message was sent to the carrier
messaging.outgoing.message.failedAn outbound message failed to send
messaging.outgoing.message.deliveredAn outbound message was confirmed delivered to the recipient
messaging.outgoing.message.undeliveredAn outbound message could not be delivered to the recipient
messaging.incoming.message.receivedAn inbound message was received from a recipient
messaging.broadcast.initiatedA broadcast has been initiated and messages are being dispatched
messaging.broadcast.completedA broadcast has finished processing all messages successfully
messaging.broadcast.failedA broadcast has failed — all batches errored or no messages were sent
tracking.link.clickedA tracking link was clicked by a human visitor
tracking.link.createdA new tracking link was created
tracking.link.updatedA tracking link was updated
tracking.link.deletedA tracking link was deleted
tracking.link.expiredA tracking link expired automatically

Wildcard patterns

Use wildcard patterns to subscribe to multiple event types with a single entry:

PatternMatches
*All event types
messaging.*All messaging events (outgoing, incoming, and broadcast)
messaging.outgoing.*All outgoing message events
messaging.outgoing.message.*All outgoing message status events
messaging.incoming.*All incoming message events
messaging.incoming.message.*All incoming message events
messaging.broadcast.*All broadcast lifecycle events
tracking.*All tracking events (link clicks and lifecycle)
tracking.link.*All tracking link events

Payload structure

Every webhook delivery sends a JSON payload with a consistent envelope format.

Envelope

{
  "schemaVersion": "v1",
  "webhookId": "whk_abc123",
  "events": [
    {
      "eventId": "evt_def456",
      "eventType": "messaging.outgoing.message.sent",
      "timestamp": "2026-03-17T12:00:00.000Z",
      "data": {
        "id": "msg_789xyz",
        "direction": "outbound",
        "from": "+15551234567",
        "to": "+15559876543",
        "status": "sent",
        "broadcastId": null,
        "sentAt": "2026-03-17T12:00:00.000Z"
      }
    }
  ]
}
FieldTypeDescription
schemaVersionstringPayload format version (currently "v1")
webhookIdstringThe webhook configuration ID this delivery is for
eventsarrayArray of event objects included in this delivery
events[].eventIdstringUnique identifier for this event (prefixed with evt_)
events[].eventTypestringThe event type that triggered this delivery
events[].timestampstringISO 8601 timestamp of when the event occurred
events[].dataobjectEvent-specific payload (see schemas below)
💡

webhookId identifies your webhook configuration — it is the same for every delivery to that webhook. Use eventId as the unique identifier per event.

📦

The events array contains a minimum of 1 event per delivery. Events may be batched into a single delivery, so always iterate the full array rather than reading only the first element.

⚠️

The schemaVersion field indicates the payload format version. The schema will evolve over time — always check this field and parse the payload accordingly. Ignore unknown fields gracefully to ensure forward compatibility.

Outgoing message events

These fields are included in the data object for all outgoing message events (queued, sent, failed, delivered, undelivered).

Common fields

FieldTypeDescription
idstringMessage ID (prefixed with msg_)
directionstringAlways "outbound" for outgoing messages
fromstringSender phone number
tostringRecipient phone number
statusstringMessage status (see per-event details below)
broadcastIdstring | nullBroadcast ID (prefixed with brc_) if part of a broadcast, otherwise null

Per-event fields

messaging.outgoing.message.queued

FieldTypeDescription
statusstring"queued"
createdAtstringISO 8601 timestamp of when the message was created

messaging.outgoing.message.sent

FieldTypeDescription
statusstring"sent"
sentAtstringISO 8601 timestamp of when the message was sent to the carrier

messaging.outgoing.message.delivered

FieldTypeDescription
statusstring"delivered"
deliveredAtstringISO 8601 timestamp of when delivery was confirmed

messaging.outgoing.message.failed

FieldTypeDescription
statusstring"failed"
errorTypestringError classification (e.g., opted_out, invalid_destination, provider_error)
errorMessagestringHuman-readable error description
createdAtstringISO 8601 timestamp of when the failure occurred

messaging.outgoing.message.undelivered

FieldTypeDescription
statusstring"undelivered"
errorTypestringError classification
errorMessagestringHuman-readable error description
failedAtstringISO 8601 timestamp of when the delivery failure was reported

See Error Codes — Message error types for the full list of errorType values and their descriptions.


Incoming message events

messaging.incoming.message.received

FieldTypeDescription
idstringMessage ID (prefixed with msg_)
directionstringAlways "inbound"
fromstringSender phone number (the recipient who replied)
tostringYour phone number that received the message
statusstring"received"
bodystring | undefinedText content of the message (may be absent for media-only messages)
mediaItemsarray | nullArray of media attachments, or null if none
mediaItems[].mediaUrlstringURL of the media file
mediaItems[].contentTypestringMIME type of the media (e.g., image/jpeg)
receivedAtstringISO 8601 timestamp of when the message was received

Broadcast events

messaging.broadcast.initiated

Fired when a broadcast has been initiated and messages are being dispatched to carriers.

FieldTypeDescription
idstringBroadcast ID (prefixed with brc_)
statusstring"initiated"
fromstringSender phone number
totalRecipientsnumberTotal number of recipients in the broadcast
createdAtstringISO 8601 timestamp of when the broadcast was created
initiatedAtstringISO 8601 timestamp of when the broadcast was initiated

messaging.broadcast.completed

Fired when a broadcast has finished processing all messages (at least one message was sent successfully).

FieldTypeDescription
idstringBroadcast ID (prefixed with brc_)
statusstring"completed"
fromstringSender phone number
totalRecipientsnumberTotal number of recipients in the broadcast
totalSentnumberNumber of messages successfully sent
totalFailednumberNumber of messages that failed to send
createdAtstringISO 8601 timestamp of when the broadcast was created
completedAtstringISO 8601 timestamp of when the broadcast finished processing

messaging.broadcast.failed

Fired when a broadcast has failed — either all batches errored or no messages were successfully sent.

FieldTypeDescription
idstringBroadcast ID (prefixed with brc_)
statusstring"failed"
fromstringSender phone number
totalRecipientsnumberTotal number of recipients in the broadcast
totalSentnumberNumber of messages successfully sent (always 0 for failed)
totalFailednumberNumber of messages that failed to send
errorMessagestringDescription of why the broadcast failed
createdAtstringISO 8601 timestamp of when the broadcast was created
failedAtstringISO 8601 timestamp of when the broadcast was marked as failed

Tracking link events


Click event

tracking.link.clicked

Fired when a tracking link is clicked by a human visitor. Bot and link-preview clicks are filtered out automatically.

FieldTypeDescription
idstringClick ID (prefixed with clk_)
trackingLinkIdstringTracking link ID (prefixed with tlk_)
urlstringThe full tracking link URL
redirectUrlstringThe destination URL the visitor was redirected to
messageIdstringMessage ID (prefixed with msg_) — present when the link is associated with a message
broadcastIdstringBroadcast ID (prefixed with brc_) — present when the link is associated with a broadcast
flowIdstringFlow ID (prefixed with fl_) — present when the link is associated with a flow
clickedAtstringISO 8601 timestamp of when the click occurred
ipAddressstring | nullIP address of the visitor

Lifecycle events

tracking.link.created

Fired when a new tracking link is created.

FieldTypeDescription
idstringTracking link ID (prefixed with tlk_)
urlstringThe full tracking link URL
redirectUrlstringThe destination URL
createdAtstringISO 8601 timestamp of when the tracking link was created

tracking.link.updated

Fired when a tracking link is updated.

FieldTypeDescription
idstringTracking link ID (prefixed with tlk_)
urlstringThe full tracking link URL
redirectUrlstringThe destination URL (may have changed)
updatedAtstringISO 8601 timestamp of when the tracking link was updated

tracking.link.deleted

Fired when a tracking link is manually deleted.

FieldTypeDescription
idstringTracking link ID (prefixed with tlk_)
urlstringThe full tracking link URL
deletedAtstringISO 8601 timestamp of when the tracking link was deleted

tracking.link.expired

Fired when a tracking link expires automatically based on its configured expiration time.

FieldTypeDescription
idstringTracking link ID (prefixed with tlk_)
urlstringThe full tracking link URL
expiredAtstringISO 8601 timestamp of when the tracking link expired

Signature verification

Every webhook delivery is signed using HMAC-SHA256 with your API key secret. Verify the signature to confirm that the payload was sent by Textingline and has not been tampered with.

Signature header

HeaderDescription
X-Sms-Factory-SignatureHMAC-SHA256 hex digest of the raw request body, signed with your API key secret

Additional headers

HeaderDescription
X-Webhook-IdThe webhook configuration ID
X-Webhook-EventThe event type that triggered this delivery
X-Webhook-TimestampISO 8601 timestamp of when the delivery was sent

Verification example (Node.js)

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, apiKeySecret) {
  const expected = crypto
    .createHmac('sha256', apiKeySecret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// In your webhook handler:
app.post('/webhooks', (req, res) => {
  const signature = req.headers['x-sms-factory-signature'];
  const isValid = verifyWebhookSignature(
    req.rawBody,
    signature,
    API_KEY_SECRET,
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Process the events
  const { events } = req.body;
  for (const event of events) {
    console.log(`Received ${event.eventType}:`, event.data);
  }

  res.status(200).send('OK');
});

Verification example (Python)

import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signature: str, api_key_secret: str) -> bool:
    expected = hmac.new(
        api_key_secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

# In your webhook handler (Flask example):
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Sms-Factory-Signature')
    is_valid = verify_webhook_signature(request.data, signature, API_KEY_SECRET)

    if not is_valid:
        return 'Invalid signature', 401

    payload = request.get_json()
    for event in payload['events']:
        print(f"Received {event['eventType']}: {event['data']}")

    return 'OK', 200

Delivery and retries

Success criteria

A delivery is considered successful when your endpoint returns any 2xx HTTP status code within 30 seconds. Any other response (or a timeout) is treated as a failure and triggers a retry.

Retry schedule

Failed deliveries are retried with increasing backoff intervals:

AttemptDelay after failure
1 (initial)Immediate
25 minutes
310 minutes
41 hour
56 hours
  • Max retries is configurable per webhook (1–5 attempts, default: 5)
  • Each retry uses the same payload and signature as the original delivery
  • If a webhook configuration is disabled or deleted, pending retries are automatically canceled

Source IP addresses

All webhook deliveries originate from the following IP addresses. If your endpoint is behind a firewall, add these CIDRs to your allowlist:

CIDR
104.154.30.130/32
34.45.101.118/32

Rate limiting

Webhook deliveries are rate-limited to 150 deliveries per minute per endpoint URL. If the rate limit is exceeded, deliveries are automatically rescheduled after a short delay. Rate-limited deliveries do not count as failed attempts.

Automatic disabling

If your webhook endpoint is consistently unreachable, the platform will automatically disable it to prevent wasting resources on repeated failures.

How it works

If all delivery attempts (including retries) for an event fail, the platform records when continuous failures began. If 3 days pass without a single successful delivery, the webhook is automatically disabled.

Any single successful delivery resets the failure tracking, so intermittent failures will not trigger auto-disable.

When a webhook is auto-disabled, it is turned off and all pending retries are canceled. You will see a message explaining why it was disabled.

Re-enabling a disabled webhook

To re-enable a webhook that was auto-disabled:

  1. Fix the issue with your endpoint (ensure it is reachable and returns 2xx responses)
  2. Use the Send test button to verify your endpoint is healthy
  3. Toggle the webhook back on in the dashboard

Re-enabling a webhook resets all failure tracking, giving it a clean slate.

Delivery logs

🚧

Delivery logs are coming soon. You will be able to query delivery history, filter by status, event type, and date range to debug and monitor your webhook integrations.

Testing your webhooks

Send a test event

Use the Send test button in the Textingline dashboard to verify your endpoint is reachable and responding correctly. The test sends a sample event to your configured URL and displays the result — including the full error message if delivery fails. This is the fastest way to confirm your endpoint is working before going live.

To send a test event:

  1. Go to Webhooks in the dashboard
  2. Select the webhook you want to test
  3. Click Send test
  4. Review the result — a successful test returns a 2xx response from your endpoint, while a failed test shows the error details (e.g., connection refused, timeout, non-2xx status code)
💡

The test event uses the same signing mechanism as real deliveries, so it also validates that your signature verification logic is working correctly.

Local development with ngrok

During development, your local server isn't reachable from the internet. Use ngrok to create a secure tunnel to your local machine.

  1. Install ngrok
# macOS
brew install ngrok

# Or download from https://ngrok.com/download
  1. Start your local webhook server on a port (e.g., 3000)

  2. Start ngrok to create a public URL pointing to your local server

ngrok http 3000
  1. Copy the forwarding URL from the ngrok output (e.g., https://a1b2c3d4.ngrok-free.app)

  2. Configure your webhook in the dashboard using the ngrok URL as the endpoint (e.g., https://a1b2c3d4.ngrok-free.app/webhooks)

  3. Send a test event using the Send test button, or trigger a real event (e.g., send a message) to see the webhook delivery arrive at your local server

⚠️

ngrok URLs change each time you restart ngrok (on the free plan). Remember to update your webhook endpoint URL in the dashboard when the URL changes.

Best practices

  • Return 200 quickly. Acknowledge the webhook immediately and process the event asynchronously. Long-running processing in the request handler may cause timeouts and unnecessary retries.
  • Verify signatures in production. Always validate the X-Sms-Factory-Signature header to ensure the payload is authentic and has not been modified in transit.
  • Handle duplicates idempotently. In rare cases (e.g., network issues, retries), you may receive the same event more than once. Use the eventId field to deduplicate events on your end.
  • Use HTTPS endpoints. Always use HTTPS URLs for your webhook endpoints to ensure payloads are encrypted in transit.
  • Monitor your endpoint. If your endpoint is consistently failing, deliveries will be retried up to the configured maximum. Check your server logs and ensure your endpoint is healthy.
  • Do not rely on event ordering. Events may arrive out of order, especially when retries are involved. Use the timestamp field inside each event to determine the actual sequence of events rather than the order in which they are received.
  • Monitor for auto-disabled webhooks. Check the disabledReason and disabledAt fields in your webhook responses to detect endpoints that have been automatically disabled due to persistent failures.
  • Account limits. Each account can have up to 10 active webhooks.