Skip to main content
Once a line is yours, you read it, attach your own metadata to it, adjust how it sends, watch its state, change its variant, transfer it and end it. This page covers each of these operations, with who can call it, its effect on billing and the events it sends. Read it when you build the part of your product that looks after lines you already own. To get a line, see Getting a line.

Your lines

GET /v1/lines/my returns the lines of your account whose status is active or reserved, of every kind: rented, BYOD, shared and test lines. The response is { lines: [...] }, ordered by created_at, oldest first, with full phone numbers. It has no pagination and no filter, and the API does not bound the number of lines in one response. The list leaves out the lines whose status is suspended, provisioning or cancelled. A line that disappears from the list has not necessarily ended: keep the ids of your lines, and read a missing rented or BYOD line by its id. GET /v1/lines/{id} returns one rented or BYOD line, in any status, to any caller. It requires no credential. A missing, malformed or revoked credential gets the masked view with 200, never 401. A masked number in a response to your own key therefore means that the API did not accept that key as the owner’s. Shared lines, test lines and dedicated pool lines are not served by this operation: it returns 404 not_found for them, even to their owner. Read them with GET /v1/lines/my. No read operation returns a shared, test or dedicated pool line that is neither active nor reserved: keep its id. PATCH /v1/lines/{id} still returns it, and so does terminate for a shared or test line that has ended. Some fields of the line object need a note: The operations of this page return the line’s own phone number, in full to its owner. Request it only when you need it, and do not copy it into logs, tickets or other tools. These operations return no Apple ID, no email address and no message body. A suspended line still belongs to you but cannot send. A line billed by a per-line subscription is suspended when a payment of that subscription fails, and iSnap can also suspend a line. A suspended line of a wholesale partner stays on the monthly count. No event announces a suspension or its end, and only iSnap lifts one: write to support@isnap.ai.

Metadata

metadata is an object of your own keys on a line, for example the id of the account that the line serves in your system. iSnap stores the values as given and does not interpret them. The events about a line carry its metadata: see Envelope.
Line metadata is readable by anyone who holds the line id, and it stays on a line that returns to the marketplace or that you transfer. Never put personal data or secrets in line metadata. Refer to a contact, or to an account in your own system, by an id of your own.
A line that you reserve from the marketplace keeps the keys that an earlier owner left. Read its metadata and remove the keys you do not know: see Metadata at acquisition. PATCH /v1/lines/{id} merges the metadata you send into the stored object: The limits and the refusals:
  • A metadata object holds at most 20 keys and 8,192 bytes once serialized as JSON. A request over the limit returns 400 validation_failed. A merge whose result is over the limit returns 400 metadata_too_large. A request that only removes keys is never refused with metadata_too_large.
  • The keys migration_ref and proof_line are reserved. A request that names either, even with null, returns 400 reserved_metadata_key. The same rule applies to the metadata of POST /v1/lines/assign and POST /v1/lines/shared.
  • The body must contain metadata, billing_anchor_at, or both: 400 no_fields_to_patch otherwise. For billing_anchor_at, see Anniversary and billing anchor.
  • When a request carries both fields, the API applies billing_anchor_at first and metadata second, as two separate changes. If it refuses the metadata, the change of the billing anchor is already stored. Send the two fields in separate requests when that matters to you.
The operation works on any line you own, in any status and of any kind, shared and test lines included. It returns 404 not_found for any other line. It sends no event. A line-scoped key is not restricted to its list here: it can change the metadata and the billing anchor of any line of its account. It also gets the full phone number of any line of its account from GET /v1/lines/{id}.

Configuration

Each line has a send configuration: its sending limits, its spacing, its reply lane, what happens when a limit is reached, its time zone and its typing indicator default. Sending limits explains what each setting does to your messages. This section explains how to read and change the settings. GET /v1/lines/{id}/config works on any line you own and returns: Default values depend on the kind of device the line runs on, and iSnap can change them. Do not hardcode them: read knobs.<key>.default, default_activity_curve, or the value of config for a key that is not in overridden_keys. PATCH /v1/lines/{id}/config sets the keys you send and keeps the others. One rule governs the numbers: a limit can only be lowered below its default, and a delay can only be raised above its default. The refusals:
  • For each key, the range is checked first, on every line: a number outside its range returns 422 limit_out_of_bounds or 400 validation_failed, even when it is above the default. limit_out_of_bounds carries the allowed min and max in error.issues[0].
  • A limit inside its range and above its default returns 403 cannot_increase_limit. On a BYOD line, the first five keys of the table are exempt: they can go above the default, up to knobs.<key>.max.
  • min_inter_message_seconds or min_inter_new_contact_seconds below its default returns 403 cannot_decrease_delay.
  • null is not accepted for any key, and an unknown key is refused: both return 400 validation_failed.
