POST /v1/messages sends one message to one recipient from one of your lines. This page describes the request, what the 202 response means, the order in which iSnap checks a send, how to follow a message to its final status, what each failure means, and how to cancel a message that has not left. Read it before you write the code that sends. The sending limits of a line, and the reasons iSnap holds a message, have their own page: Sending limits.
The send request
POST /v1/messages takes a JSON body. Send Content-Type: application/json with every body.
A request carries a non-empty
body, at least one attachment, or both. With neither, it returns 400 validation_failed.
to takes a phone number only. An email address, an Apple ID, or a value that is not a valid phone number returns 400 bad_request, even when POST /v1/lookup reports that the address can receive iMessage. One request sends to one recipient.
The API cannot choose the service: channel takes only auto, and no setting of a line or of a message changes how the line’s device chooses. See Services and fallback.
A body field that POST /v1/messages does not define is ignored, without an error. A misspelled field is therefore dropped silently: check the field names against this table.
Attachments in a send
Each id inattachments is an attachment that your account created with POST /v1/attachments. An unknown id, an id of another account, and the id of a file received on an inbound message return 404 not_found.
iSnap does not check the status of an attachment when it accepts the send. An attachment that is not ready when the line’s device takes the message is left out of the message, without an error. Wait for status: ready before you send an attachment: see Attachments.
The line’s device sends only the first attachment of a message, and the message is still reported sent. Send one attachment per message. How the device sends a file describes the rest.
A send with no body and exactly one attachment whose content type starts with audio/ is a voice note: its kind is voice. Every other send has kind: message.
Effects
effect names a bubble effect or a screen effect:
Any other name returns
400 validation_failed. iSnap stores the effect and passes it to the line’s device. The device applies it only to a message that has a body and no attachment, sent over iMessage, and only when the device supports effects: see Device features. In every other case the message goes out without the effect and is still reported sent. The API does not report whether the contact saw the effect.
Replies
reply_to_message_id is the msg_ id of a message on the same line, inbound or outbound.
- An unknown id returns
404 not_found. So does a message that iSnap has deleted: see Retention. - A message of another line returns
422 reply_to_cross_line. - When iSnap later deletes the message that the reply points to, the reply stays and its
reply_to_message_idbecomesnull.
reply_to_message_id never makes a send fail on the device.
Metadata
metadata holds at most 20 keys and 8,192 bytes once serialized as JSON: a larger object returns 400 validation_failed. iSnap stores it unchanged and returns it on the message and in the message object of every event about the message. It is null when you send none. Use it to carry the ids of your own records, such as the order that the message is about.
The response
POST /v1/messages returns 202 with the message object, whether the message leaves now or iSnap holds it. A 202 means that iSnap accepted the message, never that it was sent.
use_typing_indicator holds the value in force for this message: the one you sent, or the line’s default_use_typing_indicator. On a line that is not an iMessage line it is always false. reply_lane is true when the message is in the reply lane: a held message enters the lane, and reply_lane changes from false to true, when its contact writes to the line. ignore_quiet_hours holds the value in force for this message, and never changes after the send: see Sending during quiet hours.
queue_info is filled only in this response. It is null when you read the message, in a repeated request with the same idempotency key, and in events.
A
202 sent with an account API key also carries two headers about the new-contact limit: see Headers on a send.
How iSnap checks a send
After the checks that every request goes through (see How a request is checked), iSnap checks a send in this order. The first check that fails decides the response.- The line exists and belongs to your account:
404 not_foundotherwise. - A line-scoped key has the line in its list:
403 forbiddenotherwise. - The line is
active:403 line_suspendedfor a suspended line,400 line_not_activefor any other status. - The
Idempotency-Key, when present: a repeated request returns the message already created, before the checks below. tois a valid phone number:400 bad_requestotherwise.reply_to_message_id:404 not_foundor422 reply_to_cross_line.- Each attachment id:
404 not_found. - On an inbound-only line, the recipient has written to the line:
403 outbound_first_forbiddenotherwise. - The sending limits of the line: the message is held, or refused with
429on a line set to refuse.
Following a message
The message statuses table defines each status and says which are final. This section says when each step happens and what to expect from it.
Build on these rules:
- Nothing is guaranteed to follow
sent,deliveredorread. A message can staysentuntil iSnap deletes it, at least 72 hours after it recorded it. Do not wait in a loop fordelivered. - A
failedcan followsent,deliveredorread. - The order of the reports is not strict: a message can be
readwithdelivered_atstillnull, and a late send confirmation can setstatusback tosentwhiledelivered_atorfailed_atstays set. Read the timestamps, not onlystatus. - Delivery order is not guaranteed. For message events, apply Building the status from events.
Building the status from events
Events about one message can arrive in any order, and thedata.message of one event can be older than that of another. The message object carries no version, and its timestamps do not order its events: a failure reported after sent carries the time of the send in failed_at. Apply these rules:
cancelledis final. Treatfailedas final too: a late send confirmation can setstatusback tosent, and sendmessage.sent, after a failure that the line’s device reported, withfailed_atstill set. Once you storefailedorcancelled, ignore thestatusof every later event about the message, and treat a message that has afailed_atas failed.- Otherwise keep the furthest of
sent,deliveredandreadthat an event reported, whatever the order of arrival. A message with adelivered_atwas delivered, even when a later snapshot readssent. - Copy
sent_at,delivered_at,read_atandfailed_atwhen a snapshot sets one, and never clear one. - Before
sent, a message can move betweenscheduledandqueuedin both directions. When you need the status for certain, readGET /v1/messages/{id}.
cancelled is final, and failed is final for your records. A message sent as SMS or RCS gets no delivered and no read, and a failed can still follow its sent. An iMessage can still become failed after delivered or read, until iSnap deletes it. After that, no event and no read concerns the message: keep the last status you stored.
Receipts
Whatsent, delivered and read prove depends on the service that carried the message. This section describes iMessage lines. iSnap makes no statement on this page about what the device of an Android line reports.
sentmeans that the line’s device handed the message to Apple’s messaging service. It does not mean delivered, and afailedcan follow.- iMessage.
deliveredandreadcome from Apple, through the line’s device, anddelivered_atandread_atare the times that Apple recorded.readarrives only when the contact shares read receipts. Areadafter adeliveredis not guaranteed: once a message has its first receipt, a laterreador a later failure can be missed. When Apple records an error on an iMessage already reportedsent, the message becomesfailed, with noerror_message, and itsfailed_atholds the time of the send, not of the failure. - SMS and RCS. iSnap reports no delivery and no read. A failure after
sentis reported only while the line’s device still reads the message: the API does not bound that time, and a later failure is not reported.
Failures
A message becomesfailed in four cases. error_code tells three of them apart:
error_code is set only when iSnap failed the message itself, and takes one of the three values above: branch on it. error_message is diagnostic text. When the device reported the failure, it is text written by the line’s device, or null when the device gave none. Log it with the message id. Do not branch on it: its values are not part of the contract.
iSnap never resends a failed message. To try again, send a new message with a new idempotency key: the key of the failed message returns the failed message.
A message sent on a line that has no paired device stays queued. It is not given to a device paired later, and it fails after 48 hours with carrier_unavailable. A message waiting for a paired device that is offline leaves when that device reconnects. While the line’s device cannot send, for example while its iMessage session is not ready, it keeps the messages it took and can send them later. iSnap does not wait for it: a message that the device took and did not report on within 10 minutes, 4 times in a row, fails with error_code: no_terminal_ack, and the device can still send it afterward.
A message that iSnap fails itself stops counting against the sending limits. A message that the device reports as failed keeps counting: see Messages that stop counting.
Services and fallback
The service is what carried the message:iMessage, SMS or RCS. The service field of the message reports it once the device confirms the send. This section describes iMessage lines.
iMessage is the default service of the line’s device. The device sends over SMS or RCS when it knows that the recipient is not registered with iMessage: from a lookup, from an earlier send, or from Apple while the device watches the send. When it does not know, it can use an existing SMS or RCS Messages conversation with the recipient. The device does not fall back to SMS or RCS:
- when the iMessage send times out: the device retries it over iMessage;
- when Apple reports another error: the device can try again, then the message fails;
- after an iMessage reported
sent, even when it is never delivered or fails later; - when Apple’s “not registered” verdict arrives after the device stopped watching the send.
service shows which one carried the message.
The line needs a route for SMS and RCS. Rented iMessage lines have one. On a BYOD iMessage line, the route exists only when an iPhone forwards its text messages to the Mac: sms_fallback is then active in GET /v1/lines/{id}/health. Without it, a message to a recipient who is not on iMessage fails, and no SMS is attempted.
message.fallback_triggered means that the message went out, or was attempted, over SMS or RCS instead of iMessage. It does not mean that an iMessage attempt failed first. fallback_service names the service, and the same report also produces message.sent or message.failed. When the SMS attempt times out, the device retries the whole send, iMessage first.
Device features
Several features depend on what the line’s device can do. This section describes iMessage lines. iSnap makes no statement on this page about the device of an Android line. Rented iMessage lines provide every feature of the table. On a BYOD iMessage line, the features depend on your own Mac and iPhone: readruntime_capabilities in GET /v1/lines/{id}/health. A feature works when its capability is active.
reactions_out, reaction_remove_out, mark_read_out and effects always have the same value. typing_out has it too, unless typing was turned off on the device. lookup_probe is active when they are. sms_fallback is active only when an iPhone forwards its text messages to the Mac.
The device reports its capabilities every 30 seconds by default, so a value can be that old. After 3 failed attempts in a row of one feature, the device pauses that feature for 30 seconds to 300 seconds while the capability still reads active: a feature can fail while its capability is active.
Retrying a send
POST /v1/messages honors the Idempotency-Key header. Always send one: generate it once per message, and store it with your own record of the message.
A repeat with the same key and the same content returns the message already created, as it is now: see Messages, reactions and contact cards for the scope, the lifetime and the conflicts of the key. Checks 1 to 3 of How iSnap checks a send run before the key: a repeat to a line that is no longer active is refused, even when the first request was accepted.
Retry a write only on an operation that honors Idempotency-Key, with the key of the first attempt.
By default, the SDK sends a key of its own with every write, and reuses it only on its automatic retries of that call. To retry a send yourself, pass the key of the first attempt in idempotencyKey, as the sample of The send request does.
Canceling a message
DELETE /v1/messages/{id} cancels a message whose status is queued or scheduled and that the line’s device has not taken: any held message, and a queued message whose device is offline or has not taken it. The response is 200 with the message, status: cancelled and cancelled_at set.
- A canceled message is not given to the device again. A message that the device took and did not report on within 10 minutes can be canceled once iSnap has taken it back, and the device can still send the copy it kept. iSnap sends
message.cancelled. - A message that had counted against the sending limits stops counting against them. A held message had not counted.
- Later messages to the same recipient still leave: see Order.
409 too_late_to_cancel when the message is sent, delivered, read, failed or already cancelled, and when it is queued and the line’s device has taken it. On a queued message, the 409 means that the device has the message: it cannot be recalled, and the device goes on with the send. Read the message, or wait for its events, to know its outcome.
The operation ignores Idempotency-Key. Repeating it is harmless: a second cancel of a canceled message returns 409 too_late_to_cancel.
Shared lines
A shared line has no number of its own. The first message to a contact creates a binding: one of iSnap’s own numbers serves that contact until the binding ends, see Shared lines. That number is thefrom of the message and the outbound_number of the chat.
- When no number can take a new contact, the send returns
503 line_pool_exhausted.503 quota_state_unavailablemeans that iSnap could not read the state of the shared line: retry. - When the number that serves the contact cannot take the message now, iSnap holds the message with
schedule_reason: carrier_retryuntilscheduled_for: 60 seconds later when the number is not available, and until the number’s own limits or spacing free room otherwise. - The sending limits, the headers and
queue_infoof a send are those of the shared line itself. - A reply from the contact is recorded only while a number serves the contact.
Inbound-only lines
An inbound-only line can message a contact only after that contact wrote to it, at any time in the past. A send to any other recipient returns403 outbound_first_forbidden: nothing is stored, and no sending limit is used. Once the contact has written, the line sends to them like a two-way line.
Test lines and trial keys
A test line sends throughPOST /v1/messages like any other line you own, under the same sending limits. It records an inbound message only when the sender is the paired contact of a trial that runs on the line: a reply from anyone else is not recorded, and no event is sent for it.
A trial key sends text only, from the trial’s line to the trial’s paired number, outside the sending limits of the line. See Send with the trial key.
An isnap_test_ account key is not a separate test environment: it acts on the same account, lines and billing as an isnap_live_ key. A send with a test key reaches a real recipient. To test an integration, send to a phone number of your own.
Events
A send produces these events:
Every one of them carries the whole message in
data.message. The sending limits produce line.quota_warning and line.quota_exceeded: see Events of the sending limits.
These events do not announce everything. A delivery receipt or a read receipt for a message that iSnap has deleted sends nothing, and a status that the message already has is not sent again. See What events do not announce, and reconcile from GET /v1/messages and GET /v1/messages/{id}. Deduplicate on event_id. Delivery order is not guaranteed: apply Building the status from events.
After a line changes owner, by a transfer or by a return to the marketplace, the events about the messages you sent on it go to the next owner, and you no longer receive them; the message events carry the message they concern. See A chat is never deleted.
Errors
The refusals of each operation are on its reference page:POST /v1/messages and DELETE /v1/messages/{id}. A trial key has refusals of its own: see With a trial key. The errors that every operation shares are in Errors. Retry a 429 after Retry-After seconds. Retry a 500 or a 503 up to 2 times, with exponential backoff. Do not retry any other 4xx: fix the request.
Personal data
A message carries personal data: the handles into and from, the text in body, attachment file names, and whatever you put in metadata. Request it only when needed. Do not copy it into logs, tickets or other tools. Refer to a contact by an id of your own, and keep only ids in metadata.
Next steps
- Sending limits: the limits, holds, quiet hours and order that apply to every send.
- Receiving messages: inbound messages and chats.
- Attachments: upload a file before you send it.