Skip to content
Linq

Invitation Batches

Invitations in bulk, as an asynchronous batch with its own status and per-row outcome.

Submit a batch of invitations
POST/v1/invitation_batches
Get a batch run's progress
GET/v1/invitation_batches/{batch}
List a batch run's recipients
GET/v1/invitation_batches/{batch}/recipients
Cancel a batch run
POST/v1/invitation_batches/{batch}/cancel
ModelsExpand Collapse
InvitationBatchCreateResponse object { accepted_count, recipient_count, validate_only, skipped }

validate_only’s report: what a live submit of the same body would have accepted, with no run created.

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).

InvitationBatchRetrieveResponse object { id, accepted_count, counts, 8 more }

A run’s progress. Poll this, not the recipients.

id: string

Run id (invb_…).

accepted_count: number

How many recipients the run will attempt: recipient_count minus the rows the submit screen skipped. The same number the submit response returned, and frozen with it.

formatint32
counts: object { cancelled, failed, pending, 2 more }

How many of a run’s recipients stand in each state. EVERY key is always present, zero included: a client reads five numbers instead of branching on which keys exist, and a state that has not happened yet is 0 rather than missing.

cancelled: number

Stopped by a cancel before it was attempted.

formatint32
failed: number

Accepted at submit, then permanently refused before sending; consent withdrawn in between, a superseded or closed chat, a revoked template grant. The row’s reason says which.

formatint32
pending: number

Not yet attempted. This is the number the ETA projects.

formatint32
sent: number

Handed to the invitation pipeline: an invitation_id exists on that row. Delivery to Apple is reported by that invitation’s own status and webhooks, not here.

formatint32
skipped: number

Dropped by the SUBMIT screen under on_invalid: skip_invalid, with the reason the submit response gave. Frozen at creation; nothing later moves a row into or out of it.

formatint32
created_at: string
formatdate-time
recipient_count: number

How many recipients the run was SUBMITTED with, frozen at creation; dropped rows included. Always the length of the array you sent, and always counts’ total.

formatint32
status: "queued" or "running" or "completed" or "cancelled"

queued (created, nothing attempted yet) → running (at least one recipient attempted, some still pending) → completed (no recipient is pending; every one is sent, failed, cancelled or skipped), or cancelled when you stopped it. completed does NOT mean every invitation was delivered: it means the run has nothing left to attempt, and per-row outcomes are in counts and the recipient results.

One of the following:
"queued"
"running"
"completed"
"cancelled"
template_id: string
updated_at: string

Last change to the run row itself (its status). NOT bumped by a per-recipient transition, so it is a run-lifecycle timestamp and not a progress heartbeat; read counts for progress.

formatdate-time
eta: optional object { at, queued_ahead, rate_per_second, 2 more }

The completion estimate. Absent when none can honestly be quoted; see the model.

at: string

The same estimate as a timestamp on OUR clock; seconds from when this response was composed, so a client does not have to correct for its own skew.

formatdate-time
queued_ahead: number

How many other invitations of yours are queued ahead of this run’s remaining rows in the same brand queue. A run does not clear in isolation; another campaign submitted first is counted here, and so are this run’s own recipients that have already been handed to the delivery queue.

formatint64
rate_per_second: number

