Receiving Messages
Learn how to receive and process incoming SMS/MMS messages using webhooks. The Approved Contact API sends real-time notifications to your application when messages arrive, allowing you to build interactive, two-way messaging experiences.
Webhook Configuration
Webhooks are HTTP POST requests sent to your application when specific events occur. You can configure webhooks at the tenant or phone number level. Each webhook includes event data and security headers for validation.
Configuring Webhooks
Set up your webhook URL through the tenant configuration:
curl -X PUT https://api.approvedcontact.com/api/v1/tenants/YOUR_TENANT_ID \
-u "your-email@example.com:your-password" \
-H "Content-Type: application/json" \
-d '{
"webhookPrimaryUri": "https://your-app.com/webhooks/messages",
"webhookSecondaryUri": "https://your-backup.com/webhooks/messages",
"webhookSecret": "your-webhook-secret-key",
"webhookEventType": "All"
}'
Webhook Configuration Options
| Option | Description |
|---|---|
webhookPrimaryUri |
Primary endpoint for webhook deliveries (required) |
webhookSecondaryUri |
Fallback endpoint if primary fails (optional) |
webhookSecret |
Secret key for HMAC-SHA256 signature generation |
webhookEventType |
Event filter: All, MessageReceived, or OrderStatus |
webhookBasicAuthUsername |
Optional Basic Auth username |
webhookBasicAuthPassword |
Optional Basic Auth password |
Webhook Payload
When an incoming message is received, the API sends a POST request to your webhook URL with the message details in the request body.
Incoming Message Payload
{
"messageId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"from": "+15551234567",
"tos": ["+15559876543"],
"message": "Hello! I have a question about my order.",
"type": 0,
"tenantId": "00000000-0000-0000-0000-000000000000",
"sentDate": "2025-01-11T10:30:00Z",
"targetNumber": "+15559876543",
"phoneNumberId": "550e8400-e29b-41d4-a716-446655440000",
"threadId": "thread-abc123",
"threadKey": "key-xyz789",
"dlrId": "DLR-12345",
"segmentCount": 1,
"tag": null,
"attachments": []
}
Payload Fields
| Field | Type | Description |
|---|---|---|
messageId |
GUID | Unique message identifier |
from |
string | Sender phone number (E.164 format) |
tos |
array | Recipient phone numbers |
message |
string | Message text content |
type |
int | 0=Inbound, 1=Outbound, 2=System |
sentDate |
datetime | When the message was sent (UTC) |
targetNumber |
string | Your phone number that received the message |
threadId |
string | Conversation thread identifier |
attachments |
array | MMS media attachments (if any) |
MMS Attachments
When an MMS message is received with media, the attachments array contains:
{
"attachments": [
{
"name": "image.jpg",
"url": "https://storage.approvedcontact.com/attachments/abc123/image.jpg",
"contentType": "image/jpeg",
"size": 245760
}
]
}
Security & Validation
All webhook requests include security headers to verify authenticity. Always validate these headers before processing webhook data to prevent spoofing and replay attacks.
Webhook Headers
- X-AC-WebhookEvent: Event type (e.g., "MessageReceived")
- X-AC-Signature: HMAC-SHA256 signature of the request body
- X-AC-Timestamp: Unix timestamp when the webhook was sent
- Authorization: Basic Auth (if configured)
Signature Verification
Verify the webhook signature to ensure the request came from Approved Contact:
using System.Security.Cryptography;
using System.Text;
public bool VerifyWebhookSignature(string payload, string signature, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedSignature = Convert.ToBase64String(hash);
return signature == expectedSignature;
}
// Usage in ASP.NET Core
[HttpPost("webhooks/messages")]
public async Task HandleWebhook()
{
var payload = await new StreamReader(Request.Body).ReadToEndAsync();
var signature = Request.Headers["X-AC-Signature"].ToString();
var secret = _configuration["Webhook:Secret"];
if (!VerifyWebhookSignature(payload, signature, secret))
{
return Unauthorized("Invalid signature");
}
var message = JsonSerializer.Deserialize(payload);
// Process message...
return Ok();
}
import hmac
import hashlib
import base64
def verify_webhook_signature(payload, signature, secret):
expected_signature = base64.b64encode(
hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).digest()
).decode()
return hmac.compare_digest(signature, expected_signature)
# Usage in Flask
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/messages', methods=['POST'])
def handle_webhook():
payload = request.get_data(as_text=True)
signature = request.headers.get('X-AC-Signature')
secret = os.environ['WEBHOOK_SECRET']
if not verify_webhook_signature(payload, signature, secret):
return jsonify({'error': 'Invalid signature'}), 401
message = request.get_json()
# Process message...
return jsonify({'success': True}), 200
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage in Express
const express = require('express');
const app = express();
app.post('/webhooks/messages', express.raw({type: 'application/json'}), (req, res) => {
const payload = req.body.toString();
const signature = req.headers['x-ac-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (!verifyWebhookSignature(payload, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const message = JSON.parse(payload);
// Process message...
res.status(200).json({ success: true });
});
Handling Messages
Your webhook endpoint should process incoming messages quickly and return a 200 status code. For long-running operations, queue the message for async processing.
Example: Auto-Reply Handler
[HttpPost("webhooks/messages")]
public async Task HandleIncomingMessage()
{
var payload = await new StreamReader(Request.Body).ReadToEndAsync();
// Verify signature
if (!VerifyWebhookSignature(payload, Request.Headers["X-AC-Signature"], _webhookSecret))
{
return Unauthorized();
}
var message = JsonSerializer.Deserialize(payload);
// Check if it's an inbound message
if (message.Type == 0) // Inbound
{
// Queue for processing
await _messageQueue.EnqueueAsync(new ProcessIncomingMessageCommand
{
MessageId = message.MessageId,
From = message.From,
Message = message.Message,
TargetNumber = message.TargetNumber
});
// Optional: Send immediate auto-reply
if (message.Message.ToLower().Contains("help"))
{
await SendAutoReply(message.From, message.TargetNumber);
}
}
return Ok();
}
private async Task SendAutoReply(string to, string from)
{
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", _apiCredentials);
var response = new
{
from = from,
to = new[] { to },
body = "Thank you for contacting us! A representative will respond shortly."
};
await client.PostAsJsonAsync(
"https://api.approvedcontact.com/api/v1/messages",
response
);
}
Delivery Receipts
Webhooks also deliver status updates for outbound messages, allowing you to track delivery success, failures, and delivery timing.
Delivery Receipt Payload
{
"messageId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "delivered",
"statusReason": null,
"deliveredAt": "2025-01-11T10:30:15Z",
"errorCode": null,
"errorMessage": null
}
Message Statuses
- queued: Message accepted and queued for delivery
- sending: Message being processed by carrier
- sent: Message successfully sent to carrier
- delivered: Message delivered to recipient's device
- failed: Message delivery failed permanently
- undelivered: Carrier couldn't deliver message
Best Practices
Endpoint Requirements
- Return Quickly: Respond within 5 seconds to avoid timeouts
- Use HTTPS: Webhook URLs must use HTTPS
- Return 200-299: Any other status code triggers a retry
- Be Idempotent: Handle duplicate deliveries gracefully
Retry Behavior
- 4xx errors: Retry with exponential backoff (max 30 min delay)
- 5xx errors: Retry with exponential backoff (max 1 min delay)
- Respect Retry-After header if your endpoint returns it
Processing Tips
- Store the messageId to detect and skip duplicate deliveries
- Use a message queue for async processing of complex logic
- Log all webhook deliveries for debugging and auditing
- Implement circuit breakers for downstream service calls
- Monitor webhook success rates and latency
Testing Webhooks
Use tools like ngrok to expose your local development environment:
# Start ngrok tunnel
ngrok http 5000
# Update webhook URL
curl -X PUT https://api.approvedcontact.com/api/v1/tenants/YOUR_TENANT_ID \
-u "your-email@example.com:your-password" \
-H "Content-Type: application/json" \
-d '{
"webhookPrimaryUri": "https://your-random-id.ngrok.io/webhooks/messages",
"webhookSecret": "test-secret"
}'