Delivery Report Endpoint
The Delivery Report Endpoint lets external aggregators report the delivery status of messages sent through the SMS API (formerly OptiText API). It provides real-time feedback on message delivery, which helps track campaign success and handle failed deliveries appropriately.
This endpoint uses the same custom authentication scheme as the Handshake Endpoint.
Delivery Report Endpoint
Endpoint
POST <BASE_URL>/api/v1/aggregator/delivery-report
The <BASE_URL> is provided by the tenant during SMS API configuration. It contains the base URL plus a Base36-encoded identifier that combines the tenant ID and application ID.
Headers
The following headers are mandatory for all requests:
app-api-key: The tenant's API Token (the Optimove API key from Settings > SMS Configuration).x-hub-signature: HMAC-SHA256 signature of the raw request body, formatted assha256=<signature>.content-type: Must be set toapplication/json.
Generating the Signature
The x-hub-signature header contains an HMAC-SHA256 signature of the raw request body, computed with your shared secret, formatted as sha256=<signature>.
const crypto = require("crypto");
const rawBody = req.rawBody; // Raw request body string (before JSON parsing)
const secret = process.env.WEBHOOK_SECRET; // Your shared secret
const signature = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const hubSignature = `sha256=${signature}`;import hashlib
import hmac
import os
raw_body = request.body # Raw request body bytes (before JSON parsing)
secret = os.environ["WEBHOOK_SECRET"].encode("utf-8") # Your shared secret
signature = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
hub_signature = f"sha256={signature}"Request Body Parameters
Send delivery reports in the format below. The metadata is the same metadata Optimove sends to your Campaign Transmission Webhook; it is required so Optimove can map incoming delivery reports to tenant campaigns.
{
"type": {
"status": "Delivered",
"subStatus": "DeliveredToHandset"
},
"recipient": {
"message": "Your campaign message content",
"mobileNumber": "+1234567890",
"customerId": "customer-123"
},
"metadata": {
"appId": 123,
"brandId": "brand-456",
"campaignId": "campaign-789",
"campaignType": 1,
"channelId": 493,
"engagementId": "engagement-abc",
"isHashed": false,
"scheduledTime": 1735737600,
"tenantId": 456
}
}type object:
| Parameter | Type | Mandatory/Optional | Description | How it Affects Results |
|---|---|---|---|---|
type.status | String | Mandatory | Overall delivery status: Delivered, Unknown, or Failed. | Drives the high-level delivery outcome. |
type.subStatus | String | Mandatory | Specific status: DeliveredToHandset, Unknown, InvalidNumber, or Failed. | Drives the analytics metric (see Status Mapping). |
recipient object:
| Parameter | Type | Mandatory/Optional | Description | How it Affects Results |
|---|---|---|---|---|
recipient.message | String | Mandatory | Original message content that was sent. | Used for reconciliation. |
recipient.mobileNumber | String | Mandatory | Recipient's mobile number (international format). | Must match the number from the campaign exactly. |
recipient.customerId | String | Mandatory | Unique identifier for the customer/recipient. | Correlates the report to the recipient. |
metadata object:
| Parameter | Type | Mandatory/Optional | Description | How it Affects Results |
|---|---|---|---|---|
metadata.appId | Integer | Mandatory | Application identifier. | Maps the report to the app. |
metadata.brandId | String | Mandatory | Brand identifier. | Maps the report to the brand. |
metadata.campaignId | String | Mandatory | Campaign identifier. | Maps the report to the campaign. |
metadata.campaignType | Integer | Mandatory | Type of campaign: 1 (Scheduled) or 2 (Triggered). | Maps the report to the campaign type. |
metadata.channelId | Integer | Mandatory | Communication channel identifier. | Maps the report to the channel. |
metadata.engagementId | String | Optional | Engagement tracking identifier. | Correlates engagement when present. |
metadata.isHashed | Boolean | Mandatory | Whether customer data is hashed. | Indicates how identifiers are interpreted. |
metadata.scheduledTime | Long | Mandatory | Unix timestamp of scheduled time. | Maps the report to the scheduled send. |
metadata.tenantId | Integer | Mandatory | Tenant identifier. | Maps the report to the tenant. |
Preserve all original
metadatafrom the campaign request. Optimove relies on it to match delivery reports to the originating campaign.
Response
A successful report returns 200 OK:
{
"message": "Delivery report processed successfully",
"processedAt": "2024-01-15T10:30:00.000Z"
}Error Codes
| Status Code | Description |
|---|---|
400 | Invalid request — missing fields, missing auth headers, or validation errors. |
401 | Unauthorized — issues with the base URL identifier or with the app-api-key / x-hub-signature headers. |
500 | Internal server error during processing. |
400 Bad Request — missing appId:
{ "message": "appId is missing." }400 Bad Request — validation errors:
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Type": ["The Type field is required."],
"Recipient": ["The Recipient field is required."],
"MessageMetadata": ["The MessageMetadata field is required."]
}
}500 Internal Server Error:
{ "message": "Delivery report processing failed." }Complete Example
curl -X POST "<BASE_URL>/api/v1/aggregator/delivery-report" \
-H "Content-Type: application/json" \
-H "app-api-key: <APP_API_KEY>" \
-H "x-hub-signature: sha256=<SIGNATURE>" \
-d '{
"type": {
"status": "Delivered",
"subStatus": "DeliveredToHandset"
},
"recipient": {
"message": "Your campaign message content",
"mobileNumber": "+1234567890",
"customerId": "customer-123"
},
"metadata": {
"appId": 123,
"brandId": "brand-456",
"campaignId": "campaign-789",
"campaignType": 1,
"channelId": 493,
"engagementId": "engagement-abc",
"isHashed": false,
"scheduledTime": 1735737600,
"tenantId": 456
}
}'Status Mapping Guidelines
- Successful delivery: Use
status: "Delivered"withsubStatus: "DeliveredToHandset"when delivery to the recipient's device is confirmed. - Failed delivery: Use
status: "Failed"withsubStatus: "InvalidNumber"for invalid numbers, orsubStatus: "Failed"for general failures. - Unknown: Use
status: "Unknown"withsubStatus: "Unknown"when the status cannot be determined. How eachsubStatusmaps to Optimove analytics:
| SubStatus | Analytics Metric |
|---|---|
DeliveredToHandset | Tracked as delivered |
InvalidNumber, Failed | Tracked as token_bounced |
Unknown | Tracked as dropped |
Retry Logic
Do retry on network timeouts, 500-level server errors, and authentication failures (after refreshing credentials). Do not retry on 400-level validation errors (fix the request format instead) or successful 200 responses. Use exponential backoff for retry attempts.
Rate Limiting
Send delivery reports in real-time as messages are processed. For high volumes, batch multiple reports where appropriate, implement exponential backoff for failed requests, and monitor response times to adjust sending frequency.
Best Practices
- Timing: Send reports as soon as delivery status is known, for both successful and failed deliveries. Don't delay waiting for a final status.
- Data accuracy: Preserve all original metadata, match mobile numbers exactly (including formatting), and use the appropriate status codes for the actual result.
- Monitoring: Track delivery report success rates, watch for patterns in failures, and alert on high failure or error rates.
Security Considerations
- Always use HTTPS to protect data in transit.
- Keep API keys and shared secrets secure; never expose them in client-side code.
- Validate the signature on every request.
- Handle customer data according to privacy regulations.
Support
If you run into issues, contact the Optimove developer support team with your Base URL, redacted request/response examples, the error messages received, a timestamp, and the original message metadata for context.