Recipients per second the estimate was taken at: min(your brand's effective rate, our overall rate / active brands).

formatdouble
remaining: number

How many of this run’s recipients the estimate covers; counts.pending.

formatint32
seconds: number

Estimated seconds until the last remaining recipient is released. 0 when nothing is pending.

formatint64
not_before: optional string

The run’s scheduled start, if you gave it one. A queued run with a not_before in the future is waiting for its own schedule, not for us.

formatdate-time
spread_over_seconds: optional number

The run’s smoothing window, if you gave it one.

formatint32
InvitationBatchListRecipientsResponse object { data, next_cursor }

One page of a run’s recipients, in your submitted order.

data: array of object { idx, reference_id, state, 3 more }
idx: number

This row’s position in the array you submitted, zero-based: the same index each refusal names (/recipients/7/to), and this run’s identity for that row. Never renumbered: skipped rows stay in place, so the index cannot develop holes.

formatint32
reference_id: string

Your correlation handle for this recipient, echoed back.

state: "pending" or "sent" or "failed" or 2 more
One of the following:
"pending"
"sent"
"failed"
"cancelled"
"skipped"
to: string

The recipient string AS YOU SENT IT, not the canonical form, so a result matches a row of your own list without you re-normalizing anything.

invitation_id: optional string

The invitation this recipient became, once it has been attempted; read GET /v1/invitations/{inv} for its delivery state, the customer’s response and its webhooks. Absent while the row is pending, and for any row that never became one (skipped, cancelled, and a failed row refused before an invitation existed).

reason: optional object { code, message }

Present exactly on the rows that will not send (skipped, failed), absent on every other.

code: string

to_required | to_invalid | duplicate_recipient | consent_required | a reference_id code (too_long, forbidden_character), or, for a row that failed after acceptance, the send-time refusal: consent_required, chat_superseded, chat_closed, or invitation_template_not_authorized (only here: your brand’s approval was withdrawn while the run was in flight; at submit the same condition is the run-level 403 invitation_card_not_granted).

message: string
next_cursor: string

Pass back as ?cursor= for the next page. EMPTY when this page exhausted the run, that, not an empty data, is how paging ends.

InvitationBatchCancelResponse object { id, accepted_count, cancellation, 6 more }

A stopped run: where it now stands, and exactly what the cancel caught.

id: string

Run id (invb_…).

accepted_count: number

How many recipients the run would have attempted: recipient_count minus the rows the submit screen skipped.

formatint32
cancellation: object { already_sent, in_flight, queued, unreleased }

What a cancel actually stopped, split by how far each recipient had got.

The four numbers exist because two of them are NOT cancelled and you have to know which. Cancel is EXACT for everything that has not been attempted and cannot recall anything that has: a request already on Apple’s wire is on Apple’s wire.

unreleased + queued is what this call stopped. in_flight + already_sent is what went out anyway. The four PARTITION the run; every recipient that became an invitation is in exactly one of them, and so is every recipient we stopped before it became one, so nothing falls between them. All four are recomputed from the run’s rows on every call, so cancelling twice reports the same split rather than a set of zeros.

already_sent: number

The send was already ATTEMPTED before your cancel landed. NOT stopped, and not recallable; by us or by anyone. Whether Apple accepted it is that invitation’s own delivery state; this counts the attempt, so a send that was attempted and refused is here rather than nowhere.

formatint32
in_flight: number

An attempt was on the wire when your cancel landed. NOT stopped: it either reached Apple or did not, and we report rather than guess.

formatint32
queued: number

Released to the delivery queue but never attempted. STOPPED; this is the set that makes a cancel three seconds into a 50,000-recipient run mean something.

formatint32
unreleased: number

Never handed to the delivery queue at all; recipients we had not reached yet, plus invitations the pacer had not released. STOPPED.

formatint32
counts: object { cancelled, failed, pending, 2 more }

The run’s recipients by state, the same object the progress read returns. Rows this cancel stopped before they were attempted are now cancelled; a row that had already become an invitation stays sent: the invitation exists, and whether it went out is that invitation’s own delivery state.

cancelled: number

Stopped by a cancel before it was attempted.

formatint32
failed: number

Accepted at submit, then permanently refused before sending; consent withdrawn in between, a superseded or closed chat, a revoked template grant. The row’s reason says which.

formatint32
pending: number

Not yet attempted. This is the number the ETA projects.

formatint32
sent: number

Handed to the invitation pipeline: an invitation_id exists on that row. Delivery to Apple is reported by that invitation’s own status and webhooks, not here.

formatint32
skipped: number

Dropped by the SUBMIT screen under on_invalid: skip_invalid, with the reason the submit response gave. Frozen at creation; nothing later moves a row into or out of it.

formatint32
created_at: string
formatdate-time
recipient_count: number

How many recipients the run was SUBMITTED with, frozen at creation. Always counts’ total.

formatint32
status: "cancelled" or "completed"

cancelled when this call (or an earlier one) stopped a live run. completed when the run’s own fan-out had ALREADY finished; every recipient was materialized before you cancelled; in which case the run keeps that status and cancellation reports what the cancel still caught in the delivery queue. It is never any other value: those two are the only states a run can be in after this call.

One of the following:
"cancelled"
"completed"
template_id: string
updated_at: string

When the run reached cancelled. Unchanged by a repeated cancel.

formatdate-time