To return a key to its default, send the key with its default value: the override is removed and the key leaves overridden_keys. For activity_curve, the default value is default_activity_curve. Three keys have rules of their own:
  • timezone must be a valid IANA time zone name: 400 validation_failed otherwise. Once set, it stays in overridden_keys: this operation can replace it, not remove it. With no override, the time zone in force comes from the line’s US area code, else from its country when the country has a single time zone, else it is Europe/London for a +44 number, else America/Chicago. See Quiet hours.
  • activity_curve holds at least one entry. A curve that is empty, names an hour twice or has a total weight of 0 returns 422 invalid_activity_curve. An hour outside 0 to 23, or a weight that is negative or not an integer, returns 400 validation_failed. In a response, config.activity_curve is the curve of the line, never empty: its override, or default_activity_curve. On a shared line, the curve of the number that serves the contact applies to the sends instead: see Quiet hours. A curve that you send and that is the same as default_activity_curve removes the override instead of setting one. Two curves are the same when every hour from 0 to 23 has the same weight in both, an hour with no entry counting as 0: the order of the entries does not matter. A PATCH that does not send activity_curve leaves its override in place.
  • default_use_typing_indicator is true unless you override it. A message that does not state use_typing_indicator takes the line’s value.
A change of configuration sends no event. A line-scoped key gets 403 forbidden on both operations for a line outside its list.

Health

GET /v1/lines/{id}/health returns the health of a BYOD line: what its device last reported. It serves BYOD lines only. For a rented, shared or test line that you own, it returns 403 rental_health_not_exposed: use Attention for those lines. For a line you do not own, it returns 404 not_found. state is the summary. The API evaluates the three values in this order: The other fields: When apple_id_flagged becomes true, the API sends line.apple_id_flagged once, for rented and BYOD lines alike. Only iSnap clears the flag, and no event announces that it was cleared: read apple_id_flagged again to know.

Connectivity events

The events about the connection of a line differ by line kind. A BYOD line runs on your own device, so you get each change with its detail. For a rented line or a test line, you get only an outage that lasts. A shared line has no device of its own. Of these events, it gets only line.disconnected, with device_session_id: null. These events do not cover every change. For a line that is not BYOD, no event announces an outage shorter than 15 minutes, a line that stops receiving, or a change of capability. No event announces that iSnap suspended a line, or that a device session was replaced or revoked by iSnap. See What events do not announce, and reconcile from the operations of this page: Attention for every line, Health for a BYOD line. Deduplicate on event_id. Delivery order is not guaranteed. Order events by the timestamps inside data, not by arrival. See Duplicates and Order.

Attention

GET /v1/attention lists the lines of your account that have a fault at the time of the call: an open incident, a degraded state, or a failing device check. The response is { entries: [...] }, with one entry per line, even when the line has several faults. The fields of an entry: On a line that is not a BYOD line, reasons is empty and detail_withheld is true: the API does not disclose why. To report the fault, write to support@isnap.ai with the line_id. The list holds faults, not statuses: a suspended line is not listed for its suspension, and GET /v1/lines/my leaves it out. Read each rented or BYOD line whose id you keep with GET /v1/lines/{id} to see its status. Entries are ordered by since, oldest first, then by line_id. Entries with since: null come last. There is no pagination, and the API does not bound the number of entries. A line-scoped key gets only the entries of the lines in its list. The list is the state at the time of the call, not a history: an entry disappears when the fault ends. Use it to rebuild what you know after a missed event, or on a schedule:
  1. Call GET /v1/attention.
  2. Replace the faults you stored with the entries of the response.
  3. Treat each of your lines that is not in the response as having no fault to report. For a line that is not BYOD, that means no fault that has lasted 15 minutes.

Ending a line

Three operations end a line or change when it ends. To give back a line that is only reserved, use release instead: see Reservations.

Terminate

POST /v1/lines/{id}/terminate changes the line immediately:
  • status becomes cancelled, which is final: the line never returns to another status. It keeps its owner.
  • Its queued and scheduled messages fail with carrier_terminated.
  • Its device sessions are revoked.
The effect on billing depends on how the line is billed: A direct account can also cancel the per-line subscription of a rented line in the iSnap dashboard. The line then stays active until the end of the paid month, and returns to the marketplace with no owner instead of becoming cancelled: see Ends you do not request. The optional reason is a note of 1 to 255 characters, without control characters. It defaults to terminated by the customer and comes back as reason in the line.terminated event. The events of a termination are line.terminated with origin: customer_initiated, line.disconnected with reason: customer_initiated, one message.failed for each failed message, and one binding.released for each released binding of a shared line. Hosted macOS sessions that ran the line are taken off it and listed in released_macos_session_ids of line.terminated. Each stays rented until you cancel it with POST /v1/macos-sessions/{id}/cancel. Terminating a line that is already cancelled returns 200 with the line unchanged and sends no event, so a retry is safe. A reserved or provisioning line returns 409 line_not_customer_terminable. A test line returns 409 test_line_in_use while a trial that is not revoked uses it; after it ends, its cancel_at holds the time it ended. When the per-line subscription cannot be read or canceled, the call returns 409 with subscription_cancel_failed, subscription_lookup_failed or subscription_id_untrustworthy, and the line is not terminated. Branch on error.code, never on error.message. Write to support@isnap.ai with the trace_id and request_id of the response. After a termination, the owner can still read a rented or BYOD line with GET /v1/lines/{id}. A shared or test line cannot be read after it ends.

