Skip to content
Linq

Submit a batch of invitations

POST/v1/invitation_batches

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_invalid decides whether bad rows refuse the whole submit (HTTP 422 code 1005, nothing created) or are dropped while the rest proceed (HTTP 202 with a skipped array).
  • validate_only: true runs every screen and creates nothing: no run, no recipients, and the Idempotency-Key stays 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 invitation opt-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 (with Retry-After, legitimately hours) means your queue plus this run would exceed the delivery horizon; retry later. HTTP 422 code 1004 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 same Idempotency-Key succeeds 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, its eta says so, and it can be cancelled like any other.
  • Per-row problems arrive inside error.details (or skipped), 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_removed if the request still carries the retired brand_logo_att_id field or a recipient still carries parameters
  • HTTP 429: backlog_horizon_exceeded, with Retry-After
Header ParametersExpand Collapse
"Idempotency-Key": string
Body ParametersJSONExpand Collapse
recipients: array of object { reference_id, to, locale }

The recipient set, in your order. 1–10,000 rows per submit: the ceiling is what one request can carry and still be validated, stored and reported on as a unit, and a larger campaign is more than one batch (the run id makes each one trackable). An empty array is 422 recipients_required; over the ceiling is 422 too_many_recipients, both naming /recipients.

reference_id: string

REQUIRED per recipient: an order number, case id or similar, echoed on that invitation’s delivery webhooks. A batch does not relax it: a run of 10,000 sends with one shared correlation handle is not reconcilable.

to: string

The customer’s phone number, bare E.164 (partner formatting tolerated). Normalized to a canonical tel:+E164 handle at submit: an unparseable one is a to_invalid naming that row, and two rows that normalize alike are a duplicate_recipient. Your own string is stored as sent and echoed back in the run’s per-recipient results.

locale: optional string

Overrides the run’s locale for this recipient.

template_id: string

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.

locale: optional string

The run’s default locale; a recipient may override it. Absent renders the template in its own default.

not_before: optional string

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.

formatdate-time
on_invalid: optional "reject_batch" or "skip_invalid"

What to do when some rows cannot be sent to. Defaults to reject_batch: partial acceptance is something you ask for, never something that happens to you, because “half my campaign went out” is not a state to reach by omission.

One of the following:
"reject_batch"
"skip_invalid"
spread_over_seconds: optional number

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.

formatint32
minimum0
maximum604800
validate_only: optional boolean

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.

ReturnsExpand Collapse
accepted_count: number

How many rows a live submit would have accepted.

formatint32
recipient_count: number

The length of the recipient array you submitted.

formatint32
validate_only: true

Always true. Echoed rather than implied, so a response read out of a log cannot be mistaken for an accepted campaign.

skipped: optional array of object { code, message, param }

The rows a live skip_invalid submit would have dropped. Absent when nothing would be.

code: string

to_required | to_invalid | duplicate_recipient | consent_required | or a reference/locale code (required, too_long, forbidden_character).

message: string
param: string

JSON pointer at the offending row AND field of your own request (/recipients/7/to).

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"
}