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 as sha256=<signature>.
  • content-type: Must be set to application/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:

ParameterTypeMandatory/OptionalDescriptionHow it Affects Results
type.statusStringMandatoryOverall delivery status: Delivered, Unknown, or Failed.Drives the high-level delivery outcome.
type.subStatusStringMandatorySpecific status: DeliveredToHandset, Unknown, InvalidNumber, or Failed.Drives the analytics metric (see Status Mapping).

recipient object:

ParameterTypeMandatory/OptionalDescriptionHow it Affects Results
recipient.messageStringMandatoryOriginal message content that was sent.Used for reconciliation.
recipient.mobileNumberStringMandatoryRecipient's mobile number (international format).Must match the number from the campaign exactly.
recipient.customerIdStringMandatoryUnique identifier for the customer/recipient.Correlates the report to the recipient.

metadata object:

ParameterTypeMandatory/OptionalDescriptionHow it Affects Results
metadata.appIdIntegerMandatoryApplication identifier.Maps the report to the app.
metadata.brandIdStringMandatoryBrand identifier.Maps the report to the brand.
metadata.campaignIdStringMandatoryCampaign identifier.Maps the report to the campaign.
metadata.campaignTypeIntegerMandatoryType of campaign: 1 (Scheduled) or 2 (Triggered).Maps the report to the campaign type.
metadata.channelIdIntegerMandatoryCommunication channel identifier.Maps the report to the channel.
metadata.engagementIdStringOptionalEngagement tracking identifier.Correlates engagement when present.
metadata.isHashedBooleanMandatoryWhether customer data is hashed.Indicates how identifiers are interpreted.
metadata.scheduledTimeLongMandatoryUnix timestamp of scheduled time.Maps the report to the scheduled send.
metadata.tenantIdIntegerMandatoryTenant identifier.Maps the report to the tenant.
ℹ️

Preserve all original metadata from 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 CodeDescription
400Invalid request — missing fields, missing auth headers, or validation errors.
401Unauthorized — issues with the base URL identifier or with the app-api-key / x-hub-signature headers.
500Internal 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" with subStatus: "DeliveredToHandset" when delivery to the recipient's device is confirmed.
  • Failed delivery: Use status: "Failed" with subStatus: "InvalidNumber" for invalid numbers, or subStatus: "Failed" for general failures.
  • Unknown: Use status: "Unknown" with subStatus: "Unknown" when the status cannot be determined. How each subStatus maps to Optimove analytics:
SubStatusAnalytics Metric
DeliveredToHandsetTracked as delivered
InvalidNumber, FailedTracked as token_bounced
UnknownTracked 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.