Cancel at the anniversary

POST /v1/lines/{id}/cancel sets cancel_at and changes nothing else: the line keeps working, and it is billed as before until that time. Wholesale partners only. A direct account ends a line with terminate, or by canceling its per-line subscription in the dashboard.
  • cancel_at is the next occurrence of anniversary_at strictly after the call, computed in UTC: the same day of the month, moved to the last day of a shorter month, at the same time of day.
  • Canceling a line that already has a cancel_at returns the same cancel_at, so a retry is safe.
  • A line with anniversary_at: null returns 409 line_has_no_anniversary: state the anniversary first, as described in Anniversary and billing anchor.
  • A test line returns 403 pool_line_not_cancellable: end it with terminate. A dedicated pool line returns the same code: to end it, write to support@isnap.ai.
  • A line that is neither active nor suspended returns 409 line_not_customer_cancellable.
iSnap ends the line after cancel_at passes; the API does not bound the delay. The line then ends as with terminate, and the same events are sent, with origin: scheduled_cancellation and the cancel_at in line.terminated, and reason: scheduled_cancellation in line.disconnected. POST /v1/lines/{id}/uncancel sets cancel_at back to null. On a line with no cancel_at, it returns 200 and changes nothing. On a line that has ended, it returns 409 line_already_terminated. Neither cancel nor uncancel sends an event: the events come when the line ends.

Ends you do not request

A line can also end, or leave your account, without a call of yours. Not every such end sends line.terminated: A line that returns to the marketplace keeps its metadata and its chats. The account that gets it next lists the chats, and can receive your inbound messages that were not marked read and the events about your earlier messages. Before you let a line return to the marketplace, export what you need from its chats: see A chat is never deleted. To catch these, read your lines on a schedule and compare: see Your lines and What events do not announce.

Anniversary and billing anchor

A line carries two monthly dates, and they can differ: the anniversary decides when a scheduled cancellation ends the line, and the billing anchor decides the day of the month on which the line is billed. An occurrence is computed in UTC: the same day of the month, moved to the last day of a shorter month, at the same time of day. The anniversary. iSnap sets anniversary_at once, when a line becomes the line of a wholesale partner: at activate, assign, the creation of a shared line, the pairing of a BYOD line, a transfer, or the hand-over of the line of a pre-order. It is null on a line of a direct account. A line that you brought in from another system has none until you state it. POST /v1/lines/{id}/anniversary records the anniversary_at that you state and overwrites any earlier value. Wholesale partners only: 409 line_not_wholesale_metered otherwise. The call returns 400 anniversary_in_the_future for a timestamp after the call, 409 line_cancellation_pending while the line has a cancel_at, and 403 pool_line_not_cancellable on a test line or a dedicated pool line. The billing anchor. State it at activate, or with PATCH /v1/lines/{id} and billing_anchor_at. null clears it. A past timestamp is accepted. A timestamp more than 31 days after the call returns 400 billing_anchor_too_far. Sending the value that the line already has changes nothing. The next billing anchor. next_anchor_at is the next occurrence strictly after the time of the read. Only a line billed on the monthly invoice of a wholesale partner is billed at it: a line billed by a per-line subscription also shows a value, and its per-line subscription does not charge at that time. It is null when the line is neither active nor suspended, when it was never activated, and when it has a cancel_at on or before that occurrence. Two dates, two days. cancel_at follows anniversary_at, and next_anchor_at follows billing_anchor_at. On a line with a stated billing anchor, the two can fall on different days of the month: a line taken on the 2nd with a billing anchor on the 11th is billed on the 11th, and a cancellation ends it on the 2nd. The dates follow the owner. anniversary_at, billing_anchor_at, cancel_at and pending_variant are cleared when a line leaves its owner without ending: a release, a transfer, or a return to the marketplace. A terminated line keeps its cancel_at and its pending_variant. None of these changes sends an event.

Variant changes

The variant of a rented line is its inbound_only value: false for a two-way line, true for an inbound-only line. POST /v1/lines/{id}/variant moves a line from one to the other and keeps its number. The request takes inbound_only (required), and effective: now, the default, or next_anchor. The operation works on an active rented line that has no per-line subscription:

