Delivery reports
Receive message status by webhook and handle acknowledgements, retries and idempotency.
Set deliveryReportUrl when creating a batch to receive status updates for each recipient and status.
Webhook behavior
iP.1 sends each report with POST and expects 200 OK. The published retry policy is:
- any response other than
200 OKtriggers another attempt - retries are made at ten-minute intervals
- no more than ten attempts are made
- the report is removed after the final failed attempt
Respond quickly with 200 OK after validating the payload and placing it in an internal queue. Perform slow or failure-prone processing asynchronously.
Example payload
{
"id": "message_abc123",
"batchId": "batch_abc123",
"owner": "ip1-XXXXX",
"recipient": "46700000000",
"code": 201,
"duration": 0,
"created": "2026-10-05T08:03:21Z",
"segments": 1,
"price": 0.0416,
"currency": "SEK",
"reference": "order-123"
}
The older source table contained two mislabeled rows (batchId for owner and recipient for sender). The payload example and fields below use the actual JSON keys.
Fields
| Field | Type | Description |
|---|---|---|
id |
string |
Message ID. |
batchId |
string |
Parent batch ID. |
owner |
string |
Owning SMS account, when included. |
recipient |
string |
Recipient’s MSISDN. |
code |
integer |
New status code. |
duration |
integer |
Indicates whether the status is final according to the published model. |
created |
string |
Status timestamp in ISO 8601. |
segments |
integer |
Number of SMS segments. |
price |
number |
Total message cost. |
currency |
string |
Price currency. |
reference |
string | null |
Your own reference from the batch. |
Secure, idempotent receiver
- Allow HTTPS only.
- Limit the payload size.
- Validate Content-Type and the JSON schema.
- Use the combination of
id,codeandcreatedas an idempotency key. - Allow for the same report to arrive more than once.
- Compare event timestamps before overwriting a local status.
- Do not log complete phone numbers or message text unless required.
export async function POST({ request }) {
const report = await request.json();
if (!report.id || !Number.isInteger(report.code)) {
return new Response('Invalid report', { status: 400 });
}
await queue.add('ip1-delivery-report', report);
return new Response(null, { status: 200 });
}
The example demonstrates the principle; add authentication or network controls according to the options agreed for the integration.
See Status codes for the meaning of code and Messages for retrieving the complete history.
