POST /api/v1/messages/batch takes a list of complete pushes, up to 100, and processes them
independently.
It is a list of separate updates, not one update fanned out to many recipients. Order updates are individual by nature: fifty orders means fifty tracking numbers, so a shared body would only serve the marketing blast this product is deliberately not for.
curl https://api.beaconbox.eu/api/v1/messages/batch \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: dispatch-run-2026-10-02" \
-d '{
"items": [
{"recipient_email": "a@example.com",
"subject": "Your order is on its way",
"body": "Tracking XY123456789EE.",
"reference": "#A-10294",
"order_status": "shipped",
"kind": "updateable"},
{"recipient_email": "b@example.com",
"subject": "Your order is on its way",
"body": "Tracking XY987654321EE.",
"reference": "#A-10295",
"order_status": "shipped",
"kind": "updateable"}
]
}'from beaconbox import MessagePush, OrderStatus
result = client.messages.push_batch(
[
MessagePush(
recipient_email="a@example.com",
subject="Your order is on its way",
body="Tracking XY123456789EE.",
reference="#A-10294",
order_status=OrderStatus.SHIPPED,
kind="updateable",
),
MessagePush(
recipient_email="b@example.com",
subject="Your order is on its way",
body="Tracking XY987654321EE.",
reference="#A-10295",
order_status=OrderStatus.SHIPPED,
kind="updateable",
),
],
idempotency_key="dispatch-run-2026-10-02",
)
print(result.succeeded, result.failed)
for item in result.failures:
log.error("item %d rejected: %s", item.index, item.error_code)use BeaconBox\Enum\MessageKind;
use BeaconBox\Enum\OrderStatus;
use BeaconBox\Model\MessagePush;
$result = $client->messages->pushBatch(
[
new MessagePush(
recipientEmail: 'a@example.com',
subject: 'Your order is on its way',
body: 'Tracking XY123456789EE.',
reference: '#A-10294',
orderStatus: OrderStatus::Shipped,
kind: MessageKind::Updateable,
),
new MessagePush(
recipientEmail: 'b@example.com',
subject: 'Your order is on its way',
body: 'Tracking XY987654321EE.',
reference: '#A-10295',
orderStatus: OrderStatus::Shipped,
kind: MessageKind::Updateable,
),
],
idempotencyKey: 'dispatch-run-2026-10-02',
);
echo $result->succeeded, '/', $result->failed;
foreach ($result->failures() as $item) {
error_log("item {$item->index} rejected: {$item->errorCode}");
}{
"items": [
{
"index": 0,
"ok": true,
"result": {
"id": "v55favd767qu",
"recipient_email": "a@example.com",
"subject": "Your order is on its way",
"kind": "updateable",
"status": "active",
"created": true,
"scheduled_for": null,
"nudged": true,
"email": {
"sending": true,
"skipped_reason": null
},
"sms": null,
"sms_units": 0,
"whatsapp": null,
"whatsapp_opt_in": null,
"reference": "#A-10294",
"order_status": "shipped",
"created_at": "2026-10-02T20:11:10.674762Z",
"updated_at": "2026-10-02T20:11:10.674762Z"
},
"error_code": null,
"detail": null
},
{
"index": 1,
"ok": false,
"result": null,
"error_code": "common.validation_failed",
"detail": "body: must not be blank"
}
],
"succeeded": 1,
"failed": 1
}One key for the whole call
The batch takes a single Idempotency-Key. Replaying it replays the whole stored response,
including the per-item failures, rather than re-running the items that succeeded.
So a retry after a dropped connection is safe, and the failures you get back the second time are the same failures, not new attempts.
Derive the key from the run rather than from the contents: a dispatch batch keyed
dispatch-run-2026-10-02 collapses correctly when the job retries, which is the case this is for.
When to use it
Use a batch when you genuinely have many updates at once: a nightly dispatch run, a courier file imported in one go.
Do not use it to parallelise unrelated pushes. A batch is one request with one key, so an item that needs retrying cannot be retried on its own. For independent updates arriving at their own pace, individual pushes with individual keys are both simpler and more robust.
Limits
One hundred items, and at least one. A batch over the limit is rejected with
common.validation_failed before anything is processed, so nothing is half-sent.
