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
Pending- Order is queued for processingInProgress- Actively being processedCompleted- Successfully completedPartiallyComplete- Some numbers succeeded, some failedFailed- Order failed completely
Action Types
Provision- Phone number being activatedUnprovision- Phone number being deactivated
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
- 4xx errors: Exponential backoff up to 30 minutes between retries
- 5xx errors: Exponential backoff up to 1 minute between retries
- 429 (Rate Limit): Respects Retry-After header if provided
- Timeout: 30 seconds default timeout per request
- Max Duration: Retries continue for up to 24 hours
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
- Return Quickly: Respond within 5 seconds. Queue long-running tasks.
- Use HTTPS: Webhook URLs must use HTTPS
- Return 200-299: Any other status code triggers a retry
- Be Idempotent: Handle duplicate deliveries gracefully using messageId
- Validate Signatures: Always verify X-AC-Signature header
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
- Log all webhook deliveries with timestamps
- Track success rates and latencies
- Set up alerts for high failure rates (>5%)
- Monitor queue depths if using async processing