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.

Real-Time Delivery: Webhooks deliver incoming messages to your application within milliseconds of receipt. Your webhook endpoint must be publicly accessible and return a success status code (200-299) to acknowledge receipt.

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

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

Best Practices

Endpoint Requirements

Retry Behavior

Automatic Retries: If your endpoint returns an error or times out, the API will retry delivery with exponential backoff. Retries continue for up to 24 hours before moving to a failure queue.
  • 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

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"
  }'
Security Warning: Never disable signature verification in production. Always validate the X-AC-Signature header to ensure webhooks are authentic.

Next Steps