openapi: 3.0.3
info:
  title: Incognito API
  version: 1.13.2
  contact:
    name: Apifonica Support
    email: support@apifonica.com
  x-logo:
    url: 'https://apifonica.com/public/img/logo.svg'
    altText: Apifonica logo
  description: |
    The Incognito Service API provides inbound telephony services on top of the
    Apifonica platform: call masking (number anonymization), group masking and
    extension-based call routing.

    # Usage

    ## Preconditions

    You have to be a registered Apifonica user to use this service:

    1. Register an Apifonica account (https://apifonica.com/)
    2. Get `account_sid` and `account_token` in your personal account profile
       (https://account.apifonica.com/profile)
    3. A pool of virtual numbers is required. Buy a number using the API
       (https://docs.apifonica.com/platform#operation/rentPhoneNumber) or in
       your personal account (https://account.apifonica.com/numbers/buy/)
    4. Create a pool of numbers resource and add numbers to it: use the API
       (https://docs.apifonica.com/platform#tag/Pools) or your personal account
       (https://apifonica.com)

    ## User flow

    1. Create a service (Service API)
    2. Create a binding between a real phone number and a masking one
       (Behavior API)
    3. Make a call to the masking number and get a call to your real phone
       number

    ## Authentication

    HTTP Basic (`account_sid:account_token`):

    * `Username` - equal to your `account_sid` (e.g., acc89ddf772-5df2-41d7-b257-000000000000)
    * `Password` - equal to your `token` (e.g., auta42e0580-8138-308a-a354-000000000000)

    ## Request tracing

    Any request may carry an optional `X-Trace-ID` header with an identifier
    of your choice (an order id, a retry counter, a UUID). We write it to our
    logs next to that request, so when you report a problem we can find
    exactly your call by that value instead of guessing by time and number.
    Without the header we still generate our own request id, but you have no
    way to name it. The header changes nothing in the response.

    ```
    curl -H "X-Trace-ID: order-4815-attempt-1" \
      -u "acc...:aut..." https://api-incognito.apifonica.com/service
    ```

    ## Formats

    - Phone numbers: E.164 without the plus sign (`37122200001`)
    - Timestamps: RFC 3339 (`2026-08-30T10:00:00Z`)
    - Resource SIDs: UUID; account SIDs are prefixed with `acc`, tokens with
      `aut`, pool SIDs with `poo`

    ## Pagination

    The call list endpoints ([`GET /service/{sid}/call`](#operation/listServiceCalls),
    [`GET /account/{sid}/call`](#operation/listAccountCalls),
    [`GET /service/{sid}/behavior/{sid}/call`](#operation/listBehaviorCalls))
    return items page by page, newest first. Both parameters are optional:
    without them the endpoint returns the complete list.

    ### Request query parameters

    - **`limit`** (int, optional): number of items to return. `0` is
      treated as no limit - the same answer as without the parameter.
    - **`last_id`** (string, optional): `call_sid` of the last item of the
      previous page. Omit it to get the **first page**, then pass the value
      of the `X-Last-ID` response header to get the next one. An unknown or
      expired `last_id` answers `400` - restart from the first page or
      narrow the date window.

    ### Response format

    The body is a plain array of call records. Pagination details come in
    the response headers:

    - **`X-Total-Count`** - total number of calls matching your filters. It
      ignores `last_id`, so it stays the same while you page through the
      list.
    - **`X-Last-ID`** - `call_sid` of the last returned item, to be used as
      `last_id` for the next page.

    The status code tells you what you got:

    - **`200 OK`** - the page holds every call matching your filters.
    - **`206 Partial Content`** - your filters match more calls than the
      page holds. Note that the last non-empty page of a walk also comes
      back as `206`.
    - **`204 No Content`** - nothing to return: no call matches your
      filters or you reached the end of the list. There is no body.

    ### Counting with HEAD

    All three endpoints also answer `HEAD` requests - the same filters, no
    body - when you only need the counters. `limit` still applies and tells
    you whether the calls fit into one page:

    - **`200 OK`** - every matching call fits into one page of `limit`
      (or there is no limit).
    - **`206 Partial Content`** - your filters match more calls than
      `limit`.
    - **`204 No Content`** - no call matches your filters. No
      `X-Total-Count` header in this case.

    The total comes in `X-Total-Count`. A `HEAD` response carries no
    `X-Last-ID`, and `last_id` has no effect on it.

    ```
    HEAD /service/{sid}/call?started_from=...&started_to=...&limit=50

    HTTP/1.1 206 Partial Content
    X-Total-Count: 1284
    ```

    ## Partial updates (JSON Patch)

    PATCH takes an RFC 6902 JSON Patch with
    `Content-Type: application/json-patch+json`: always an ARRAY of
    operations, `path` is a JSON Pointer into the resource.

    - `add` sets a missing field, `replace` overwrites an existing one,
      `remove` deletes it:
      `[{"op": "replace", "path": "/status", "value": "inactive"}]`
    - On an existing field `add` overwrites instead of merging, so `add` on
      `/specification` replaces the whole object
    - In arrays (filter `rules`) the last segment is the position: `-`
      appends, a number targets one element (`/specification/rules/0`)
    - A failed operation saves nothing: the resource stays as it was and the
      API answers 400

    # Resources

    The API is built around four resources. Every schema is documented inline
    in the operation that uses it; type-specific variants are selectable in
    the schema view by their names.

    ## Service

    The root resource, bound to an Apifonica number pool (one pool hosts one
    service). Its `type` defines the call flow:

    | Service type | Behavior type | Purpose |
    |---|---|---|
    | `IncognitoSimple` | `masquerade` | 1-to-1 masking: each behavior binds one private (masked) number to one public number from the pool. Different pool numbers can serve different private numbers. |
    | `IncognitoSimpleAutoAssign` | `masqueradeAutoAssign` | Same masking flow, but the public number is assigned automatically from the service pool when the behavior is created. |
    | `GroupIncognito` | `GroupMasquerade` | A group of two members with mutual masking (double incognito): while the binding is active, the members call each other's public numbers and never see the real ones. Only group members can reach each other. |
    | `IncognitoWithExtension` | `extension` | Extension-based routing: callers dial the service number and enter an extension; the service maintains a virtual pool of generated extensions and connects the caller to the bound private number. |
    | `IncognitoTwoWayDynamic` | `masqueradeTwoWayDynamic` | Two-way masking with dynamic callbacks: a caller of the public number gets a per-pair callback number pinned for the callback window, so both sides can call each other back without seeing real numbers. The public number of a behavior can be set manually or picked automatically. |

    The list is extendable: new call flows are added as new service types.
    Full specifications of every type with request examples:
    [Create service](#operation/createService).

    ## Behavior

    A routing rule inside a concrete service: a number pair, a group of
    members or an extension binding. The behavior `type` must match the
    service type (see the table above). A behavior can be time-limited via
    `valid_for` and auto-removed when expired. Full specifications of every
    type with request examples: [Create behavior](#operation/createBehavior).

    ## Filter

    A black/white list checked before the main routing logic; on match the
    filter `action` is executed instead of the normal flow. Filters attach to
    the whole service or to a single behavior. Rules to know:

    - A rule `mask` works as a PREFIX of the caller number: a full number
      matches one caller, a short mask matches a range or a country
      (`372` = all numbers of Estonia). "Allow only one country" = a white
      list with the country prefix.
    - With several filters, they are checked by `weight` in ascending order;
      the first match wins. Filters with the same weight are checked in
      creation order.
    - Filters run BEFORE the callback logic (callback_to_last_caller): a
      blocked number cannot get through with a callback either. The block
      does not stop other numbers from calling the blocked one via callback.
    - Changes apply immediately for new incoming calls. Calls active at the
      moment of change finish with the old configuration.

    Full specification:
    [Create service filter](#operation/createServiceFilter).

    ## Call

    A stored call record, kept when the service has `save_calls: true`. This
    is the call tracking source for your business: every masked communication
    is captured with the original caller, the public/masked pair that served
    it, start/end time and duration, whether it was a direct call or a
    callback, and the `record_sid` to fetch the call recording. Use it to
    measure channel effectiveness, reconcile calls with orders and audit
    conversations. Retention is environment specific. Record fields:
    [List service calls](#operation/listServiceCalls).

    The extended call tracking (`call_tracking: extended` on the service)
    turns the record into a full call report, available right after the call
    ends:

    - when the called party answered and how long the parties talked
      (`answered`, `talk_duration`, `time_to_answer`, `caller_wait`);
    - how the dialed leg finished (`second_leg_status`) and, where the
      carrier reports ringing, the dial-quality metrics (`pdd_ms`,
      `ring_duration`);
    - the live status of the call recording (`record_status`,
      `record_scope`) next to the `record_sid`;
    - with `save_rejected_calls: true` - also the calls that were NOT
      connected (`connected: false` with a `reject_reason`), e.g. a callback
      that missed its window, annotated with the pair it belonged to.

    The statistics block of an extended record has a stable schema: every
    field is always present, `null` until it has a value, `0` is a measured
    value - a typed client reads it without checking for missing keys (see
    the `Call` schema).

    Both settings are opt-ins on the service (create it with them or switch
    an existing one with PUT/PATCH); without them the record keeps the base
    shape above. Lists page by a cursor - see "Pagination" under Usage.

    # Call control

    ## Action

    A call control instruction the platform executes instead of the normal
    flow. Used as the service `default_action` (no behavior matched the
    call), as a filter `action` (caller hit a black/white list) and in
    `second_leg_actions` (busy / no answer).

    `default_action` may be omitted on service create and update: an explicit
    `EndCall` is stored in its place, so an unmatched call is always dropped
    gracefully. Types:

    - `PlayAudio` - play an audio file by URL
    - `Say` - synthesize speech from text (TTS)
    - `EndCall` - drop the call

    For `PlayAudio` and `Say` the `loops` field controls repetition:
    `0` (default) - infinite, `1` - play once.

    ## Media types

    Media played to a call side, used in `whisper_a`/`whisper_b` (message
    played to a side on connect), `premedia` (played to the caller while the
    second leg is dialing) and greeting messages of extension services:

    - `audio` - play a file by URL
    - `tone` - play a tone signal
    - `say` - synthesize speech from text (TTS engine/voice/language)

    # Errors

    ## Format

    Every error responds with the same `ApiError` structure: `code`,
    `reason`, `message`, `type` and optional `details`.

    ## Codes

    | Code | HTTP | Reason | What it means for the client |
    |------|------|--------|------------------------------|
    | ERR001 | 400 | Bad request | The request body is malformed or misses required fields; fix the request and retry |
    | ERR002 | 400 | SID malformed | The SID in the URL is not a valid UUID |
    | ERR003 | 500 | Runtime error | Unexpected internal error; retry, contact support if it repeats |
    | ERR004 | 400 | Content type must be compliant for PATCH | PATCH requires `Content-Type: application/json-patch+json` |
    | ERR005 | 400 | Error while applying patch+json | A patch operation failed (bad `op` or `path`); nothing was saved |
    | ERR011 | 400 | Resource validation failed | Field values are invalid; `message` lists the fields |
    | ERR012 | 404 | Resource not found | No resource with this SID (or it was auto-removed) |
    | ERR013 | 409 | Resource conflict | Another resource already uses the same pool, public number or extension; `details` names it |
    | ERR021 | 500 | Runtime error while storing resource | Storage write failed; the resource was not saved, retry |
    | ERR022 | 502 | A platform error occurred while setting up the resource | The Apifonica platform rejected the operation; `message` carries the platform answer |
    | ERR201 | 403 | API user is not authorized | Missing or wrong Basic Auth credentials |
    | ERR202 | 502 | Fail to auth the user | The auth backend did not answer; retry later |
    | ERR203 | 403 | Resource permission denied | The resource belongs to another account, or the operation is not allowed for this service type |
    | ERR301 | 400 | Out of extensions | The virtual extension pool has no free numbers; enlarge `dimension` or remove old extensions |
    | ERR302 | 400 | Pool out of numbers | The Apifonica number pool has no free numbers to assign; add numbers to the pool |

servers:
  - url: 'https://api-incognito.apifonica.com'

security:
  - basicAuth: []

tags:
  - name: Service
    description: >
      Service management. A service is bound to an Apifonica number pool
      (one pool - one service) and defines the inbound call flow type
      (see "Service types" above).
  - name: Behavior
    description: >
      Behavior management. Behaviors are the routing rules of a concrete
      service: number pairs, groups or extensions. The behavior type must
      match the parent service type. A behavior can be time-limited via
      `valid_for` and auto-removed when expired.
  - name: Filter
    description: >
      Black/white list filters. Checked before the main routing logic (and
      before the callback logic); on match the filter `action` is executed
      instead of the normal flow. Filters attach to a service or to a single
      behavior. Checked by `weight` in ascending order, first match wins;
      equal weights are checked in creation order. Changes apply immediately
      for new calls.
  - name: Call
    description: >
      Stored call records (services with save_calls = true) - the call
      tracking source: caller, public/masked pair, timing, duration,
      direct/callback flag and the recording SID. The extended call tracking
      (call_tracking = extended) adds the full real-time statistics (answer
      and talk times, leg outcome, live recording status) and - with
      save_rejected_calls - the rejected calls with their reasons. The lists
      page by a cursor (see "Pagination").
  - name: Pool
    description: |
      Apifonica number pool: its content, sync state and activity. Describes
      pools of REAL platform numbers only - the virtual extension pool of an
      `IncognitoWithExtension` service is a different entity and is reported
      in the service specification.

      Pool-scoped: requires the credentials of the account owning the pool.
      Ownership does NOT require a service on the pool, so a pool a service
      has already moved away from stays readable - that is how you decide
      whether an old pool can be released.

      `GET /pool` lists every pool the service can attribute to your account
      in one read-only view: the pools with a service, the orphaned ones,
      the pools deleted on the platform (`sync_status: pool_deleted`) and
      the pools your services reference that were never synced
      (`sync_status: never_synced`). The list is served from the service
      storage and refreshes stale replicas in the background; it pages by a
      `pool_sid` cursor (see "Pagination"). Managing the pools
      themselves (creating, changing numbers, deleting) stays in the
      Apifonica Platform API.

      Activity counters (`behaviors`, `calls`) cover objects created since
      release 1.11.0, when the pool started being stamped on them. For a pool
      that existed before, zero counters mean "no stamped data" rather than
      "no activity" until a full call retention window has passed;
      `calls.first_call_at` shows since when the data is complete.

      Pool content is cached in the service memory after the first pool usage
      (cache TTL is a service setting, 5 minutes by default) and replicated to
      the service storage. After changing numbers in the pool on the Apifonica
      platform, reset the cache with `DELETE /pool/{pool_sid}/cache` so the
      changes are picked up immediately (otherwise they apply after the cache
      TTL expires).
paths:
  /service:
    post:
      tags: [Service]
      summary: Create service
      operationId: createService
      description: >
        Creates a service on a free Apifonica number pool. A pool can host
        only one service (409 on conflict). Pool existence is validated
        against the Apifonica platform.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ServiceCreateRequest' }
            examples:
              IncognitoSimple:
                summary: IncognitoSimple (1-to-1 masking)
                value:
                  type: IncognitoSimple
                  description: Delivery calls masking
                  status: active
                  pool_sid: 'poo00000-0000-0000-0000-000000000000'
                  save_calls: true
                  specification:
                    number_forward: false
                    timeout: 30
                  default_action:
                    type: PlayAudio
                    specification:
                      url: 'https://xml.apifonica.com/controller/voicing/audio/no_redirection.wav'
                      loops: 1
              IncognitoSimpleAutoAssign:
                summary: IncognitoSimpleAutoAssign (public number from the pool)
                value:
                  type: IncognitoSimpleAutoAssign
                  description: Auto-assigned masking
                  status: active
                  pool_sid: 'poo00000-0000-0000-0000-000000000000'
                  save_calls: true
                  specification:
                    number_forward: false
                    timeout: 30
                  default_action:
                    type: EndCall
                    specification: {}
              GroupIncognito:
                summary: GroupIncognito (mutual masking of two members)
                value:
                  type: GroupIncognito
                  description: Driver-passenger group masking
                  status: active
                  pool_sid: 'poo00000-0000-0000-0000-000000000000'
                  save_calls: true
                  specification:
                    number_forward: false
                    timeout: 25
                    whisper_a:
                      type: say
                      value: You are connected through a masked line
                      tts: { engine: polly, voice: Brian, language: en-US }
                  default_action:
                    type: Say
                    specification:
                      text: The number is not connected to a group
                      language: en-US
                      voice: Brian
                      loops: 1
              IncognitoWithExtension:
                summary: IncognitoWithExtension (extension routing)
                value:
                  type: IncognitoWithExtension
                  description: Extension based routing
                  status: active
                  pool_sid: 'poo00000-0000-0000-0000-000000000000'
                  save_calls: true
                  specification:
                    number_forward: false
                    timeout: 30
                    greeting_message:
                      type: say
                      value: Enter the extension number
                      tts: { engine: polly, voice: Brian, language: en-US }
                    wrong_extension_message:
                      type: say
                      value: Wrong extension, try again
                      tts: { engine: polly, voice: Brian, language: en-US }
                    extension_pool:
                      dimension: 4
                      skip_repeating_numbers: true
                  default_action:
                    type: EndCall
                    specification: {}
              IncognitoTwoWayDynamic:
                summary: IncognitoTwoWayDynamic (two-way dynamic callbacks)
                value:
                  type: IncognitoTwoWayDynamic
                  description: Two-way masking with callbacks
                  status: active
                  pool_sid: 'poo00000-0000-0000-0000-000000000000'
                  save_calls: true
                  specification:
                    timeout: 60
                    callback_lifetime_seconds: 21600
                    callback_max_numbers: 20
                    callback_pool_sid: 'poo00000-0000-0000-0000-000000000000'
                  default_action:
                    type: EndCall
                    specification: {}
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '502': { $ref: '#/components/responses/PlatformError' }
    get:
      tags: [Service]
      summary: List services
      operationId: listServices
      description: Returns services owned by the authenticated account.
      parameters:
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum: [IncognitoSimple, GroupIncognito, IncognitoWithExtension, IncognitoSimpleAutoAssign, IncognitoTwoWayDynamic]
          description: Filter by service type.
      responses:
        '200':
          description: List of services
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ServiceResponse' }
        '204': { description: The account has no services }
        '403': { $ref: '#/components/responses/Forbidden' }

  /service/{service_sid}:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
    get:
      tags: [Service]
      summary: Get service
      operationId: getService
      responses:
        '200':
          description: Service
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceResponse' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Service]
      summary: Update service
      operationId: updateService
      description: >
        Replaces `specification`, `status`, `description` and `default_action`;
        other fields (`type`, `pool_sid`) are ignored.
        Not supported for `IncognitoSimpleAutoAssign` (403).
        For `IncognitoTwoWayDynamic` the specification is re-validated (400 on
        a violation) and a changed `callback_pool_sid` is verified and bound:
        404 - the pool does not exist, 409 - it already serves another
        service, 502 - the platform rejected the binding.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ServiceUpdateRequest' }
      responses:
        '200':
          description: Updated service
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '502': { $ref: '#/components/responses/PlatformError' }
    patch:
      tags: [Service]
      summary: Partially update service (JSON Patch)
      operationId: patchService
      description: >
        RFC 6902 JSON Patch, `Content-Type: application/json-patch+json`.
        Not supported for `IncognitoSimpleAutoAssign` (403).
        Optional fields that are absent from the resource (for example the
        `callback_*` family) are enabled with an `add` operation.
        For `IncognitoTwoWayDynamic` the patched specification is re-validated
        (400 on a violation - the window and cap bounds hold on PATCH) and a
        changed `callback_pool_sid` is verified and bound: 404 - the pool does
        not exist, 409 - it already serves another service, 502 - the platform
        rejected the binding.
      requestBody:
        required: true
        content:
          application/json-patch+json:
            schema: { $ref: '#/components/schemas/JsonPatch' }
            examples:
              enableCallback:
                summary: Enable callback to the last caller
                value:
                  - { op: add, path: /specification/callback_to_last_caller, value: true }
                  - { op: add, path: /specification/callback_lifetime_seconds, value: 86400 }
              enableExtendedTracking:
                summary: Switch the service to the extended call tracking
                value:
                  - { op: add, path: /call_tracking, value: extended }
                  - { op: add, path: /save_rejected_calls, value: true }
      responses:
        '200':
          description: Updated service
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '502': { $ref: '#/components/responses/PlatformError' }
    delete:
      tags: [Service]
      summary: Delete service
      operationId: deleteService
      description: >
        Deletes the service and asynchronously cleans up its behaviors,
        filters, calls and extension pool.
      responses:
        '204': { description: Deleted }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /service/{service_sid}/pool:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
    put:
      tags: [Service]
      summary: Move the service to another pool
      operationId: updateServicePool
      description: |
        Binds the service to another Apifonica number pool.

        The flow it serves: the current pool has no free numbers left, so you
        buy a fresh pool and move the service to it. New behaviors are then
        created on the new pool, while the behaviors of the old pool keep
        working until they expire - calls to their numbers still reach this
        service. Watch the old pool with
        [Get the pool](#operation/getPool) and release it when its activity
        is gone.

        Only `pool_sid` changes: the service specification is untouched. Not
        available for `IncognitoSimpleAutoAssign` services, whose public
        numbers are leased from the current pool.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pool_sid]
              properties:
                pool_sid:
                  type: string
                  description: Apifonica pool SID (poo...) to move the service to.
            example:
              pool_sid: poo9448ca5a-0000-0000-0000-000000000000
      responses:
        '200':
          description: The service after the move
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServiceResponse' }
        '400':
          description: Malformed pool_sid
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR011
                reason: Resource validation failed
                message: 'pool_sid: resource SID is incorrect.'
                type: Service
        '403':
          description: >
            No permission to the service, or the service type does not
            support moving the pool.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR203
                reason: Resource permission denied
                message: 'pool change is not supported for the `IncognitoSimpleAutoAssign` service type: its leased public numbers belong to the current pool'
                type: Service
        '404':
          description: The service or the target pool does not exist
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR012
                reason: Resource not found
                message: pool poo9448ca5a-0000-0000-0000-000000000000 not found
                type: Service
        '409':
          description: >
            The target pool is already used by another service (one pool
            hosts one service). `details[].data.service_sid` names it.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR013
                reason: Resource conflict
                message: pool_sid poo9448ca5a-0000-0000-0000-000000000000 is already used by the service 3f2b1c40-0000-0000-0000-000000000000
                type: Service
                details:
                  - type: ConflictInfo
                    data:
                      service_sid: 3f2b1c40-0000-0000-0000-000000000000
                      resource: service
        '502':
          description: The platform rejected the pool binding
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR022
                reason: A platform error occurred while setting up the resource
                message: '[pool-repository] response by update is not 200 OK: {"status_code":400,"error_code":10012}'
                type: Service

  /service/{service_sid}/behavior:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
    post:
      tags: [Behavior]
      summary: Create behavior
      operationId: createBehavior
      description: >
        The behavior `type` must match the parent service type (see "Service
        types"). The public number must belong to the service pool (checked
        against the Apifonica platform).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BehaviorCreateRequest' }
            examples:
              masquerade:
                summary: masquerade (service IncognitoSimple)
                value:
                  type: masquerade
                  valid_for:
                    start_date_time: '2026-08-30T10:00:00Z'
                    end_date_time: '2026-09-30T10:00:00Z'
                    auto_remove: true
                  specification:
                    public_number: { number: '37166011111' }
                    masked_number: { number: '37122200001' }
              masqueradeAutoAssign:
                summary: masqueradeAutoAssign (service IncognitoSimpleAutoAssign)
                value:
                  type: masqueradeAutoAssign
                  valid_for:
                    start_date_time: '2026-08-30T10:00:00Z'
                    end_date_time: '2026-09-30T10:00:00Z'
                    auto_remove: true
                  specification:
                    masked_number: { number: '37122200001' }
              GroupMasquerade:
                summary: GroupMasquerade (service GroupIncognito)
                value:
                  type: GroupMasquerade
                  valid_for:
                    start_date_time: '2026-08-30T10:00:00Z'
                    end_date_time: '2026-09-06T10:00:00Z'
                    auto_remove: true
                  specification:
                    member_list:
                      - name: driver
                        masked_number: { number: '37122200001' }
                        public_number: { number: '37166011111' }
                      - name: passenger
                        masked_number: { number: '37122200002' }
                        public_number: { number: '37166011112' }
              extension:
                summary: extension (service IncognitoWithExtension)
                value:
                  type: extension
                  valid_for:
                    start_date_time: '2026-08-30T10:00:00Z'
                    auto_remove: false
                  specification:
                    masked_number: { number: '37122200001' }
              masqueradeTwoWayDynamic:
                summary: masqueradeTwoWayDynamic (service IncognitoTwoWayDynamic)
                value:
                  type: masqueradeTwoWayDynamic
                  valid_for:
                    start_date_time: '2026-08-30T10:00:00Z'
                    end_date_time: '2026-09-30T10:00:00Z'
                    auto_remove: true
                  specification:
                    masked_number: { number: '37122200001' }
                    public_number: { number: '37166011111' }
              masqueradeTwoWayDynamicAutoPick:
                summary: masqueradeTwoWayDynamic with an auto-picked public number
                value:
                  type: masqueradeTwoWayDynamic
                  valid_for:
                    start_date_time: '2026-08-30T10:00:00Z'
                    end_date_time: '2026-09-30T10:00:00Z'
                    auto_remove: true
                  specification:
                    masked_number: { number: '37122200001' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BehaviorResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: >
            The public number (or extension) is already used by another active
            behavior; details contain the conflicting behavior.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR013
                reason: Resource conflict
                message: public_number is already used in the `9c2e0d3a-0000-0000-0000-000000000000` behavior
                type: Behavior
                details:
                  - type: ConflictInfo
                    data:
                      binding_id: 9c2e0d3a-0000-0000-0000-000000000000
                      resource: behavior
    get:
      tags: [Behavior]
      summary: List behaviors
      operationId: listBehaviors
      responses:
        '200':
          description: List of behaviors
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/BehaviorResponse' }
        '204': { description: The service has no behaviors }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /service/{service_sid}/behavior/{behavior_sid}:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
      - $ref: '#/components/parameters/behaviorSid'
    get:
      tags: [Behavior]
      summary: Get behavior
      operationId: getBehavior
      responses:
        '200':
          description: Behavior
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BehaviorResponse' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Behavior]
      summary: Update behavior
      operationId: updateBehavior
      description: >
        Not supported for the `extension` behavior type (400): use PATCH to
        update its fields. Changing the public number to one used by another
        active behavior answers 409 with the conflicting behavior in details,
        the same contract as on create.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BehaviorCreateRequest' }
      responses:
        '200':
          description: Updated behavior
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BehaviorResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
    patch:
      tags: [Behavior]
      summary: Partially update behavior (JSON Patch)
      operationId: patchBehavior
      description: >
        RFC 6902 JSON Patch, `Content-Type: application/json-patch+json`.
        Changing the public number to one used by another active behavior
        answers 409 with the conflicting behavior in details, the same
        contract as on create.
      requestBody:
        required: true
        content:
          application/json-patch+json:
            schema: { $ref: '#/components/schemas/JsonPatch' }
      responses:
        '200':
          description: Updated behavior
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BehaviorResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Behavior]
      summary: Delete behavior
      operationId: deleteBehavior
      responses:
        '204': { description: Deleted }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /service/{service_sid}/filter:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
    post:
      tags: [Filter]
      summary: Create service filter
      operationId: createServiceFilter
      description: >
        Creates a black/white list for the whole service. Filters are checked
        in `weight` order before the main routing logic.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FilterCreateRequest' }
            examples:
              blacklist:
                summary: Black list with a voice message
                value:
                  type: BlackWhiteList
                  weight: 1
                  specification:
                    list_type: black
                    rules:
                      - { mask: '37123881359' }
                    action:
                      type: Say
                      specification:
                        text: You are in the black list
                        language: en-US
                        voice: Brian
                        loops: 1
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FilterResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    get:
      tags: [Filter]
      summary: List service filters
      operationId: listServiceFilters
      responses:
        '200':
          description: List of filters
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/FilterResponse' }
        '204': { description: The service has no filters }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /service/{service_sid}/filter/{filter_sid}:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
      - $ref: '#/components/parameters/filterSid'
    patch:
      tags: [Filter]
      summary: Partially update service filter (JSON Patch)
      operationId: patchServiceFilter
      description: >
        RFC 6902 JSON Patch, `Content-Type: application/json-patch+json`.
        Use it to add or remove single rules without replacing the whole list.
      requestBody:
        required: true
        content:
          application/json-patch+json:
            schema: { $ref: '#/components/schemas/JsonPatch' }
            examples:
              addRule:
                summary: Add a number to the list
                value:
                  - { op: add, path: /specification/rules/-, value: { mask: '37123881360' } }
      responses:
        '200':
          description: Updated filter
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FilterResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Filter]
      summary: Delete service filter
      operationId: deleteServiceFilter
      responses:
        '204': { description: Deleted }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /service/{service_sid}/behavior/{behavior_sid}/filter:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
      - $ref: '#/components/parameters/behaviorSid'
    post:
      tags: [Filter]
      summary: Create behavior filter
      operationId: createBehaviorFilter
      description: Creates a black/white list attached to a single behavior.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FilterCreateRequest' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FilterResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    get:
      tags: [Filter]
      summary: List behavior filters
      operationId: listBehaviorFilters
      responses:
        '200':
          description: List of filters
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/FilterResponse' }
        '204': { description: The behavior has no filters }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /service/{service_sid}/behavior/{behavior_sid}/filter/{filter_sid}:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
      - $ref: '#/components/parameters/behaviorSid'
      - $ref: '#/components/parameters/filterSid'
    get:
      tags: [Filter]
      summary: Get behavior filter
      operationId: getBehaviorFilter
      responses:
        '200':
          description: Filter
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FilterResponse' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Filter]
      summary: Update behavior filter
      operationId: updateBehaviorFilter
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FilterCreateRequest' }
      responses:
        '200':
          description: Updated filter
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FilterResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Filter]
      summary: Delete behavior filter
      operationId: deleteBehaviorFilter
      responses:
        '204': { description: Deleted }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /service/{service_sid}/behavior/{behavior_sid}/call:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
      - $ref: '#/components/parameters/behaviorSid'
    get:
      tags: [Call]
      summary: List behavior calls
      operationId: listBehaviorCalls
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/lastId'
      responses:
        '200':
          description: Call records of the behavior (the complete view)
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
            X-Last-ID: { $ref: '#/components/headers/XLastId' }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Call' }
        '206':
          description: A partial page (see "Pagination")
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
            X-Last-ID: { $ref: '#/components/headers/XLastId' }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Call' }
        '204':
          description: 'Nothing to return: no matching records or the end of the list'
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    head:
      tags: [Call]
      summary: Count behavior calls
      operationId: countBehaviorCalls
      description: >
        The counters of the behavior call list (see "Counting with HEAD"):
        the same filters as the GET, no body.
      parameters:
        - $ref: '#/components/parameters/limit'
      responses:
        '200': { $ref: '#/components/responses/CountFits' }
        '206': { $ref: '#/components/responses/CountExceedsLimit' }
        '204': { $ref: '#/components/responses/CountEmpty' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /service/{service_sid}/call:
    parameters:
      - $ref: '#/components/parameters/serviceSid'
    get:
      tags: [Call]
      summary: List service calls
      operationId: listServiceCalls
      parameters:
        - $ref: '#/components/parameters/startedFrom'
        - $ref: '#/components/parameters/startedTo'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/lastId'
      responses:
        '200':
          description: Call records of the service in the interval (the complete view)
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
            X-Last-ID: { $ref: '#/components/headers/XLastId' }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Call' }
              example:
                - service_sid: 3f2b1c40-0000-0000-0000-000000000000
                  behavior_sid: cb1bd6df-0000-0000-0000-000000000000
                  call_sid: cal401369dd-0000-0000-0000-000000000000
                  callback_pair_sid: cbp91f43a2-0000-0000-0000-000000000000
                  initial_call_sid: cal401369dd-0000-0000-0000-000000000000
                  caller: '998901112233'
                  public_number: '998785552734'
                  masked_number: '998907654321'
                  callback_number: '998785552712'
                  callback: false
                  call_tracking: extended
                  started: '2026-09-01T10:24:12+00:00'
                  ended: '2026-09-01T10:24:57+00:00'
                  duration: 45
                  answered: '2026-09-01T10:24:18+00:00'
                  talk_duration: 39
                  time_to_answer: 6
                  second_leg_status: answered
                  pdd_ms: null
                  ring_duration: null
                  caller_wait: 5
                  connected: true
                  reject_reason: null
                  is_blocked: false
                  blocked_by: null
                  blocked_reason: null
                  record_sid: recf669ad01-0000-0000-0000-000000000000
                  record_status: ready
                  record_scope: full
                - service_sid: 3f2b1c40-0000-0000-0000-000000000000
                  behavior_sid: cb1bd6df-0000-0000-0000-000000000000
                  call_sid: cal7fe7db2b-0000-0000-0000-000000000000
                  callback_pair_sid: cbp91f43a2-0000-0000-0000-000000000000
                  initial_call_sid: cal401369dd-0000-0000-0000-000000000000
                  callback_number: '998785552712'
                  caller: '998907654321'
                  public_number: '998785552712'
                  masked_number: null
                  callback: true
                  call_tracking: extended
                  started: '2026-09-09T11:02:07+00:00'
                  ended: null
                  duration: null
                  answered: null
                  talk_duration: null
                  time_to_answer: null
                  second_leg_status: null
                  pdd_ms: null
                  ring_duration: null
                  caller_wait: null
                  connected: false
                  reject_reason: callback_window_expired
                  is_blocked: false
                  blocked_by: null
                  blocked_reason: null
                  record_sid: null
                  record_status: null
                  record_scope: null
        '206':
          description: A partial page (see "Pagination")
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
            X-Last-ID: { $ref: '#/components/headers/XLastId' }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Call' }
        '204':
          description: 'Nothing to return: no matching records in the interval or the end of the list'
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    head:
      tags: [Call]
      summary: Count service calls
      operationId: countServiceCalls
      description: >
        The counters of the service call list (see "Counting with HEAD"):
        the same interval as the GET, no body.
      parameters:
        - $ref: '#/components/parameters/startedFrom'
        - $ref: '#/components/parameters/startedTo'
        - $ref: '#/components/parameters/limit'
      responses:
        '200': { $ref: '#/components/responses/CountFits' }
        '206': { $ref: '#/components/responses/CountExceedsLimit' }
        '204': { $ref: '#/components/responses/CountEmpty' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /account/{account_sid}/call:
    parameters:
      - name: account_sid
        in: path
        required: true
        schema: { type: string, format: uuid }
        description: Account SID.
    get:
      tags: [Call]
      summary: List account calls
      operationId: listAccountCalls
      parameters:
        - $ref: '#/components/parameters/startedFrom'
        - $ref: '#/components/parameters/startedTo'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/lastId'
      responses:
        '200':
          description: Call records of all account services in the interval (the complete view)
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
            X-Last-ID: { $ref: '#/components/headers/XLastId' }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Call' }
        '206':
          description: A partial page (see "Pagination")
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
            X-Last-ID: { $ref: '#/components/headers/XLastId' }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Call' }
        '204':
          description: 'Nothing to return: no matching records in the interval or the end of the list'
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
    head:
      tags: [Call]
      summary: Count account calls
      operationId: countAccountCalls
      description: >
        The counters of the account call list (see "Counting with HEAD"):
        the same interval as the GET, no body.
      parameters:
        - $ref: '#/components/parameters/startedFrom'
        - $ref: '#/components/parameters/startedTo'
        - $ref: '#/components/parameters/limit'
      responses:
        '200': { $ref: '#/components/responses/CountFits' }
        '206': { $ref: '#/components/responses/CountExceedsLimit' }
        '204': { $ref: '#/components/responses/CountEmpty' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /pool:
    get:
      tags: [Pool]
      summary: List the pools of the account
      operationId: listPools
      description: >
        Every pool the service can attribute to your account, in `pool_sid`
        order, each with the same state and activity as `GET /pool/{pool_sid}`.
        Included: the pools with a service, the orphaned pools (no
        `service_sid`), the pools deleted on the platform
        (`sync_status: pool_deleted`, kept so their remaining activity stays
        visible) and the pools your services reference that have no replica
        yet (`sync_status: never_synced`). The rows come from the service
        storage; like the single pool read, the list refreshes the stale and
        the never-synced replicas of the page in the background (the response
        never waits for the platform), so such a pool shows its real state by
        the next read. Pages by the `pool_sid` cursor: pass the
        `X-Last-ID` header back as `last_id`. Unlike the call lists an unknown
        `last_id` is not an error here - the page starts at the first pool
        above it.
      parameters:
        - name: window_seconds
          in: query
          required: false
          schema: { type: integer, minimum: 1, default: 2592000 }
          description: The period the `calls.count` of every row covers, in seconds (default 30 days).
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/lastId'
      responses:
        '200':
          description: The pools of the account (the complete view)
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
            X-Last-ID: { $ref: '#/components/headers/XLastId' }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Pool' }
        '206':
          description: A partial page (see "Pagination")
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
            X-Last-ID: { $ref: '#/components/headers/XLastId' }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Pool' }
        '204':
          description: 'Nothing to return: the account has no pools or the end of the list'
          headers:
            X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR001
                reason: Bad request
                message: window_seconds must be a positive number of seconds
                type: Pool
        '403': { $ref: '#/components/responses/Forbidden' }
    head:
      tags: [Pool]
      summary: Count the pools of the account
      operationId: countPools
      description: >
        The counters of the pool list (see "Counting with HEAD"): no body.
      parameters:
        - $ref: '#/components/parameters/limit'
      responses:
        '200': { $ref: '#/components/responses/CountFits' }
        '206': { $ref: '#/components/responses/CountExceedsLimit' }
        '204': { $ref: '#/components/responses/CountEmpty' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /pool/{pool_sid}:
    parameters:
      - name: pool_sid
        in: path
        required: true
        schema: { type: string }
        description: Apifonica pool SID (poo...).
    get:
      tags: [Pool]
      summary: Get the pool with its activity
      operationId: getPool
      description: >
        Returns the pool content, the state of its replica in the service
        storage and the activity of the objects living on its numbers:
        behaviors (bindings) and calls. Use it to decide whether a pool is
        still in use - typically an old pool after moving the service to a
        new one.
      parameters:
        - name: window_seconds
          in: query
          required: false
          schema: { type: integer, minimum: 1, default: 2592000 }
          description: >
            The period the call counter covers, in seconds (30 days by
            default). `calls.first_call_at` and `calls.last_call_at` always
            cover the whole retained history regardless of this value.
      responses:
        '200':
          description: Pool
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Pool' }
        '400':
          description: Malformed window_seconds
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR001
                reason: Bad request
                message: window_seconds must be a positive number of seconds
                type: Pool
        '403':
          description: The pool belongs to another account
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR203
                reason: Resource permission denied
                message: You dont have permission to this pool
                type: Pool
        '404':
          description: The pool does not exist on the platform
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR012
                reason: Resource not found
                message: Pool not found
                type: Pool

  /pool/{pool_sid}/cache:
    parameters:
      - name: pool_sid
        in: path
        required: true
        schema: { type: string }
        description: Apifonica pool SID (poo...).
    delete:
      tags: [Pool]
      summary: Reset the pool cache
      operationId: deletePoolCache
      description: >
        Drops the cached pool content and re-reads the pool from the platform,
        refreshing the stored replica as well. Call it after changing numbers
        in the pool so the service picks the changes up immediately instead of
        waiting for the cache TTL.
      responses:
        '204': { description: Cache dropped and the pool re-read }
        '403':
          description: The pool belongs to another account
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR203
                reason: Resource permission denied
                message: You dont have permission to this pool
                type: Pool
        '404':
          description: The pool does not exist on the platform
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiError' }
              example:
                code: ERR012
                reason: Resource not found
                message: Pool not found
                type: Pool

components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: |
        * `Username` - equal to your `account_sid` (e.g., acc89ddf772-5df2-41d7-b257-000000000000)
        * `Password` - equal to your `token` (e.g., auta42e0580-8138-308a-a354-000000000000)

  parameters:
    serviceSid:
      name: service_sid
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Service SID.
    behaviorSid:
      name: behavior_sid
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Behavior SID.
    filterSid:
      name: filter_sid
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Filter SID.
    startedFrom:
      name: started_from
      in: query
      required: false
      schema: { type: string, format: date-time }
      description: 'Interval start (RFC 3339). Default: 30 minutes ago.'
    startedTo:
      name: started_to
      in: query
      required: false
      schema: { type: string, format: date-time }
      description: 'Interval end (RFC 3339). Default: now.'
    limit:
      name: limit
      in: query
      required: false
      schema: { type: integer, minimum: 0 }
      description: >
        Page size (see "Pagination"). `0` is treated as no limit - the same
        answer as without the parameter.
    lastId:
      name: last_id
      in: query
      required: false
      schema: { type: string }
      description: >
        The page cursor (see "Pagination"): the value of the X-Last-ID
        response header of the previous page. On the call lists an unknown
        or expired cursor answers 400; on the pool list it is a plain
        boundary.

  headers:
    XTotalCount:
      description: >
        Total number of items matching your filters. Stays the same while
        you page through the list.
      schema: { type: integer }
    XLastId:
      description: >
        The identifier of the last returned item (`call_sid` on the call
        lists, `pool_sid` on the pool list) - pass it back as `last_id` to
        get the next page.
      schema: { type: string }

  responses:
    CountFits:
      description: Every matching item fits into one page of `limit` (or there is no limit). No body.
      headers:
        X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
    CountExceedsLimit:
      description: Your filters match more items than `limit`. No body.
      headers:
        X-Total-Count: { $ref: '#/components/headers/XTotalCount' }
    CountEmpty:
      description: No item matches your filters. No body and no `X-Total-Count` header.
    BadRequest:
      description: Validation or request error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example:
            code: ERR011
            reason: Resource validation failed
            message: 'specification: cannot be blank; type: cannot be blank.'
            type: Service
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example:
            code: ERR012
            reason: Resource not found
            message: Service not found with given sSid
            type: Service
    Forbidden:
      description: Not authorized or permission denied
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example:
            code: ERR203
            reason: Resource permission denied
            message: You dont have permission to this service
            type: Service
    Conflict:
      description: >
        Resource conflict: the pool (or the callback pool of an
        IncognitoTwoWayDynamic specification) already has a service
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example:
            code: ERR013
            reason: Resource conflict
            message: 'customer service for pool_sid poo9448ca5a-0000-0000-0000-000000000000 already exists https://api-incognito.apifonica.com/service/3f2b1c40-0000-0000-0000-000000000000. Please set another pool'
            type: Service
    PlatformError:
      description: >
        The Apifonica platform rejected the operation (e.g. the pool does
        not exist or the call application update failed).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
          example:
            code: ERR022
            reason: A platform error occurred while setting up the resource
            message: '[pool-repository] pool not found'
            type: Service

  schemas:
    # ------------------------------------------------------------------
    # Service
    # ------------------------------------------------------------------

    ServiceCreateRequest:
      type: object
      required: [type, pool_sid, specification]
      properties:
        type:
          type: string
          enum: [IncognitoSimple, GroupIncognito, IncognitoWithExtension, IncognitoSimpleAutoAssign, IncognitoTwoWayDynamic]
          description: Service type; defines the call flow and the expected specification.
        description: { type: string }
        status: { type: string, enum: [active, inactive], default: active }
        pool_sid:
          type: string
          description: Apifonica number pool SID (poo...). One pool hosts one service.
        save_calls:
          type: boolean
          description: Store call records (see the Call resource).
        call_tracking:
          type: string
          enum: [base, extended]
          default: base
          description: |
            The level of the call tracking:
            - `base` (default) - the core call record: the numbers, the
              timing, the duration and the recording SID;
            - `extended` - the full real-time call report on top: the answer
              and talk times, the outcome of the dialed leg, the dial
              metrics and the live recording status (see the Call resource).
            The mode can be changed on an existing service with PUT or
            PATCH; it applies to the calls made after the change.
        save_rejected_calls:
          type: boolean
          default: false
          description: >
            Also store the calls that were NOT connected (`connected: false`
            with a `reject_reason`). Off by default: the list then contains
            connected calls only.
        specification:
          anyOf:
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoSimple'
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoSimpleAutoAssign'
            - $ref: '#/components/schemas/ServiceSpecificationGroupIncognito'
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoWithExtension'
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoTwoWayDynamic'
          description: Specification matching the service `type`.
        default_action:
          allOf:
            - $ref: '#/components/schemas/Action'
          description: >
            Optional. When omitted, an explicit `EndCall` action is stored:
            a call that matched no behavior is dropped gracefully. An unknown
            action `type` is rejected with 400.

    ServiceUpdateRequest:
      type: object
      description: >
        Fields replaced by PUT; other fields are ignored. NB: PUT is a full
        replace of these fields - omitting call_tracking or
        save_rejected_calls resets them to the defaults (and a resulting
        tracking mode change resubscribes the pool applications).
      properties:
        status: { type: string, enum: [active, inactive] }
        description: { type: string }
        call_tracking:
          type: string
          enum: [base, extended]
          description: See ServiceCreateRequest.
        save_rejected_calls:
          type: boolean
          description: See ServiceCreateRequest.
        specification:
          anyOf:
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoSimple'
            - $ref: '#/components/schemas/ServiceSpecificationGroupIncognito'
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoWithExtension'
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoTwoWayDynamic'
        default_action:
          allOf:
            - $ref: '#/components/schemas/Action'
          description: >
            Optional. When omitted, an explicit `EndCall` action is stored.
            An unknown action `type` is rejected with 400.

    ServiceResponse:
      type: object
      properties:
        href: { type: string, format: uri }
        service_sid: { type: string, format: uuid }
        type:
          type: string
          enum: [IncognitoSimple, GroupIncognito, IncognitoWithExtension, IncognitoSimpleAutoAssign, IncognitoTwoWayDynamic]
        status: { type: string, enum: [active, inactive] }
        description: { type: string }
        pool_sid: { type: string }
        event_controller:
          type: object
          description: Webhook endpoint the Apifonica platform calls for this service.
          properties:
            url: { type: string, format: uri }
        owner:
          type: object
          properties:
            acc_sid: { type: string }
        save_calls: { type: boolean }
        call_tracking:
          type: string
          enum: [base, extended]
          description: The effective call tracking level.
        save_rejected_calls:
          type: boolean
          description: The EFFECTIVE rejected-calls setting.
        creation_time: { type: string, format: date-time }
        update_time: { type: string, format: date-time }
        specification:
          anyOf:
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoSimple'
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoSimpleAutoAssign'
            - $ref: '#/components/schemas/ServiceSpecificationGroupIncognito'
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoWithExtension'
            - $ref: '#/components/schemas/ServiceSpecificationIncognitoTwoWayDynamic'
        default_action:
          $ref: '#/components/schemas/Action'

    # ---- Service specifications (one per service type) ----

    ServiceSpecificationIncognitoSimple:
      type: object
      title: IncognitoSimple
      description: >
        1-to-1 masking. Behaviors of type `masquerade` bind private (masked)
        numbers to public numbers of the pool. Whisper/premedia set here act
        as defaults and can be overridden per behavior. The `callback_*`
        fields are optional and absent until enabled (use JSON Patch `add` on
        an existing service).
      required: [timeout]
      properties:
        number_forward:
          type: boolean
          description: Pass the original caller number to the second leg.
        timeout:
          type: integer
          minimum: 11
          description: >
            Dial (ringing) timeout of the outgoing leg, seconds. Does NOT
            limit the duration of an established call.
        callback_to_last_caller:
          type: boolean
          description: >
            Callback feature (optional): when the masked party calls the
            public number back, connect to the last caller of the pair.
        callback_lifetime_seconds:
          type: integer
          minimum: 60
          maximum: 31536000
          description: How long a pair is remembered for the callback.
        callback_direction:
          type: string
          enum: [both, one_way]
          description: Which directions trigger the callback logic.
        whisper_a:
          $ref: '#/components/schemas/MediaSpec'
        whisper_b:
          $ref: '#/components/schemas/MediaSpec'
        premedia:
          $ref: '#/components/schemas/PremediaSpec'
        second_leg_actions:
          type: object
          description: Actions on the second leg outcomes.
          properties:
            no_answer: { $ref: '#/components/schemas/Action' }
            busy: { $ref: '#/components/schemas/Action' }

    ServiceSpecificationIncognitoSimpleAutoAssign:
      type: object
      title: IncognitoSimpleAutoAssign
      description: >
        Same masking flow as IncognitoSimple, but the public number of each
        behavior is assigned automatically from the service pool
        (`masqueradeAutoAssign` behaviors). Service update (PUT/PATCH) is not
        supported for this type. `auto_assign_pool` statistics are read-only.
      required: [timeout]
      properties:
        number_forward:
          type: boolean
          description: Pass the original caller number to the second leg.
        timeout:
          type: integer
          minimum: 11
          description: >
            Dial (ringing) timeout of the outgoing leg, seconds. Does NOT
            limit the duration of an established call.
        whisper_a:
          $ref: '#/components/schemas/MediaSpec'
        whisper_b:
          $ref: '#/components/schemas/MediaSpec'
        premedia:
          $ref: '#/components/schemas/PremediaSpec'
        second_leg_actions:
          type: object
          properties:
            no_answer: { $ref: '#/components/schemas/Action' }
            busy: { $ref: '#/components/schemas/Action' }
        auto_assign_pool:
          type: object
          readOnly: true
          description: Public number pool statistics.
          properties:
            capacity: { type: integer, readOnly: true }
            used: { type: integer, readOnly: true }

    ServiceSpecificationGroupIncognito:
      type: object
      title: GroupIncognito
      description: >
        Mutual masking of a group of two members (`GroupMasquerade`
        behaviors): each member calls the public number of the other and both
        real numbers stay hidden. Only group members can reach each other.
        The `callback_*` fields are optional (see IncognitoSimple).
      required: [timeout]
      properties:
        number_forward:
          type: boolean
          description: Pass the original caller number to the second leg.
        timeout:
          type: integer
          minimum: 11
          description: >
            Dial (ringing) timeout of the outgoing leg, seconds. Does NOT
            limit the duration of an established call.
        callback_to_last_caller:
          type: boolean
          description: 'Callback feature (optional): see IncognitoSimple.'
        callback_lifetime_seconds:
          type: integer
          minimum: 60
          maximum: 31536000
        callback_direction:
          type: string
          enum: [both, one_way]
        whisper_a:
          $ref: '#/components/schemas/MediaSpec'
        whisper_b:
          $ref: '#/components/schemas/MediaSpec'
        premedia:
          $ref: '#/components/schemas/PremediaSpec'
        second_leg_actions:
          type: object
          properties:
            no_answer: { $ref: '#/components/schemas/Action' }
            busy: { $ref: '#/components/schemas/Action' }

    ServiceSpecificationIncognitoWithExtension:
      type: object
      title: IncognitoWithExtension
      description: >
        Extension-based routing (`extension` behaviors): callers dial the
        service number, hear the `greeting_message` and enter an extension;
        the service connects them to the bound private number. Extensions are
        generated from a virtual pool defined by `extension_pool`.
      required: [timeout, extension_pool]
      properties:
        number_forward: { type: boolean }
        timeout:
          type: integer
          minimum: 11
          description: >
            Dial (ringing) timeout of the outgoing leg, seconds. Does NOT
            limit the duration of an established call.
        whisper_a: { $ref: '#/components/schemas/MediaSpec' }
        whisper_b: { $ref: '#/components/schemas/MediaSpec' }
        premedia: { $ref: '#/components/schemas/PremediaSpec' }
        greeting_message:
          $ref: '#/components/schemas/MediaSpec'
        wrong_extension_message:
          $ref: '#/components/schemas/MediaSpec'
        extension_pool:
          type: object
          required: [dimension]
          properties:
            dimension:
              type: integer
              minimum: 1
              maximum: 6
              description: Extension length in digits.
            skip_repeating_numbers:
              type: boolean
              description: Skip extensions like 1111 when generating.
            capacity: { type: integer, readOnly: true }
            used: { type: integer, readOnly: true }

    ServiceSpecificationIncognitoTwoWayDynamic:
      type: object
      title: IncognitoTwoWayDynamic
      description: >
        Two-way masking with dynamic callbacks (`masqueradeTwoWayDynamic`
        behaviors): a caller of a behavior public number gets a callback number
        pinned to the caller-seller pair for the callback window, so both
        sides can call each other back without seeing real numbers. All fields
        can be changed on a live service with JSON Patch: a new window applies
        to each pair on its next call, a new cap on the next pair creation,
        a new callback pool on new pairs only (existing pairs live out their
        windows on the old numbers).
      required: [callback_lifetime_seconds, callback_pool_sid]
      properties:
        timeout:
          type: integer
          minimum: 11
          default: 60
          description: >
            Dial (ringing) timeout of the outgoing leg, seconds. Does NOT
            limit the duration of an established call.
        callback_lifetime_seconds:
          type: integer
          minimum: 60
          maximum: 31536000
          description: >
            The callback window: how long after the LAST call of a pair the
            callback still connects. Any call of the pair restarts the
            countdown; past the window the call goes to the default action.
            The main driver of the pool usage - choose it deliberately.
        callback_max_numbers:
          type: integer
          minimum: 1
          default: 20
          description: >
            Cap of concurrently kept pairs per behavior: when exceeded, the
            least recently active pair is displaced and its callback stops
            working. Protects the pool from over-popular behaviors.
        callback_pool_sid:
          type: string
          description: >
            The pool the callback numbers are taken from (poo...). Mandatory:
            for the single-pool setup state the service `pool_sid` here
            explicitly. `PUT /service/{service_sid}/pool` does NOT touch this
            field - a dedicated callback pool is changed with JSON Patch.
        whisper_a:
          $ref: '#/components/schemas/MediaSpec'
        whisper_b:
          $ref: '#/components/schemas/MediaSpec'
        premedia:
          $ref: '#/components/schemas/PremediaSpec'
        second_leg_actions:
          type: object
          description: Actions on the second leg outcomes.
          properties:
            no_answer: { $ref: '#/components/schemas/Action' }
            busy: { $ref: '#/components/schemas/Action' }

    # ------------------------------------------------------------------
    # Behavior
    # ------------------------------------------------------------------

    BehaviorCreateRequest:
      type: object
      required: [type, specification]
      properties:
        type:
          type: string
          enum: [masquerade, masqueradeAutoAssign, GroupMasquerade, extension, masqueradeTwoWayDynamic]
          description: Must match the parent service type.
        valid_for:
          $ref: '#/components/schemas/ValidFor'
        specification:
          anyOf:
            - $ref: '#/components/schemas/BehaviorSpecificationMasquerade'
            - $ref: '#/components/schemas/BehaviorSpecificationMasqueradeAutoAssign'
            - $ref: '#/components/schemas/BehaviorSpecificationGroupMasquerade'
            - $ref: '#/components/schemas/BehaviorSpecificationExtension'
            - $ref: '#/components/schemas/BehaviorSpecificationMasqueradeTwoWayDynamic'
          description: Specification matching the behavior `type`.

    BehaviorResponse:
      type: object
      properties:
        href: { type: string, format: uri }
        behavior_sid: { type: string, format: uuid }
        service_sid: { type: string, format: uuid }
        pool_sid:
          type: string
          description: >
            The pool the public number of this behavior came from, snapshotted
            on creation. Stored since release 1.11.0; it stays unchanged when
            the service moves to another pool.
        type:
          type: string
          enum: [masquerade, masqueradeAutoAssign, GroupMasquerade, extension, masqueradeTwoWayDynamic]
        status:
          type: string
          enum: [active, inactive]
          description: Derived from `valid_for` dates.
        valid_for: { $ref: '#/components/schemas/ValidFor' }
        specification:
          anyOf:
            - $ref: '#/components/schemas/BehaviorSpecificationMasquerade'
            - $ref: '#/components/schemas/BehaviorSpecificationMasqueradeAutoAssign'
            - $ref: '#/components/schemas/BehaviorSpecificationGroupMasquerade'
            - $ref: '#/components/schemas/BehaviorSpecificationExtension'
            - $ref: '#/components/schemas/BehaviorSpecificationMasqueradeTwoWayDynamic'
        creation_time: { type: string, format: date-time }
        update_time: { type: string, format: date-time }

    # ---- Behavior specifications (one per behavior type) ----

    BehaviorSpecificationMasquerade:
      type: object
      title: masquerade
      description: >
        Behavior of `IncognitoSimple` services: a public/masked number pair.
        Whisper/premedia here override the service-level defaults.
      required: [masked_number, public_number]
      properties:
        masked_number:
          type: object
          required: [number]
          properties:
            number: { type: string, description: 'Private number, E.164 without plus, e.g. 37122200001.' }
        public_number:
          type: object
          required: [number]
          properties:
            number:
              type: string
              description: 'Public (masking) number, E.164 without plus. Must belong to the service pool.'
        whisper_a: { $ref: '#/components/schemas/MediaSpec' }
        whisper_b: { $ref: '#/components/schemas/MediaSpec' }
        premedia: { $ref: '#/components/schemas/PremediaSpec' }

    BehaviorSpecificationMasqueradeAutoAssign:
      type: object
      title: masqueradeAutoAssign
      description: >
        Behavior of `IncognitoSimpleAutoAssign` services: only the masked
        number is supplied, the public number is assigned automatically from
        the service pool. The response includes the assigned `public_number`
        and read-only `pool` statistics.
      required: [masked_number]
      properties:
        masked_number:
          type: object
          required: [number]
          properties:
            number: { type: string, description: 'Private number, E.164 without plus.' }
        public_number:
          type: object
          readOnly: true
          description: Assigned automatically from the service pool.
          properties:
            number: { type: string }
        pool:
          type: object
          readOnly: true
          description: Pool statistics.
          properties:
            free: { type: integer }
            used: { type: integer }
        whisper_a: { $ref: '#/components/schemas/MediaSpec' }
        whisper_b: { $ref: '#/components/schemas/MediaSpec' }
        premedia: { $ref: '#/components/schemas/PremediaSpec' }

    BehaviorSpecificationGroupMasquerade:
      type: object
      title: GroupMasquerade
      description: >
        Behavior of `GroupIncognito` services: exactly 2 members with mutual
        masking (double incognito). Each member calls the public number of
        the other and hears only masked numbers.
      required: [member_list]
      properties:
        member_list:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type: object
            required: [masked_number, public_number]
            properties:
              name: { type: string }
              masked_number:
                type: object
                properties:
                  number: { type: string }
              public_number:
                type: object
                properties:
                  number: { type: string }
              whisper_in: { $ref: '#/components/schemas/MediaSpec' }
              whisper_out: { $ref: '#/components/schemas/MediaSpec' }

    BehaviorSpecificationExtension:
      type: object
      title: extension
      description: >
        Behavior of `IncognitoWithExtension` services: the masked number gets
        a generated extension from the service virtual pool. Callers dial the
        service number and enter the extension. The response includes the
        assigned `extension` with pool statistics.
      required: [masked_number]
      properties:
        masked_number:
          type: object
          required: [number]
          properties:
            number: { type: string }
        public_number:
          type: object
          properties:
            number: { type: string }
        extension:
          type: object
          readOnly: true
          description: Assigned extension and pool statistics.
          properties:
            number: { type: string, description: Generated extension digits. }
            pool:
              type: object
              properties:
                free: { type: integer }
                used: { type: integer }

    BehaviorSpecificationMasqueradeTwoWayDynamic:
      type: object
      title: masqueradeTwoWayDynamic
      description: >
        Behavior of `IncognitoTwoWayDynamic` services: a public/masked pair of
        numbers whose callers get per-pair callback numbers. The public number
        is optional: when omitted, a free number of the service pool is picked
        automatically (free = not a public number of another active behavior).
        Whisper/premedia here override the service-level defaults.
      required: [masked_number]
      properties:
        masked_number:
          type: object
          required: [number]
          properties:
            number: { type: string, description: 'Private (seller) number, E.164 without plus, e.g. 37122200001.' }
        public_number:
          type: object
          properties:
            number:
              type: string
              description: >
                Public number, E.164 without plus. Must belong to the service
                pool and be unique among the public numbers of the service.
                Omit the field to have a free pool number picked automatically.
        whisper_a: { $ref: '#/components/schemas/MediaSpec' }
        whisper_b: { $ref: '#/components/schemas/MediaSpec' }
        premedia: { $ref: '#/components/schemas/PremediaSpec' }

    # ------------------------------------------------------------------
    # Filter
    # ------------------------------------------------------------------

    FilterCreateRequest:
      type: object
      required: [type, specification]
      properties:
        type:
          type: string
          enum: [BlackWhiteList]
        weight:
          type: integer
          description: Filters are checked in weight order (lower first).
        specification:
          $ref: '#/components/schemas/BlackWhiteListSpecification'

    FilterResponse:
      type: object
      properties:
        filter_sid: { type: string, format: uuid }
        type: { type: string, enum: [BlackWhiteList] }
        href: { type: string, format: uri }
        weight: { type: integer }
        specification: { $ref: '#/components/schemas/BlackWhiteListSpecification' }
        service_sid: { type: string, format: uuid }
        behavior_sid:
          type: string
          format: uuid
          description: Present for behavior filters only.

    BlackWhiteListSpecification:
      type: object
      title: BlackWhiteList
      description: >
        `black`: the `action` runs for numbers matching `rules`;
        `white`: the `action` runs for numbers NOT matching `rules`.
      required: [list_type, rules, action]
      properties:
        list_type:
          type: string
          enum: [black, white]
        rules:
          type: array
          items:
            type: object
            required: [mask]
            properties:
              mask:
                type: string
                description: >
                  Prefix of the caller number, E.164 without plus. A full
                  number (37123881359) matches one caller; a short prefix
                  matches a range or a country (372 = all numbers of
                  Estonia).
        action:
          $ref: '#/components/schemas/Action'

    # ------------------------------------------------------------------
    # Call
    # ------------------------------------------------------------------

    Call:
      type: object
      description: |
        Stored call record. Retention (TTL) is environment specific.

        A record comes in one of two shapes, and `call_tracking` on the record
        itself says which one - so a history of a service that switched levels
        stays self-describing. The record of a service with the extended call
        tracking carries the statistics block (every field marked "Extended
        tracking only" below), updated in real time. The record of the base
        tracking has no statistics as a class: those keys are absent, and
        `ended` / `duration` keep their historical form (an unfinished call
        shows the zero date in `ended` and `0` in `duration`).

        Within a shape every field is always present, and a value that does
        not exist is `null` (`0` is a measured value, not an absence). The two
        exceptions are named on the fields themselves: `pool_sid` and the pair
        fields are absent rather than `null`.
      # The keys every record carries, whatever its shape and service type.
      # Everything outside this list is conditional: the statistics fields
      # come with the extended tracking, the pair fields with a callback pair,
      # pool_sid with a record written since 1.11.0.
      required:
        - service_sid
        - behavior_sid
        - call_sid
        - record_sid
        - caller
        - public_number
        - masked_number
        - callback
        - started
        - call_tracking
        - ended
        - duration
      properties:
        service_sid: { type: string, format: uuid }
        behavior_sid:
          type: string
          format: uuid
          nullable: true
          description: >
            The behavior that served the call. `null` when the call was
            rejected before a behavior was found (`no_active_behavior`,
            `anonymous_caller`, a service-level filter with `blocked`, a late
            callback whose pair carried no behavior); set on `call_loop` and
            on a behavior-level filter.
        pool_sid:
          type: string
          description: >
            The pool the called number belongs to, as reported by the
            platform. Stored since release 1.11.0: on the records written
            before that the key is absent, not `null`.
        call_sid: { type: string, description: 'Platform call SID (cal...).' }
        record_sid:
          type: string
          nullable: true
          description: >
            Platform record SID when the call was recorded. Fetch the
            recording from the Apifonica Platform API by this SID. See
            `record_scope` for what the record covers. `null` on an extended
            record until the recording starts; the synthetic
            `empty_record_sid` of the service on a base record without a
            recording.
        record_status:
          type: string
          enum: [started, stopped, aborted, failed, ready, null]
          nullable: true
          description: |
            Extended tracking only: the live status of the recording,
            updated in real time (`null` until the recording starts):
            - `started` - the call is being recorded;
            - `stopped` - the call ended, the file is being prepared;
            - `ready` - the recording is downloadable;
            - `failed` / `aborted` - no recording will be available.
        record_scope:
          type: string
          enum: [full, second_leg, null]
          nullable: true
          description: |
            Extended tracking only: which recording `record_sid` points at
            (`null` while there is no record):
            - `full` - the record covers the whole communication, from the
              call pickup to the end (the normal case);
            - `second_leg` - the record covers the dialed leg only (from the
              dial start); returned when the full record is not available
              for the call.
        caller: { type: string, nullable: true, description: 'Original caller number. `null` on an `anonymous_caller` reject.' }
        public_number: { type: string, description: The dialed number. }
        masked_number:
          type: string
          nullable: true
          description: >
            The number the call was forwarded to. `null` on every rejected
            record. On the callback-to-last-caller records of the
            IncognitoSimple family it is the last caller that was dialed back.
        connected:
          type: boolean
          nullable: true
          description: >
            Extended tracking only. `true` - the call was connected (the
            masked party was dialed; whether it answered is
            `second_leg_status`). `false` - the call was rejected, see
            `reject_reason`.
        reject_reason:
          type: string
          enum: [callback_window_expired, callback_evicted_by_limit, no_active_behavior, blocked, anonymous_caller, call_loop, null]
          nullable: true
          description: |
            Why the call was rejected (`connected: false`; stored when the
            service has `save_rejected_calls: true`; `null` on a connected
            call):
            - `callback_window_expired` - a callback after the pair window
              ran out (annotated with the pair fields of the late pair);
            - `callback_evicted_by_limit` - the pair was displaced by the
              per-behavior cap;
            - `no_active_behavior` - the dialed number has no active
              behavior;
            - `blocked` - a moderation filter fired (see `is_blocked`,
              `blocked_by`, `blocked_reason`);
            - `anonymous_caller` - a suppressed or garbled caller number;
            - `call_loop` - a call with no meaningful route (e.g. the masked
              party dialing its own public number).
        is_blocked:
          type: boolean
          description: Extended tracking only. The call was turned away by a moderation filter.
        blocked_by:
          type: string
          nullable: true
          description: Extended tracking only. The filter_sid of the filter that fired; `null` when no filter fired.
        blocked_reason:
          type: string
          enum: [black_list, white_list, null]
          nullable: true
          description: Extended tracking only. Which list type of the filter fired; `null` when no filter fired.
        callback:
          type: boolean
          description: >
            True for the reverse call direction: the callback-to-last-caller
            records of the IncognitoSimple family and the seller-to-buyer
            callbacks of IncognitoTwoWayDynamic. Forward calls of a pair (the
            first call, the buyer repeats and buyer callbacks - identical on
            the wire) stay false; whether a call went over a pair is what
            callback_pair_sid says.
        callback_pair_sid:
          type: string
          description: >
            IncognitoTwoWayDynamic only, and only on the records that belong
            to a pair - on every other record the key is absent, not `null`.
            The buyer-seller pair of the call:
            shared by the first call, the repeats and the callbacks of both
            sides - group a dialog by this field, no number or date matching
            needed.
        initial_call_sid:
          type: string
          description: >
            IncognitoTwoWayDynamic only. The call_sid of the call that opened
            the pair; on that first record it equals the record's own call_sid.
        callback_number:
          type: string
          description: >
            IncognitoTwoWayDynamic only. The number pinned to the pair - what
            the seller sees as the buyer's number. On the buyer-side callbacks
            public_number and callback_number diverge by design: public_number
            is what the buyer dialed, callback_number is the seller-facing pair
            number.
        call_tracking:
          type: string
          enum: [base, extended]
          description: >
            The tracking level this record was collected with - the
            record-level echo of the service setting. A history of a service
            that switched levels stays self-describing: `base` records carry
            the core fields, `extended` ones the full statistics.
        started:
          type: string
          format: date-time
          description: The call arrival.
        answered:
          type: string
          format: date-time
          nullable: true
          description: >
            Extended tracking only: when the dialed party answered. `null`
            until it does. A mobile voicemail pickup counts as an answer.
        ended:
          type: string
          format: date-time
          nullable: true
          description: >
            The call end. On extended-tracking records `null` until the
            call ends; on base-tracking records an unfinished call shows the
            zero date ("0001-01-01T00:00:00+00:00").
        duration:
          type: integer
          nullable: true
          description: >
            Seconds from the call arrival to its end. On extended-tracking
            records `null` until the call ends, then a measured value (`0`
            included); on base-tracking records `0` until the call is
            synced.
        talk_duration:
          type: integer
          nullable: true
          description: >
            Extended tracking only: seconds of the conversation - from the
            answer to the end. `null` when the dialed party never answered.
        time_to_answer:
          type: integer
          nullable: true
          description: >
            Extended tracking only: seconds from the call arrival to the
            answer. `null` when the dialed party never answered.
        caller_wait:
          type: integer
          nullable: true
          description: >
            Extended tracking only: seconds the caller spent on the line
            waiting for the answer. `null` until the answer.
        pdd_ms:
          type: integer
          nullable: true
          description: >
            Extended tracking only: the post-dial delay in MILLISECONDS -
            from the dial start to the ringing. Measured only when the
            carrier of the dialed number reports the ringing signal; many
            routes do not, and the field stays `null` there.
        ring_duration:
          type: integer
          nullable: true
          description: >
            Extended tracking only: seconds the dialed number rang before
            answering. Needs the ringing signal of the carrier, `null` on
            routes that do not deliver it (same as `pdd_ms`).
        second_leg_status:
          type: string
          enum: [answered, busy, no_answer, canceled, rejected, null]
          nullable: true
          description: |
            Extended tracking only: the final outcome of the dialed leg
            (`null` while it has no outcome yet, and on a rejected call):
            - `answered` - the dialed party picked up (a mobile voicemail
              pickup counts as an answer);
            - `busy` / `no_answer` / `rejected` - it did not pick up;
            - `canceled` - the caller hung up before the answer.

    # ------------------------------------------------------------------
    # Pool
    # ------------------------------------------------------------------

    Pool:
      type: object
      description: >
        An Apifonica number pool with its sync state and activity. Describes
        pools of real platform numbers; the virtual extension pool of an
        `IncognitoWithExtension` service is reported in the service
        specification instead. The object is full-structured: every field is
        present on every pool and a value that does not exist is `null`. Two
        exceptions: `service_sid` is absent when no service occupies the
        pool, `deleted_at` is present on a deleted pool only.
      # Everything but the two conditional keys: service_sid (only while a
      # service occupies the pool) and deleted_at (only on a deleted pool).
      required:
        - pool_sid
        - name
        - call_app_sid
        - numbers_total
        - synced_at
        - sync_status
        - sync_paused
        - behaviors
        - calls
      properties:
        pool_sid: { type: string }
        name:
          type: string
          nullable: true
          description: >
            Pool name on the platform, as of the last successful sync of the
            pool replica. `null` for a pool that was never synced.
        call_app_sid:
          type: string
          nullable: true
          description: >
            The call application bound to the pool, as of the last successful
            sync (`null` until then). A binding made by the service itself
            (service creation, pool change) shows up with the next sync,
            within the sync cadence.
        numbers_total:
          type: integer
          nullable: true
          description: >
            Number of numbers in the pool according to the stored replica;
            `null` for a never-synced pool (the size is unknown, not zero). The
            `behaviors` and `calls` counters are local facts and are numbers in
            every state.
        synced_at:
          type: string
          format: date-time
          nullable: true
          description: >
            The last platform answer for the pool (the content, or the latest
            "pool not found" of a deleted pool). `null` for a never-synced pool.
        sync_status:
          type: string
          enum: [ok, syncing, pool_deleted, never_synced]
          description: |
            The state of the pool replica, always present:
            - `ok` - the replica is complete, `numbers_total` is the pool size;
            - `syncing` - the first fill is in progress;
            - `pool_deleted` - the platform reports the pool as gone; see
              `deleted_at` and `sync_paused`;
            - `never_synced` - the service holds no replica of the pool yet: a
              service references it, but nothing has asked the platform about
              it since the replica exists (a service created before that and
              quiet since). `synced_at`, `name`, `call_app_sid` and
              `numbers_total` are `null`. Reading the pool (this list or
              `GET /pool/{pool_sid}`) starts the sync in the background; the
              next read shows the real state.
        deleted_at:
          type: string
          format: date-time
          description: >
            The first platform "pool not found" answer of the current
            `pool_deleted` series. Present on a deleted pool only (the state
            itself is `sync_status`); a platform answer with the pool content
            clears it.
        sync_paused:
          type: boolean
          description: >
            `true` when the deletion is confirmed: the platform kept answering
            "pool not found" for at least 30 minutes after `deleted_at`. The
            service then stops the background sync of the pool (one safety
            probe a day) and keeps the tombstone. `DELETE /pool/{pool_sid}/cache`
            asks the platform right away, which lifts the tombstone when the
            pool is back. `false` on a live pool and while a deletion is not
            yet confirmed.
        service_sid:
          type: string
          format: uuid
          description: >
            The service occupying the pool. Absent when no service uses the
            pool any more (it was moved to another one) - such a pool stays
            readable so its remaining activity can be checked.
        behaviors:
          type: object
          description: >
            Behaviors (bindings) whose public number belongs to this pool.
          required: [active, total, numbers_in_use, last_created_at]
          properties:
            active: { type: integer, description: Active by `valid_for` right now. }
            total: { type: integer }
            numbers_in_use:
              type: integer
              description: >
                Distinct public numbers of the pool used by active behaviors.
                Lower than `active` for extension services, where several
                behaviors share one public number.
            last_created_at: { type: string, format: date-time, nullable: true, description: '`null` while the pool has no behaviors.' }
        calls:
          type: object
          description: Calls that arrived on the numbers of this pool.
          required: [window_seconds, count, last_call_at, first_call_at]
          properties:
            window_seconds: { type: integer, description: The period `count` covers. }
            count: { type: integer }
            last_call_at:
              type: string
              format: date-time
              nullable: true
              description: The latest call on the pool in the whole retained history; `null` without calls.
            first_call_at:
              type: string
              format: date-time
              nullable: true
              description: >
                The earliest call on the pool in the retained history (`null`
                without calls). Calls
                carry their pool since release 1.11.0, so this value also
                tells since when the counters are complete.

    # ------------------------------------------------------------------
    # Common structures: actions, media, validity window, errors
    # ------------------------------------------------------------------

    Action:
      type: object
      description: >
        Call control action used in `default_action`, filter actions and
        `second_leg_actions`.
      required: [type]
      properties:
        type:
          type: string
          enum: [PlayAudio, EndCall, Say]
        specification:
          oneOf:
            - $ref: '#/components/schemas/PlayAudioSpecification'
            - $ref: '#/components/schemas/SaySpecification'
            - $ref: '#/components/schemas/EndCallSpecification'
      example:
        type: PlayAudio
        specification:
          url: 'https://xml.apifonica.com/controller/voicing/audio/no_redirection.wav'
          loops: 1

    PlayAudioSpecification:
      type: object
      title: PlayAudio
      required: [url]
      properties:
        url: { type: string, format: uri }
        loops:
          type: integer
          minimum: 0
          default: 0
          description: '0 = infinite repetition, 1 = play once.'

    SaySpecification:
      type: object
      title: Say (TTS)
      required: [text]
      properties:
        text: { type: string }
        engine: { type: string, example: polly }
        voice: { type: string, example: Brian }
        language: { type: string, example: en-US }
        speed: { type: string }
        loops:
          type: integer
          minimum: 0
          default: 0
          description: '0 = infinite repetition, 1 = play once.'

    EndCallSpecification:
      type: object
      title: EndCall
      properties:
        signal:
          type: string
          description: Optional SIP signal.

    MediaSpec:
      type: object
      description: Whisper / greeting media.
      required: [type, value]
      properties:
        type:
          type: string
          enum: [audio, tone, say]
        value:
          type: string
          description: Audio URL, tone spec or TTS text depending on `type`.
        tts:
          type: object
          description: 'Required when type = say.'
          properties:
            engine: { type: string, example: polly }
            voice: { type: string, example: Brian }
            language: { type: string, example: en-US }
            speed: { type: string }

    PremediaSpec:
      type: object
      description: >
        Media played to the caller while the second leg is dialing (instead of
        ringback). Same fields as MediaSpec plus interruption control.
      required: [type, value]
      properties:
        type: { type: string, enum: [audio, tone, say] }
        value: { type: string }
        tts:
          type: object
          properties:
            engine: { type: string }
            voice: { type: string }
            language: { type: string }
            speed: { type: string }
        interruptible:
          type: boolean
          description: Stop the premedia when the second leg answers.
        tone_duration:
          type: number
          description: 'type = tone: tone duration, seconds.'

    ValidFor:
      type: object
      description: Behavior activity window.
      properties:
        start_date_time:
          type: string
          format: date-time
          description: Activation time (RFC 3339).
        end_date_time:
          type: string
          format: date-time
          description: Deactivation time; empty = no end.
        auto_remove:
          type: boolean
          description: Remove the behavior automatically when expired.

    JsonPatch:
      type: array
      description: RFC 6902 JSON Patch document.
      items:
        type: object
        required: [op, path]
        properties:
          op: { type: string, enum: [add, remove, replace, move, copy, test] }
          path: { type: string, example: /specification/timeout }
          value: {}
          from: { type: string }

    ApiError:
      type: object
      description: Unified error format. Codes are listed in the "Error codes" section.
      properties:
        code: { type: string, example: ERR011 }
        reason: { type: string, example: Resource validation failed }
        message: { type: string, example: 'specification: cannot be blank; type: cannot be blank.' }
        type: { type: string, example: Service }
        details:
          type: array
          items:
            type: object
            properties:
              type: { type: string }
              data: { type: object, additionalProperties: true }
