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.
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
metadataobject holds at most 20 keys and 8,192 bytes once serialized as JSON. A request over the limit returns400 validation_failed. A merge whose result is over the limit returns400 metadata_too_large. A request that only removes keys is never refused withmetadata_too_large. - The keys
migration_refandproof_lineare reserved. A request that names either, even withnull, returns400 reserved_metadata_key. The same rule applies to themetadataofPOST /v1/lines/assignandPOST /v1/lines/shared. - The body must contain
metadata,billing_anchor_at, or both:400 no_fields_to_patchotherwise. Forbilling_anchor_at, see Anniversary and billing anchor. - When a request carries both fields, the API applies
billing_anchor_atfirst andmetadatasecond, as two separate changes. If it refuses themetadata, the change of the billing anchor is already stored. Send the two fields in separate requests when that matters to you.
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_boundsor400 validation_failed, even when it is above the default.limit_out_of_boundscarries the allowedminandmaxinerror.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 toknobs.<key>.max. min_inter_message_secondsormin_inter_new_contact_secondsbelow its default returns403 cannot_decrease_delay.nullis not accepted for any key, and an unknown key is refused: both return400 validation_failed.
overridden_keys. For activity_curve, the default value is default_activity_curve.
Three keys have rules of their own:
timezonemust be a valid IANA time zone name:400 validation_failedotherwise. Once set, it stays inoverridden_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 isEurope/Londonfor a+44number, elseAmerica/Chicago. See Quiet hours.activity_curveholds at least one entry. A curve that is empty, names an hour twice or has a total weight of 0 returns422 invalid_activity_curve. Anhouroutside 0 to 23, or aweightthat is negative or not an integer, returns400 validation_failed. In a response,config.activity_curveis the curve of the line, never empty: its override, ordefault_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 asdefault_activity_curveremoves 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. APATCHthat does not sendactivity_curveleaves its override in place.default_use_typing_indicatoristrueunless you override it. A message that does not stateuse_typing_indicatortakes the line’s value.
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:
- Call
GET /v1/attention. - Replace the faults you stored with the entries of the response.
- 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 onlyreserved, use release instead: see Reservations.
Terminate
POST /v1/lines/{id}/terminate changes the line immediately:
statusbecomescancelled, which is final: the line never returns to another status. It keeps its owner.- Its
queuedandscheduledmessages fail withcarrier_terminated. - Its device sessions are revoked.
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_atis the next occurrence ofanniversary_atstrictly 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_atreturns the samecancel_at, so a retry is safe. - A line with
anniversary_at: nullreturns409 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 withterminate. A dedicated pool line returns the same code: to end it, write to support@isnap.ai. - A line that is neither
activenorsuspendedreturns409 line_not_customer_cancellable.
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 sendsline.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 itsinbound_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
Witheffective 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
Witheffective: "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_atis the first occurrence of the billing anchor at or afternot_before.not_beforeis optional and defaults to the time of the call. A pastnot_beforereturns400 not_before_in_the_past. Witheffective: "now",not_beforeis 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.
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 issuspended, 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.
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 readsbilling_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_atandactivated_at. A line that had noactivated_atgets the time of its conversion to a test line. - The end. A test line cannot be canceled at a date:
cancelandanniversaryreturn403 pool_line_not_cancellable. End it withterminate. - 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.
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 returns409 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.