Send many messages
https://api.norreach.app/v1/messages/batchUp to 100 messages in one request — each item exactly one POST /messages: its own saved contact, its own send checks, its own approval (approve applies to every item, with the same rules), its own result. Fields shared by every item can go in defaults (an item's own fields win). The answer lists every item IN INPUT ORDER: status 202 with its result (poll each result.id with GET /messages/{id}), or the item's own problem (404 not_found, 422 send_blocked / validation_failed, 429 send_cap_reached …) — one bad item never stops the others. The hourly cap (500 API messages per workspace) counts every accepted item: items are taken in order, and once the hour's allowance is used up the remaining items answer send_cap_reached individually. One Idempotency-Key covers the whole batch (a replay answers the same per-item results); ?dry_run=true checks every item and changes nothing.
curl -X POST "https://api.norreach.app/v1/messages/batch" \
-H "Authorization: Bearer $NORREACH_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"defaults": {
"channel": "email",
"subject": "Nurses for the summer",
"html": "<p>Hi {{first_name}},</p><p>We have nurses free in June.</p>"
},
"items": [
{
"contact_id": "7d1c3b2a-5e4f-4a1b-9c8d-0e1f2a3b4c5d"
},
{
"contact_id": "8e2d4c3b-6f5a-4b2c-8d9e-1f2a3b4c5d6e",
"channel": "sms",
"text": "Hi! Nurses free in June — shall I call you?"
}
]
}'{
"object": "batch",
"data": [
{
"index": 0,
"status": 202,
"result": {
"object": "send_request",
"id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
"kind": "message",
"channel": "email",
"contact_id": "7d1c3b2a-5e4f-4a1b-9c8d-0e1f2a3b4c5d",
"status": "awaiting_approval",
"approval_key": "rule_send:5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
"note": "It waits in Approvals until someone says yes.",
"scheduled_for": null,
"sender_id": null
}
},
{
"index": 1,
"status": 422,
"problem": {
"type": "https://api.norreach.app/problems/send_blocked",
"title": "This can't be sent to this person.",
"status": 422,
"code": "send_blocked",
"detail": "The contact is on your suppression list.",
"reason": "suppressed",
"docs": "https://docs.norreach.app/errors#send_blocked"
}
}
],
"counts": {
"accepted": 1,
"failed": 1
},
"dry_run": false
}Authorizations
Bearer authentication: send Authorization: Bearer <key>, with an API key or an OAuth access token. The key needs the scope messages:send. API keys and scopes
Query parameters
Run every check and return what would happen, changing nothing.
Headers
Any unique string (≤ 255). The same key with the same request returns the first answer for 24 hours.
Body
application/json
The messages, at most 100.
Show child attributesHide child attributes
An id (UUID).
Required here or in defaults.
emailsmswhatsappThe message (required unless template or html).
Email: the subject line (required for email).
Email only: the email as HTML. It is made safe on our side (formatting, links, images, tables and inline styles are kept; scripts, forms, frames and event handlers are removed; links http(s)/mailto only). text is its plain-text alternative — derived from the HTML when you leave it out. Your signature and opt-out line are added exactly as for messages sent from the app.
WhatsApp only: an approved template (required outside the 24-hour window).
Show child attributesHide child attributes
An approved WhatsApp template of this workspace (GET /templates).
Its language code, e.g. sv or en_US (default: the template's own).
Values for the template's variables, in order.
Send under this campaign's settings (approval mode, sending window).
Send from this sender: an id from GET /senders — one of the workspace's own connected senders for the channel (an email inbox or domain sender address; for SMS a number that can text — voice-only numbers can't; for WhatsApp the workspace's business number). Default: the workspace's usual sender for the channel.
Schedule it: an RFC 3339 time with a zone (e.g. 2026-11-20T09:00:00+01:00), in the future and at most 30 days ahead. Nothing goes out before it; the send checks still apply at that time, so quiet hours or your sending window can move it to the next allowed moment. It can be approved before then, and cancelled until it goes out (POST /messages/{id}/cancel).
Shared fields: every item that leaves a field out takes it from here.
Show child attributesHide child attributes
emailsmswhatsappThe message (required unless template or html).
Email: the subject line (required for email).
Email only: the email as HTML. It is made safe on our side (formatting, links, images, tables and inline styles are kept; scripts, forms, frames and event handlers are removed; links http(s)/mailto only). text is its plain-text alternative — derived from the HTML when you leave it out. Your signature and opt-out line are added exactly as for messages sent from the app.
Show child attributesHide child attributes
An approved WhatsApp template of this workspace (GET /templates).
Its language code, e.g. sv or en_US (default: the template's own).
Values for the template's variables, in order.
Send under this campaign's settings (approval mode, sending window).
Send from this sender: an id from GET /senders — one of the workspace's own connected senders for the channel (an email inbox or domain sender address; for SMS a number that can text — voice-only numbers can't; for WhatsApp the workspace's business number). Default: the workspace's usual sender for the channel.
Schedule it: an RFC 3339 time with a zone (e.g. 2026-11-20T09:00:00+01:00), in the future and at most 30 days ahead. Nothing goes out before it; the send checks still apply at that time, so quiet hours or your sending window can move it to the next allowed moment. It can be approved before then, and cancelled until it goes out (POST /messages/{id}/cancel).
Approve every accepted item now as the key's creator (needs approvals:decide).
Response
200application/json— Send many messages
One entry per item, in input order.
Show child attributesHide child attributes
The item's position in items (0-based).
What this item alone would have answered: 202 (200 on a dry run), else its problem's status.
The send request, when it was accepted.
Show child attributesHide child attributes
The request's id — poll GET /messages/{id} or GET /calls/{id} with it.
messagecallAn id (UUID).
awaiting_approval = it waits in Approvals; queued = approved, it goes out within a minute through the send checks; scheduled = approved, it goes out at scheduled_for through the send checks.
awaiting_approvalqueuedscheduleddry_runThe Approvals key (approve it with POST /approvals/{key}/approve).
Dry run: what would happen.
Show child attributesHide child attributes
awaiting_approvalqueuedscheduledMessages: not before this time (UTC) — send_at, normalized. Quiet hours and the sending window can move it later.
Messages: the sender asked for (null = the workspace's default for the channel).
Why this item wasn't accepted — the same problem document a single POST /messages answers.
Show child attributesHide child attributes
Show child attributesHide child attributes
Errors
Errors are application/problem+json with a stable code.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | The request couldn't be read. |
| 400 | unknown_field | The request has a field this operation doesn't know. |
| 400 | idempotency_key_required | This operation needs an Idempotency-Key header. |
| 401 | unauthorized | A valid API key is required. |
| 403 | insufficient_scope | This API key doesn't have the scope this operation needs. |
| 403 | feature_not_enabled | The API isn't switched on for this workspace. |
| 403 | role_forbidden | The key's creator's role can't do this. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. |
| 409 | outreach_paused | Outreach is paused for this workspace. |
| 413 | too_large | The request body is too large. |
| 422 | validation_failed | The request has invalid fields. |
| 422 | idempotency_mismatch | This Idempotency-Key was used with a different request. |
| 429 | rate_limited | Too many requests. Slow down and try again. |
| 500 | server_error | Something went wrong on our side. |
| 503 | unavailable | The API isn't available right now. Try again shortly. |