Webhook Payloads

Complete reference for all webhook payloads sent by the Approved Contact API. Webhooks deliver real-time notifications for incoming messages and phone number provisioning status updates.

Message Received Webhook

When an SMS/MMS message is received on one of your phone numbers, the API sends a POST request to your configured webhook URI with the following payload:

Headers

X-AC-WebhookEvent: MessageReceived
X-AC-Signature: [HMAC-SHA256 signature]
X-AC-Timestamp: [Unix timestamp]
Content-Type: application/json

Payload Structure

{
  "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": []
}

Field Descriptions

Field Type Description
messageId UUID Unique identifier for this message
from string Sender's phone number in E.164 format
tos array List of recipient phone numbers
message string Message text content
type integer Message type: 0=Inbound, 1=Outbound, 2=System
tenantId UUID Your tenant identifier (nullable)
sentDate ISO 8601 Timestamp when message was sent (UTC)
targetNumber string Your phone number that received the message
phoneNumberId UUID Internal ID of your phone number
threadId string Conversation thread identifier
threadKey string Thread key for grouping messages
dlrId string Delivery receipt ID (nullable)
segmentCount integer Number of SMS segments
tag string Custom tag for categorization (nullable)
attachments array List of media attachments for MMS

MMS with Attachments

When an MMS message includes media, the attachments array contains:

{
  "messageId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "from": "+15551234567",
  "tos": ["+15559876543"],
  "message": "Check out this photo!",
  "type": 0,
  "sentDate": "2025-01-11T10:30:00Z",
  "targetNumber": "+15559876543",
  "phoneNumberId": "550e8400-e29b-41d4-a716-446655440000",
  "threadId": "thread-abc123",
  "threadKey": "key-xyz789",
  "segmentCount": 1,
  "attachments": [
    {
      "name": "photo.jpg",
      "url": "https://storage.approvedcontact.com/attachments/xyz789/photo.jpg",
      "contentType": "image/jpeg",
      "size": 512000
    }
  ]
}

Order Status Webhook

When phone number provisioning or unprovisioning completes, the API sends an order status update:

Headers

X-AC-WebhookEvent: OrderStatus
X-AC-Signature: [HMAC-SHA256 signature]
X-AC-Timestamp: [Unix timestamp]
Content-Type: application/json

Payload Structure

{
  "orderId": 12345,
  "status": "Completed",
  "action": "Provision",
  "tenantId": "00000000-0000-0000-0000-000000000000",
  "provisionClass": "P2P",
  "provisionType": "SMSMMS",
  "autoProvision": true,
  "createDate": "2025-01-11T10:00:00Z",
  "createDateEpoch": 1736592000,
  "lastAttemptedDate": "2025-01-11T10:05:00Z",
  "lastAttemptedDateEpoch": 1736592300,
  "completionDate": "2025-01-11T10:05:30Z",
  "completionDateEpoch": 1736592330,
  "phoneNumbers": [
    {
      "number": "+15551234567",
      "phoneNumberId": "550e8400-e29b-41d4-a716-446655440000",
      "status": "Completed",
      "email": "support@example.com",
      "isP2P": true,
      "brandId": "B12345",
      "campaignId": "C67890",
      "error": null
    }
  ]
}

Order Status Values

Action Types

Phone Number Status

Each phone number in the order can have its own status:

{
  "number": "+15551234567",
  "phoneNumberId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "Failed",
  "email": "support@example.com",
  "error": {
    "errorCode": "INVALID_CAMPAIGN",
    "description": "Campaign ID is not active or does not exist"
  }
}

Webhook Security

All webhook requests include an HMAC-SHA256 signature in the X-AC-Signature header. Always validate this signature before processing webhook data.

Signature Verification Example

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 CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(signature),
        Encoding.UTF8.GetBytes(expectedSignature)
    );
}

[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)

@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)
    );
}

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 });
});

Retry Logic

If your webhook endpoint returns an error or times out, the API automatically retries delivery with exponential backoff.

Retry Behavior

Fallback Endpoint

Configure a secondary webhook URI as a fallback. If the primary fails, the request is automatically sent to the secondary endpoint.

{
  "webhookPrimaryUri": "https://your-app.com/webhooks/messages",
  "webhookSecondaryUri": "https://your-backup.com/webhooks/messages",
  "webhookSecret": "your-webhook-secret-key"
}

Best Practices

Endpoint Requirements

Error Handling

private readonly HashSet _processedMessageIds = new();

[HttpPost("webhooks/messages")]
public async Task HandleWebhook()
{
    try
    {
        var payload = await new StreamReader(Request.Body).ReadToEndAsync();
        var signature = Request.Headers["X-AC-Signature"].ToString();
        
        // Validate signature
        if (!VerifyWebhookSignature(payload, signature, _webhookSecret))
        {
            _logger.LogWarning("Invalid webhook signature");
            return Unauthorized();
        }
        
        var message = JsonSerializer.Deserialize(payload);
        
        // Check for duplicates
        if (_processedMessageIds.Contains(message.MessageId.ToString()))
        {
            _logger.LogInformation("Duplicate message {MessageId}", message.MessageId);
            return Ok(); // Already processed
        }
        
        // Queue for processing
        await _messageQueue.EnqueueAsync(message);
        _processedMessageIds.Add(message.MessageId.ToString());
        
        return Ok();
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error processing webhook");
        // Return 200 to prevent retries for unrecoverable errors
        return Ok();
    }
}

Monitoring

Pro Tip: Use a distributed cache (Redis) to track processed message IDs across multiple server instances to ensure true idempotency.

Next Steps