Submit a batch of invitations
Submits a batch of invitations as one run. Returns HTTP 202 with a run id immediately: the fan-out, pacing, and retries are handled for you, and none of it happens inside your request.
Behavior
- Validation covers the whole batch and names every row: each unusable
row is reported at once, with your row index and field
(
/recipients/7/to), so the number of entries is exactly the number of rows to fix.on_invaliddecides whether bad rows refuse the whole submit (HTTP 422code1005, nothing created) or are dropped while the rest proceed (HTTP 202 with askippedarray). validate_only: trueruns every screen and creates nothing: no run, no recipients, and theIdempotency-Keystays unclaimed. Success is HTTP 200 with a report instead of HTTP 202 with a run.- Consent is checked at submit, so you learn which numbers lack an
invitationopt-in immediately instead of reading delivery failures for hours. It is also re-decided before every attempt: a customer who opts out after you submit is suppressed then. - The run is admitted against the delivery backlog. HTTP 429
backlog_horizon_exceeded(withRetry-After, legitimately hours) means your queue plus this run would exceed the delivery horizon; retry later. HTTP 422code1004 means this run alone can never fit; the message names a run size that will, rounded down to a round number, and the remedy is to split it. In one uncommon configuration we name no size and ask you to send fewer per run. Both create nothing, and the sameIdempotency-Keysucceeds on retry. - A scheduled (
not_before) or smoothed (spread_over_seconds) run is admitted as it materializes instead of at submit: it waits rather than failing when your queue is full, itsetasays so, and it can be cancelled like any other. - Per-row problems arrive inside
error.details(orskipped), never as the response status: a batch’s answer is about the list, and no single status is truthful when row 3 has a bad number and row 40 has no consent.
Errors (run-level)
- HTTP 400:
body_invalid,idempotency_key_required - HTTP 403:
invitation_card_not_granted: your brand is no longer approved for the card shape the template renders - HTTP 413:
too_large: the body exceeds 8 MiB; split the batch - HTTP 422:
template_id_required,unknown_template,recipients_required,too_many_recipients,batch_exceeds_horizon;field_removedif the request still carries the retiredbrand_logo_att_idfield or a recipient still carriesparameters - HTTP 429:
backlog_horizon_exceeded, withRetry-After
Body ParametersJSON
One of YOUR invitation template ids (invt_…). ONE template per run: a run is a campaign, and two templates are two batches. The card’s name and logo come from the template, resolved ONCE at submit and frozen into the run, so editing the template mid-run cannot change what queued recipients receive.
The run’s default locale; a recipient may override it. Absent renders the template in its own default.
SCHEDULE THE RUN: do not start before this instant (RFC 3339, with an offset). Absent: the default; starts as soon as the run is accepted.
A value in the PAST is accepted and starts the run immediately; it is not an
error, because a retry of a submit whose start time has since elapsed must
give you the run you asked for rather than a refusal you cannot fix. At most
7 days ahead (422 not_before_too_far): every recipient’s opt-in is read at
SUBMIT, so a run scheduled further out would send against consent nobody has
re-checked.
A scheduled run sits at status: "queued" until its time comes, costs you
nothing meanwhile, and can be cancelled like any other. If our workers are
down across the boundary the run starts LATE, never skipped: the start is a
stored instant, not an event anyone has to be awake for.
SMOOTH THE RUN: spread it evenly over this many seconds, starting at
not_before (or at acceptance). 0: the default; hands every recipient to
the delivery machinery as fast as it will take them, which is what every run
did before this field existed. Capped at 7 days
(422 spread_over_seconds_invalid).
It can only ever make a run SLOWER. Your delivery rate is enforced independently and is unaffected by this field: a window longer than your rate needs gets you the window, a shorter one gets you your rate. Use it when the thing that cannot absorb a burst is on your side: a support queue, a fulfilment pipeline, a landing page.
DRY RUN. Every screen runs: the template grant, the parameter schemas, the
phone numbers, the duplicates, the consent records, and NOTHING is created:
no run, no recipients, and the Idempotency-Key is not claimed, so the real
submit can use the same one. The answer is the answer the identical live
request would give, except that success is 200 with a report instead of
202 with a run: so a 422 here is exactly the 422 you would have got.
Returns
Submit a batch of invitations
curl https://messages.api.linqapp.com/v1/invitation_batches \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $LINQ_AMB_API_KEY" \
-d '{
"recipients": [
{
"reference_id": "ride-88214",
"to": "+15555550142"
},
{
"reference_id": "ride-88215",
"to": "+15555550188"
}
],
"template_id": "invt_a4f2c718d0"
}'{
"id": "invb_0d5c19",
"status": "queued",
"template_id": "invt_a4f2c718d0",
"recipient_count": 2,
"accepted_count": 2,
"created_at": "2026-08-06T14:00:00Z"
}Returns Examples
{
"id": "invb_0d5c19",
"status": "queued",
"template_id": "invt_a4f2c718d0",
"recipient_count": 2,
"accepted_count": 2,
"created_at": "2026-08-06T14:00:00Z"
}