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

# Update the metadata or billing anchor of a line

> Merges metadata into a line you own, and sets or clears its billing anchor.

* **Credential:** an API key. The operation works on any line you own, in any status and of any kind. See [Metadata](/guides/managing-lines#metadata) and [Anniversary and billing anchor](/guides/managing-lines#anniversary-and-billing-anchor).
* **Line-scoped keys:** the list of a [line-scoped key](/authentication#line-scoped-keys) is not applied here. The key can change the `metadata` and the billing anchor of any line of its account.
* **`Idempotency-Key`:** not honored. The operation merges, so the same request sent twice leaves the line in the same state.
* **Events:** none. See [What events do not announce](/webhooks/event-catalog#what-events-do-not-announce).
* **Two fields, two changes:** when the body carries both fields, `billing_anchor_at` is applied first and `metadata` second. If the `metadata` is refused, the change of the billing anchor is already stored.

## Errors

| Status | `error.code` | When |
| - | - | - |
| 400 | `no_fields_to_patch` | The body has neither `metadata` nor `billing_anchor_at`. |
| 400 | `validation_failed` | The `metadata` of the request holds more than 20 keys or more than 8,192 bytes, or `billing_anchor_at` is not an ISO 8601 timestamp with a `Z` suffix. |
| 400 | `reserved_metadata_key` | `metadata` names `migration_ref` or `proof_line`, even with `null`. |
| 400 | `metadata_too_large` | The merged `metadata` holds more than 20 keys or more than 8,192 bytes. |
| 400 | `billing_anchor_too_far` | `billing_anchor_at` is more than 31 days after the call. |
| 404 | `not_found` | No line has this id, or you do not own the line. |
| 409 | `billing_anchor_change_conflict` | The line changed owner during the call. Nothing was written. |

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


## OpenAPI

````yaml PATCH /v1/lines/{id}
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}:
    patch:
      summary: Update the metadata or billing anchor of a line
      description: >-
        Updates the `metadata` of a line you own, its billing anchor, or both.
        Accepts an API key. Works on a line of any kind, in any status. The body
        must contain `metadata`, `billing_anchor_at`, or both (`400
        no_fields_to_patch` otherwise).


        `metadata` is merged into the stored object: a key with a value is set,
        a key with `null` is removed, a key you do not send is kept.
        `billing_anchor_at` states the billing anchor of the line, and `null`
        clears it. When both fields are sent, `billing_anchor_at` is applied
        first and `metadata` second, as two separate changes: when `metadata` is
        refused, the billing anchor change is kept. When the line changes owner
        during the request, it returns `409 billing_anchor_change_conflict`.


        A line-scoped key is not limited to its list on this operation: it can
        update any line of its account. This operation sends no event.
      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/PatchLine'
      responses:
        '200':
          description: The updated 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, or `metadata` exceeds 20 keys or 8,192 bytes.
            `no_fields_to_patch`: the body has neither `metadata` nor
            `billing_anchor_at`. `reserved_metadata_key`: `metadata` names
            `migration_ref` or `proof_line`. `metadata_too_large`: the merged
            `metadata` exceeds 20 keys or 8,192 bytes. `billing_anchor_too_far`:
            `billing_anchor_at` is more than 31 days after the request.
          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`.
          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'
        '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:
    PatchLine:
      type: object
      properties:
        metadata:
          type: object
          additionalProperties:
            nullable: true
          description: >-
            Keys to merge into the `metadata` of the line: a key with a value is
            set, a key with `null` is removed, a key you do not send is kept.
            The result holds at most 20 keys and 8,192 bytes once serialized as
            JSON (`400 metadata_too_large`); a request that only removes keys is
            never refused for the size of the result. The keys `migration_ref`
            and `proof_line` are reserved (`400 reserved_metadata_key`). Never
            put personal data or secrets in it.
          example:
            crm_account_id: acc_123
        billing_anchor_at:
          type: string
          nullable: true
          format: date-time
          description: >-
            The billing anchor of the line: an ISO 8601 timestamp in UTC, with a
            `Z` suffix, or `null` to clear it. A line billed on the monthly
            wholesale invoice is charged one full month at each monthly
            occurrence of this timestamp: the same day of the month, moved to
            the last day of a shorter month, at the same time of day. Without a
            billing anchor, the line is billed at each monthly occurrence of its
            activation. A change applies from the next occurrence and does not
            alter a month already billed. A past timestamp is accepted. A
            timestamp more than 31 days after the request returns `400
            billing_anchor_too_far`. Sending the value the line already has
            changes nothing.
          example: '2026-09-11T09:15:00Z'
      description: Send `metadata`, `billing_anchor_at`, or both.
    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.