Immediate change

With effective absent or now, the variant changes immediately: the next send obeys the new value. Asking for the value that the line already has returns 200 and changes nothing.

Scheduled change

With effective: "next_anchor", the change is stored and inbound_only does not move. pending_variant holds the change, for example { "inbound_only": true, "effective_at": "2026-11-08T14:22:57.392Z" }.
  • effective_at is the first occurrence of the billing anchor at or after not_before. not_before is optional and defaults to the time of the call. A past not_before returns 400 not_before_in_the_past. With effective: "now", not_before is ignored.
  • When no change is pending, scheduling the value that the line already has stores nothing and returns pending_variant: null.
  • Resending the pending change returns it unchanged. A different change while one is pending returns 409 variant_change_already_pending.
  • An immediate change does not clear a pending one.
  • A line with neither a billing anchor nor an activation timestamp returns 409 line_has_no_billing_anchor.
iSnap applies a scheduled change after effective_at passes, and before it charges the line for the new month. The API does not bound the delay. A scheduled change can be dropped without any event. pending_variant then reads null, and inbound_only is unchanged:
  • At effective_at, when the line is suspended, is not a rented line, has a per-line subscription, or already has the scheduled value.
  • When the line leaves its owner: a release, a transfer, or a return to the marketplace.
  • When the line becomes a test line.
A terminated line is the exception: it keeps its pending_variant, and the change never applies. Read pending_variant and inbound_only after effective_at to know what happened. DELETE /v1/lines/{id}/variant withdraws the pending change and returns 204 with no body. It returns 404 no_pending_variant_change when nothing is pending, and 409 variant_change_already_applied when the change was applied during the call. It works on a line in any status.

Variant events

A request for the variant that the line already has sends no event, and a dropped change sends none either: see What events do not announce.

Test lines

A test line is a line set aside for the trials of its owner: a two-way rented line that a wholesale partner converted, or a line that iSnap created for the account. It reads billing_tier: shared_pool, test_line: true and inbound_only: false. A converted line keeps its number and its capabilities, and you can still send from a test line. Both conversions are for wholesale partners only: any other account receives 402 checkout_required. POST /v1/lines/{id}/convert-to-test turns a line into a test line. The line must be an active, two-way rented line with no per-line subscription and no cancel_at. Any pending variant change is cleared. The refusals are all 409: POST /v1/lines/{id}/convert-from-test turns an active test line back into a rented line. It returns 409 not_a_test_line, 409 line_not_active, or 409 test_line_in_use while a trial that is not revoked uses the line. What a conversion does not change:
  • Billing. A test line is billed like a two-way rented line. A converted line keeps its dates: both conversions keep anniversary_at, billing_anchor_at and activated_at. A line that had no activated_at gets the time of its conversion to a test line.
  • The end. A test line cannot be canceled at a date: cancel and anniversary return 403 pool_line_not_cancellable. End it with terminate.
  • Events. Neither conversion sends an event.
GET /v1/lines/{id} returns 404 not_found for a test line: read it with GET /v1/lines/my. To get a line as a test line in one call, see as_test_line in Assignment.

Transfer

POST /v1/lines/{id}/transfer-ownership moves a line that you own to another iSnap account.
A transfer takes effect immediately, and the receiving account is not asked to accept it. When an active rented line goes to a direct account, that account is charged immediately. The line goes to the receiving account with its metadata and its chats, with their contact_handle. The delivery statuses, read receipts and reactions that the line’s device reports afterward about the messages you sent on it go to the receiving account; the message events carry the message they concern. When the receiving account marks one of these chats read, it receives in message.read each inbound message of the chat that iSnap still keeps and that you had not marked read, with its text. Before you transfer a line, export what you need from its chats: see A chat is never deleted.
The status of the line is not checked and does not change. What moves and what is cleared: A receiving wholesale partner gets a new anniversary_at and a new activated_at, both set to the time of the transfer. The effect on billing depends on the line and on the receiving account: A transfer sends no event to either account. After it, the events about the line go to the next owner, and they carry the metadata that you left on the line.

Concurrency refusals

When an operation loses a race with another change of the same line, it returns 409 with a code that says so, and the line is not changed: Read the line again, then decide whether the operation still applies. A terminate or a transfer that loses a race can already have canceled the line’s per-line subscription. A line that has not ended and still carried that subscription then leaves your account, and no event announces it. Write to support@isnap.ai with the trace_id and request_id of the response. reserve, assign, transfer-ownership and the two test line conversions can also return 409 line_on_rented_macos_session or 409 line_takes_no_macos_session, when the hosted macOS session of the line does not fit the change. Only iSnap can resolve it: write to support@isnap.ai with the trace_id and request_id of the response.