> ## Documentation Index
> Fetch the complete documentation index at: https://docs.isnap.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Transfer a line to another account

> Moves a line you own to another iSnap account, immediately and without the acceptance of that account.

* **Credential:** an API key of the owner of the line. The receiving account is not asked to accept, and it can be charged immediately: read [Transfer](/guides/managing-lines#transfer) before you call this operation.
* **Line-scoped keys:** a [line-scoped key](/authentication#line-scoped-keys) gets `403 forbidden` unless the line is in its list.
* **Test keys:** a transfer sent with an `isnap_test_` account key moves the real line, with the same effect on billing: see [Test keys](/authentication#test-keys).
* **`Idempotency-Key`:** not honored. After a transfer, the line is not yours, and a second call returns `404 not_found`.
* **Events:** none, to either account. See [What events do not announce](/webhooks/event-catalog#what-events-do-not-announce).
* **`to_user_id`:** the UUID of the receiving account. The API has no operation that returns it. Ask [support@isnap.ai](mailto:support@isnap.ai) for the UUID of the receiving account.
* **Chats:** the line's chats go with it. The receiving account can then receive your inbound messages of those chats that were not marked read and the events about your earlier messages: export what you need first, see [A chat is never deleted](/guides/receiving-messages#a-chat-is-never-deleted).

## Errors

| Status | `error.code` | When |
| - | - | - |
| 400 | `invalid_recipient` | `to_user_id` is not an iSnap account. |
| 403 | `forbidden` | The request uses a line-scoped key, and the line is not in its list. |
| 404 | `not_found` | No line has this id, or you do not own the line. |
| 409 | `line_recipient_not_wholesale` | The line is a shared line, a test line or a dedicated pool line, and the receiving account is not a wholesale partner. |
| 409 | `android_rental_not_purchasable` | The line is an `active` `rental_android` line, and the receiving account is not a wholesale partner. |
| 409 | `recipient_no_payment_method` | The receiving account has no payment method on file. It saves a card in the iSnap dashboard: see [Accounts](/concepts#accounts). |
| 409 | `payment_failed` | The payment method of the receiving account was declined. Nothing moved. |
| 409 | `subscription_cancel_failed` | Your per-line subscription of the line could not be canceled, and the line did not move. A direct receiving account can already have been charged: see [Transfer](/guides/managing-lines#transfer). |
| 409 | `transfer_conflict` | The line left your account during the call, and it did not move. A direct receiving account can already have been charged, and your per-line subscription of the line, when it had one, is already canceled, with a prorated credit. See [Transfer](/guides/managing-lines#transfer). |
| 409 | `line_on_rented_macos_session`, `line_takes_no_macos_session` | The hosted macOS session of the line does not fit the next owner. Only iSnap can resolve it: write to [support@isnap.ai](mailto:support@isnap.ai) with the `trace_id` and `request_id` of the response. When that session changed during the call, these codes can follow the same billing steps as `transfer_conflict`, and a line that carried your per-line subscription then leaves your account. See [Transfer](/guides/managing-lines#transfer). |

The errors that every operation shares are in [Errors](/errors-and-rate-limits#errors).


## OpenAPI

````yaml POST /v1/lines/{id}/transfer-ownership
openapi: 3.0.0
info:
  title: iSnap API
  version: 2.45.0
  description: >-
    The iSnap API sends and receives iMessage, RCS and SMS on real phone lines,
    manages those lines, and reports events through webhooks. Every message is
    sent to a real recipient, with an `isnap_live_` key and with an
    `isnap_test_` key alike. 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. Retry a write only on an operation that honors
    `Idempotency-Key`, with the key of the first attempt. Responses carry
    personal data: handles, names and message text.
servers:
  - url: https://api.isnap.ai
security:
  - bearerAuth: []
paths:
  /v1/lines/{id}/transfer-ownership:
    post:
      summary: Transfer a line to another account
      description: >-
        Moves a line you own to another iSnap account. Accepts an API key. The
        transfer takes effect immediately, whatever the `status` of the line,
        and the receiving account is not asked to accept it.


        A per-line subscription of yours for the line is canceled immediately,
        with a prorated credit, whatever the receiving account. When an `active`
        rented line goes to a direct account, that account must have a payment
        method on file, and it is charged immediately for a new per-line
        subscription; when that charge is declined, nothing moves. A BYOD line,
        or a rented line that is not `active`, goes to a direct account with no
        charge. A shared line, a test line or a dedicated pool line can go to a
        wholesale partner only.


        The line keeps its id, its phone number, its `status`, its variant, its
        `metadata` and its chats. The receiving account lists those chats, with
        their `contact_handle`, and can receive your unread inbound messages of
        those chats and the events about the messages you sent on the line:
        export what you need from those chats before the transfer. The
        `anniversary_at`, `billing_anchor_at`, `cancel_at` and `pending_variant`
        of the line are cleared, so a scheduled end is dropped; a wholesale
        partner that receives the line gets a new `anniversary_at` and a new
        `activated_at`, both set to the time of the transfer.


        By default the device of the line stays connected, and the line keeps
        working for its new owner. This operation sends no event to either
        account; later events about the line go to its new owner.
      parameters:
        - schema:
            type: string
            pattern: ^line_[0-9A-Za-z]{25}$
            description: 'Id of a line: `line_` followed by 25 letters or digits.'
          required: true
          description: 'Id of a line: `line_` followed by 25 letters or digits.'
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferOwnership'
      responses:
        '200':
          description: The transferred line.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    description: Always `true` on a success response.
                  data:
                    $ref: '#/components/schemas/Line'
                  trace_id:
                    type: string
                    description: >-
                      Trace id of the request that produced this response: 32
                      lowercase hexadecimal characters. Quote it when you report
                      a problem to iSnap.
                  request_id:
                    type: string
                    description: >-
                      Id of the request that produced this response: `req_`
                      followed by 25 letters or digits.
                required:
                  - success
                  - data
                  - trace_id
                  - request_id
        '400':
          description: >-
            Invalid request. `validation_failed`: the path or the body does not
            match its schema. `invalid_recipient`: `to_user_id` is not an iSnap
            account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: No valid credential. `error.code` is `unauthorized`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: >-
            Refused. `forbidden`: the request carries neither an API key in its
            `Authorization` header nor the header `X-Requested-With:
            XMLHttpRequest`, or the key is line-scoped and this line is outside
            its list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: 'Not found. `not_found`: the line does not exist, or it is not yours.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: >-
            Conflict: the line is not transferred.
            `line_recipient_not_wholesale`: a shared line, a test line or a
            dedicated pool line goes to an account that is not a wholesale
            partner. `android_rental_not_purchasable`: an `active`
            `rental_android` line goes to a direct account.
            `recipient_no_payment_method`: the receiving account has no payment
            method on file. `payment_failed`: the payment of the receiving
            account was declined. `subscription_cancel_failed`: your own
            per-line subscription for the line could not be canceled.
            `transfer_conflict`: the line left your account during the request;
            your per-line subscription for it, when it had one, is already
            canceled, with a prorated credit. After `subscription_cancel_failed`
            or `transfer_conflict`, a direct receiving account can already have
            been charged for a new per-line subscription, which iSnap then
            cancels with a prorated credit: the charge is not returned to its
            card. Write to support@isnap.ai with the `trace_id` and `request_id`
            of the response. `line_on_rented_macos_session` or
            `line_takes_no_macos_session`: the hosted macOS session of the line
            does not fit this change, and only iSnap can resolve it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: >-
            Too many requests. `error.code` is `rate_limit_exceeded` when more
            than 100 requests came from the same network address in 60 seconds,
            whatever their API key or account: wait `error.retry_after` seconds.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: >-
            Unexpected failure. `error.code` is `internal_error`. An operation
            that takes a JSON body also returns it when the body, sent as
            `application/json`, is not valid JSON. Quote `trace_id` when you
            report it to iSnap.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
      security:
        - bearerAuth: []
components:
  schemas:
    TransferOwnership:
      type: object
      properties:
        to_user_id:
          type: string
          format: uuid
          description: >-
            UUID of the iSnap account that receives the line. An unknown account
            returns `400 invalid_recipient`.
        preserve_device_session:
          type: boolean
          description: >-
            Defaults to `true`: the device of the line stays connected and the
            line keeps working for the new owner. `false` revokes the device
            sessions of the line, so the device must be paired again.
      required:
        - to_user_id
      description: >-
        The account that receives the line, and what happens to the device
        sessions of the line.
    Line:
      type: object
      properties:
        id:
          type: string
          pattern: ^line_[0-9A-Za-z]{25}$
          description: 'Id of a line: `line_` followed by 25 letters or digits.'
        phone_number:
          type: string
          description: >-
            The phone number of the line in E.164 format. An empty string, never
            `null`, when the line has no number: a BYOD line whose device has
            not reported one, or a shared line. `GET /v1/attention` returns
            `null` for such a line. Masked on `GET /v1/lines`, and on `GET
            /v1/lines/{id}` unless the caller owns the line: `+1 (AAA) ***-**DD`
            for a North American number, the country code, asterisks and the
            last two digits for any other number.
        area_code:
          type: string
          nullable: true
          description: >-
            The 3-digit North American area code of the number. `null` for any
            other number, and while the number is not known.
        country_code:
          type: string
          nullable: true
          description: >-
            ISO 3166-1 alpha-2 country of the number. `null` while the number is
            not known.
        state:
          type: string
          nullable: true
          description: >-
            Two-letter code of the US state the area code belongs to. `null`
            outside the US, and while the number is not known.
        state_name:
          type: string
          nullable: true
          description: >-
            Name of that US state, for example `California`. `null` whenever
            `state` is `null`.
        billing_tier:
          type: string
          enum:
            - rental_iphone
            - rental_android
            - byod_imessage
            - byod_android
            - shared_pool
            - shared_imessage
          description: >-
            The kind of line. `rental_iphone`: a rented line that sends iMessage
            and SMS. `rental_android`: a rented line that sends SMS and RCS.
            `byod_imessage`: a BYOD line on your own Mac and iPhone.
            `byod_android`: a BYOD line on your own Android phone.
            `shared_imessage`: a shared line. `shared_pool`: a test line when
            `test_line` is `true`, otherwise a dedicated pool line: a line of
            iSnap that is dedicated to your account.
        byod:
          type: boolean
          description: >-
            `true` for a BYOD line, which runs on your own device:
            `billing_tier` is `byod_imessage` or `byod_android`.
        inbound_only:
          type: boolean
          description: >-
            `true` for an inbound-only line: it can message a contact only after
            that contact has messaged it. A send to any other handle returns
            `403 outbound_first_forbidden`. Only a rented line can be
            inbound-only: always `false` on BYOD, shared and test lines.
        capabilities:
          nullable: true
          description: >-
            The services the line sends, as an array of lowercase names:
            `["imessage", "sms"]` on `rental_iphone` and `byod_imessage` lines,
            `["sms", "rcs"]` on `rental_android` and `byod_android` lines,
            `["imessage"]` on a shared line. A test line that you made from a
            rented line keeps the array of that line. On a dedicated pool line,
            and on a test line that iSnap created for your account, the array
            does not tell the services and can be `["sms"]`: such a line is an
            iMessage line.
        status:
          type: string
          description: >-
            One of six values. `provisioning`: the line is being prepared for
            one of your pre-orders. `available`: the line belongs to no account
            and can be reserved or assigned. `reserved`: the line is reserved
            for one account until `expires_at`; it is not billed and cannot
            send. `active`: the line belongs to its owner; it is the only status
            in which a line sends. `suspended`: the line still belongs to its
            owner but cannot send (`403 line_suspended`); a line billed by a
            per-line subscription is suspended when a payment for it fails,
            iSnap can also suspend a line, and no API operation ends a
            suspension. `cancelled`: the line is ended for good and keeps its
            owner.
        monthly_price:
          type: integer
          minimum: 0
          deprecated: true
          description: >-
            Deprecated. Catalog price of this kind of line, in US cents. It is
            not the amount billed for the line. Removed in the next major
            version: no removal date is set.
        stripe_subscription_id:
          type: string
          nullable: true
          description: >-
            Id of the per-line subscription that bills the line. `null` on a
            line billed on the monthly wholesale invoice, on BYOD, shared and
            test lines, and on a rented line that a direct account received by
            transfer while the line was not `active`.
        activated_at:
          type: string
          nullable: true
          description: >-
            Timestamp at which the line became `active` (ISO 8601, UTC). `null`
            on a line that was never activated, and again on a line that
            returned to the marketplace. A transfer to a wholesale partner sets
            it to the time of the transfer; a transfer to a direct account keeps
            the earlier value.
        expires_at:
          type: string
          nullable: true
          description: >-
            End of the reservation (ISO 8601, UTC): 15 minutes after the line
            was reserved. Read it only while `status` is `reserved`: in any
            other status it is `null` or a past timestamp.
        anniversary_at:
          type: string
          nullable: true
          description: >-
            The anniversary of the line: the timestamp at which you took it (ISO
            8601, UTC). `POST /v1/lines/{id}/cancel` ends the line at the next
            occurrence of this timestamp. iSnap sets it each time a line becomes
            a wholesale partner's, a transfer included, and `POST
            /v1/lines/{id}/anniversary` replaces it. A conversion to a test line
            and back keeps it. `null` on a line owned by a direct account or by
            no account, and on a line of a wholesale partner whose anniversary
            was never set.
        cancel_at:
          type: string
          nullable: true
          description: >-
            Timestamp at which iSnap ends this line (ISO 8601, UTC). Set by
            `POST /v1/lines/{id}/cancel` and cleared by `POST
            /v1/lines/{id}/uncancel`. The line works and is billed as before
            until then. `null` when no end is scheduled. A line that ended at
            this date keeps the value. On a test line ended with `POST
            /v1/lines/{id}/terminate`, it holds the time the line ended.
        billing_anchor_at:
          type: string
          nullable: true
          description: >-
            The billing anchor you stated for this line (ISO 8601, UTC): a line
            billed on the monthly wholesale invoice is billed at each monthly
            occurrence of this timestamp. `null` when you stated none: the line
            is then billed at each monthly occurrence of `activated_at`. State
            it with `POST /v1/pre-orders`, `POST /v1/lines/{id}/activate` or
            `PATCH /v1/lines/{id}`. A change applies from the next occurrence
            and does not alter a month already billed.
        next_anchor_at:
          type: string
          nullable: true
          description: >-
            For a line billed on the monthly wholesale invoice: the next
            timestamp at which it is billed (ISO 8601, UTC). It is the next
            occurrence, strictly after the read, of `billing_anchor_at`, or of
            `activated_at` when no billing anchor is stated: the same day of the
            month, moved to the last day of a shorter month, at the same time of
            day. `null` when the line is not `active` or `suspended`, when it
            was never activated, and when its `cancel_at` falls on or before
            that occurrence. A line billed by a per-line subscription also
            carries a value here, which does not tell when that line is billed.
        pending_variant:
          type: object
          nullable: true
          properties:
            inbound_only:
              type: boolean
              description: The value `inbound_only` takes when the change applies.
            effective_at:
              type: string
              description: >-
                The billing anchor occurrence at which the change applies (ISO
                8601, UTC). iSnap applies it after this timestamp, and before it
                charges the line for the month that starts then: the API does
                not bound the delay.
          required:
            - inbound_only
            - effective_at
          description: >-
            The variant change scheduled for this line, or `null` when none is
            pending. The `inbound_only` field of the line keeps its value until
            the change applies. Schedule a change with `POST
            /v1/lines/{id}/variant` and `effective: "next_anchor"`; withdraw it
            with `DELETE /v1/lines/{id}/variant`. It becomes `null` when the
            change is applied, when it is withdrawn, when it can no longer apply
            at its date, when the line becomes a test line, and when the line
            leaves its owner without ending. A line that ends keeps its
            `pending_variant`, and the change never applies.
        created_at:
          type: string
          description: >-
            Timestamp at which iSnap created the line (ISO 8601, UTC). It is not
            the date you acquired the line.
        metadata:
          type: object
          nullable: true
          additionalProperties:
            nullable: true
          description: >-
            Your own key-value data on the line. `null` when the line has none.
            A line created with `"metadata": {}` reads `{}`: treat `null` and
            `{}` the same. It is part of the line object for every caller, `GET
            /v1/lines` and `GET /v1/lines/{id}` included, and a line that
            returns to the marketplace keeps it: never put personal data or
            secrets in it.
        test_line:
          type: boolean
          description: >-
            `true` for a test line: a line you set aside to host the trials of
            your own prospects. Its `billing_tier` is `shared_pool`.
        shared:
          type: boolean
          description: >-
            `true` for a shared line, which has no device and no phone number of
            its own: its `billing_tier` is `shared_imessage` and its
            `phone_number` is an empty string.
        fulfillment_status:
          type: string
          nullable: true
          description: >-
            Present only on `GET /v1/lines/my`. `open` while an order for this
            line is still being fulfilled, `null` otherwise.
      required:
        - id
        - phone_number
        - area_code
        - country_code
        - state
        - state_name
        - billing_tier
        - byod
        - inbound_only
        - status
        - monthly_price
        - stripe_subscription_id
        - activated_at
        - expires_at
        - anniversary_at
        - cancel_at
        - billing_anchor_at
        - next_anchor_at
        - pending_variant
        - created_at
        - metadata
        - test_line
        - shared
      description: >-
        A line. `billing_tier`, `byod`, `shared`, `test_line` and `inbound_only`
        tell the kinds of line apart: a rented line (two-way or inbound-only), a
        BYOD line, a shared line, a test line.
    ErrorEnvelope:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - false
          description: Always `false` on an error response.
        data:
          nullable: true
          description: Always `null` on an error response.
        error:
          type: object
          properties:
            status:
              type: integer
              description: The HTTP status of the response.
            code:
              type: string
              description: >-
                Stable machine-readable code that names the refusal. Branch on
                this value.
            message:
              type: string
              description: >-
                Text for a person. It can change between versions: do not branch
                on it.
            retry_after:
              type: integer
              nullable: true
              description: >-
                Seconds to wait before the next attempt. Set on the `429`
                responses that know a delay, where the `Retry-After` header
                carries the same value. When it is above 60 seconds, do not wait
                in a request: schedule the call, or report the time. `null` on
                every other error.
            retry_at:
              type: string
              nullable: true
              description: >-
                ISO 8601 timestamp at which the refusal ends. Set only on `429
                rebind_cap_reached`. `null` on every other error.
            issues:
              type: array
              nullable: true
              items:
                type: object
                properties:
                  path:
                    type: array
                    items:
                      anyOf:
                        - type: string
                        - type: number
                    description: >-
                      Where the problem is: the field names and array indexes
                      that lead to it.
                  message:
                    type: string
                    description: Text for a person.
                  code:
                    type: string
                    description: >-
                      Kind of problem. On `validation_failed`, for example
                      `invalid_type`, `invalid_value`, `invalid_format`,
                      `invalid_union`, `invalid_key`, `invalid_element`,
                      `too_small`, `too_big`, `not_multiple_of`,
                      `unrecognized_keys` or `custom`. The list is open: read
                      `path` and `message`.
                  min:
                    type: integer
                    description: >-
                      Smallest accepted value. Present only on
                      `limit_out_of_bounds`, `mac_quantity_not_a_pack`,
                      `byop_reduction_strands_mac_sessions` and
                      `byop_reduction_strands_byod_lines`.
                  max:
                    type: integer
                    description: >-
                      Largest accepted value. Present only on
                      `limit_out_of_bounds` and
                      `mac_sessions_exceed_byop_lines`, and on the `too_big`
                      issue of an `Idempotency-Key` header longer than 255
                      characters.
                required:
                  - path
                  - message
                  - code
              description: >-
                Problems found in the request, one entry each. Set on most `400
                validation_failed` responses and on the refusals that name a
                field: a refused `handle` of `POST /v1/lookup` has `issues:
                null`. `null` on every other error.
          required:
            - status
            - code
            - message
            - retry_after
            - retry_at
            - issues
          description: >-
            What went wrong. The six fields are always present; `retry_after`,
            `retry_at` and `issues` are `null` when they do not apply.
        trace_id:
          type: string
          description: >-
            Trace id of the request that produced this response: 32 lowercase
            hexadecimal characters. Quote it when you report a problem to iSnap.
        request_id:
          type: string
          description: >-
            Id of the request that produced this response: `req_` followed by 25
            letters or digits.
      required:
        - success
        - data
        - error
        - trace_id
        - request_id
      description: The body of an error response.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        An API key created in the iSnap dashboard, or a trial key, sent as
        `Authorization: Bearer <key>`. An API key starts with `isnap_live_` or
        `isnap_test_`; both act on the same account, lines and billing. A trial
        key acts on its trial only.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.