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

# Create a webhook subscription

> Creates a subscription for a URL and an event filter, and returns its signing secret once.

* **Credential:** an account API key. A trial key returns `401 unauthorized`.
* **Line-scoped keys:** the list of a [line-scoped key](/authentication#line-scoped-keys) is not
  applied here: the key can subscribe to any line of the account, or to all lines.
* **Test keys:** a subscription created with an `isnap_test_` account key is a real subscription of
  the account, and it receives the events of every key: see [Test keys](/authentication#test-keys).
* **`Idempotency-Key`:** not honored. The same request sent twice creates two subscriptions, and each
  event is then delivered twice to the URL. See
  [Operations that honor the key](/idempotency#operations-that-honor-the-key).
* **Events:** none. No event announces a new subscription: see
  [What events do not announce](/webhooks/event-catalog#what-events-do-not-announce).
* **Secret:** the response contains the signing secret: do not log it. This operation and
  [`POST /v1/webhooks/{id}/rotate-secret`](/api-reference/webhooks/rotate-webhook-secret) are the
  only ones that return it.
* **Guide:** [Subscribing](/webhooks/overview#subscribing) explains the URL rules, the event filter
  and the line filter.

## Errors

| Status | `error.code` | When |
| - | - | - |
| 400 | `validation_failed` | `url` is missing or is not a URL. `events` is missing, is empty, or holds a name that is not an event type. `line_ids` is `null`, or one of its entries does not have the form of a line id. `error.issues` names each field. |
| 400 | `webhook_url_not_allowed` | The URL is refused. `error.issues[0].code` is `invalid_url`, `not_https`, `ssrf_blocked` or `dns_failed`: see [URL rules](/webhooks/overview#url-rules). |
| 500 | `internal_error` | The URL is longer than 2,048 characters. Shorten it. |

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


## OpenAPI

````yaml POST /v1/webhooks
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/webhooks:
    post:
      summary: Create a webhook subscription
      description: >-
        Creates a webhook subscription: iSnap sends the events that match
        `events` and `line_ids` to `url`, as an HTTPS `POST` with a JSON body.
        Accepts an API key; a line-scoped key is not restricted here and can
        subscribe to every line of the account. The signing secret is returned
        in this response only. The operation is not idempotent: the same request
        sent twice creates two subscriptions, and each event is then delivered
        twice to the URL. There is no limit on the number of subscriptions.
        `description` and `active` are ignored on create: set them with `PATCH
        /v1/webhooks/{id}`. Creating a subscription sends no event.


        After 10 consecutive deliveries that used all their attempts, iSnap
        suspends the subscription: it then sends a signed `webhook.test` request
        at most once every 15 minutes, and a response from `200` to `299` within
        10 seconds resumes the subscription. A suspended subscription still
        reads `active: true`, and no operation resumes it. Events that occur
        while a subscription is suspended or inactive are not recorded for it
        and are not delivered later. The URL is checked again before every
        attempt and every probe: when its host then resolves to a non-public
        address, no request is sent and the subscription is suspended
        immediately.


        A delivery succeeds when the URL returns a status from `200` to `299`
        within 10 seconds; redirects are not followed and `Retry-After` is
        ignored. Any other outcome is a failed attempt. A retry schedule has at
        most 7 attempts: the first immediately, then 30 seconds, 1 minute, 5
        minutes, 30 minutes, 2 hours and 12 hours after the previous attempt,
        which adds up to 14 hours 36 minutes 30 seconds. A delivery that iSnap
        resends after a suspension or a stall starts a new schedule. A `410`
        response is not retried and sets `active` to `false`. Delivery order is
        not guaranteed, and an event can be delivered more than once:
        deduplicate on `event_id`.


        Each request is signed according to Standard Webhooks, with three
        headers: `webhook-id` (the `event_id` of the event, the same on every
        attempt and on a replay), `webhook-timestamp` (the time the attempt is
        sent, in Unix seconds) and `webhook-signature` (`v1,` followed by the
        base64 HMAC-SHA256 of `<webhook-id>.<webhook-timestamp>.<body>`,
        computed on the body exactly as sent). The headers `X-iSnap-Event` (the
        event type), `X-iSnap-Delivery-Id` (the delivery id) and
        `X-iSnap-Attempt` (the attempt number, from 1) are informational and not
        signed. `X-iSnap-Webhook-Id` holds an opaque value that is not the `id`
        of the subscription.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                  description: >-
                    Public `https://` URL that receives the deliveries, at most
                    2,048 characters: a longer URL returns `500 internal_error`.
                    A URL with another scheme, or whose host is or resolves to a
                    non-public address, or does not resolve, is refused with
                    `400 webhook_url_not_allowed`.
                  example: https://hooks.your-domain.example/isnap
                events:
                  type: array
                  items:
                    $ref: '#/components/schemas/WebhookEventName'
                  minItems: 1
                  description: >-
                    Event types to receive: exact names such as
                    `message.received`, family wildcards such as `line.*`, or
                    `*` for every event. At least one entry: there is no
                    default. An exact name that is not an event type returns
                    `400 validation_failed`. Not every `line.*` event is sent
                    for every kind of line: `line.degraded` and
                    `line.capability_changed` are sent for BYOD lines only, and
                    an outage shorter than 15 minutes on a line that is not a
                    BYOD line sends neither `line.offline` nor `line.connected`.
                  example:
                    - message.received
                    - line.*
                line_ids:
                  type: array
                  items:
                    type: string
                    pattern: ^line_[0-9A-Za-z]{25}$
                    description: 'Id of a line: `line_` followed by 25 letters or digits.'
                  description: >-
                    Lines to receive events for. Omitted or empty: every line,
                    and the events tied to no line. With at least one id, the
                    subscription receives only the events tied to one of those
                    lines. The events of a trial are tied to the line the trial
                    runs on: the subscription receives them only if the list
                    contains its `pool_line_id`. Ids are checked for their
                    format only: an id that matches no line is accepted and
                    matches nothing.
                  example:
                    - line_2k9Qp7RtY4mNb8VcXs3Wd6Lf1
              required:
                - url
                - events
      responses:
        '201':
          description: The webhook subscription, with its signing secret.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                    description: Always `true` on a success response.
                  data:
                    $ref: '#/components/schemas/Webhook'
                  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. `error.code` is `validation_failed` when a
            parameter, a header or the body does not match its schema:
            `error.issues` then lists each problem. It is
            `webhook_url_not_allowed` when the URL is refused:
            `error.issues[0].code` then gives the reason, which is `not_https`
            (the scheme is not `https://`), `ssrf_blocked` (the host is, or
            resolves to, a loopback, link-local, private, carrier-grade NAT,
            multicast or reserved address; every address the host resolves to is
            checked), `dns_failed` (the host does not resolve) or `invalid_url`.
          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'
        '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:
    WebhookEventName:
      type: string
      enum:
        - message.queued
        - message.sent
        - message.delivered
        - message.read
        - message.failed
        - message.received
        - message.scheduled
        - message.cancelled
        - message.fallback_triggered
        - reaction.added
        - reaction.received
        - reaction.removed
        - line.connected
        - line.disconnected
        - line.offline
        - line.degraded
        - line.apple_id_flagged
        - line.quota_warning
        - line.quota_exceeded
        - line.capability_changed
        - line.terminated
        - line.variant_changed
        - line.variant_scheduled
        - line.variant_unscheduled
        - typing_indicator.started
        - typing_indicator.stopped
        - trial.linked
        - trial.dormant
        - trial.reactivated
        - trial.bind_conflict
        - trial.revoked
        - binding.released
        - pre_order.fulfilled
        - pre_order.cancelled
        - macos_session.rented
        - macos_session.released
        - webhook.test
        - '*'
        - message.*
        - reaction.*
        - line.*
        - typing_indicator.*
        - trial.*
        - binding.*
        - pre_order.*
        - macos_session.*
        - webhook.*
      description: >-
        An exact event type such as `message.received`, a family wildcard such
        as `line.*`, or `*` for every event. A wildcard for a family that does
        not exist is accepted and matches nothing. `webhook.test` is never
        delivered through an event filter: iSnap sends it only to a suspended
        webhook subscription, to check whether its URL responds again.
    Webhook:
      type: object
      properties:
        id:
          type: string
          pattern: ^wh_[0-9A-Za-z]{25}$
          description: >-
            Id of the webhook subscription: `wh_` followed by 25 letters or
            digits.
          example: wh_3Kp8Rn9Fh2Lm5Qx7Vw1Yz4Bc6
        url:
          type: string
          description: URL that receives the deliveries.
        events:
          nullable: true
          description: >-
            Event filter: an array of exact event types, family wildcards such
            as `line.*`, or `*`. Never `null`.
        line_ids:
          nullable: true
          description: >-
            Line filter: an array of line ids, or `null`. `null` and an empty
            array both mean every line.
        active:
          type: boolean
          description: >-
            `false` when you turned the subscription off, or when the URL
            returned `410` to a delivery. A subscription that iSnap suspended
            stays `true`: no field reports a suspension.
        failure_count:
          type: integer
          minimum: 0
          description: >-
            Number of consecutive deliveries that used all their attempts
            without a response from `200` to `299`. A successful delivery, or a
            successful `webhook.test` request, resets it to 0. iSnap suspends
            the subscription when it reaches 10.
        last_success_at:
          type: string
          nullable: true
          description: >-
            Timestamp of the last response from `200` to `299`, to a delivery or
            to a `webhook.test` request. `null` until the first one.
        last_failure_at:
          type: string
          nullable: true
          description: >-
            Timestamp of the last delivery that used all its attempts, of the
            last `410` response, or of the last delivery that was not sent
            because the URL no longer passed the URL checks. `null` until the
            first one.
        created_at:
          type: string
          description: Timestamp of creation, ISO 8601 in UTC.
        secret:
          type: string
          description: >-
            Signing secret: `whsec_` followed by 64 lowercase hexadecimal
            characters. Present in the response of `POST /v1/webhooks` only.
            Pass it as it is to a Standard Webhooks library. The HMAC key is the
            base64 decoding of the part after `whsec_`, not its hexadecimal
            decoding.
      required:
        - id
        - url
        - active
        - failure_count
        - last_success_at
        - last_failure_at
        - created_at
      description: >-
        A webhook subscription, in the shape that `POST /v1/webhooks` and `GET
        /v1/webhooks` return.
    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.