{
  "openapi": "3.1.0",
  "info": {
    "title": "Natter Business API",
    "version": "0.10.0",
    "summary": "Scoped text and media messaging, customer-initiated calls, business profiles and signed webhooks.",
    "description": "Live and Test Business API on the existing Natter API host. Complete owner signup, verify a business number and create a Live or Test key in the Business portal. Configure a webhook, then invite a consenting Natter user to start the conversation before replying. The Natter Business API is free for now. It is intended to become a paid service at a planned price of €0.001 per message (one tenth of a cent), set only to cover running costs. We will confirm prices at least 30 days before any charges begin, and nothing you use before then will be charged. Live sending needs a fresh configured price, which is zero today; usage is recorded even when free and is readable through usage, billing, ledger and invoices. A positive price is paid from the business's prepaid credit or monthly invoice credit limit; a send it cannot cover is refused with 402 payment_required. The owner portal and API keys with Profile & appearance edit the same saved public profile; private contacts remain owner-only. Profile images, wallpapers, visibility and feature permissions use the same settings as the owner portal. Enabled features support image, document, audio and video messages plus customer-initiated voice/video calls. Calls require a full-access key or one with Messaging & calls, and the customer must initiate; your integration supplies its own WebRTC engine. Streaming text/audio and business-initiated calls are not available in this release. Business access never includes personal Feeds, Atlas or customer locations. All API responses use Cache-Control: no-store and X-Content-Type-Options: nosniff. Exact routes reject query strings (including a bare '?'), encoded path aliases, fragments and opaque URLs, except GET calls/{call_id}/signals accepts only one canonical after=0..512 query. GET/DELETE reject a body or transfer encoding. JSON writes require one application/json Content-Type (optional charset=utf-8), no Content-Encoding, valid UTF-8, a single object, lowercase declared field names only, no duplicate or unknown fields. Error responses have only an error code. Natter search (D422): a verified business with a saved profile, a Live profile picture and a commissioned Live number is listed in the Natter app's business search by default; GET/PUT /v1/business/live/directory reads and switches that listing. Every reachable Live business can also be found by its exact business number. Display names that use a reserved Masons Mail, Natter or authority name, or a banned word, are refused with 422."
  },
  "servers": [
    {
      "url": "https://api.natterapp.com",
      "description": "Natter API host. Use a matching Live or Test key and explicit customer consent."
    }
  ],
  "x-implementation-status": {
    "draft": false,
    "mounted": true,
    "commissioned": false,
    "public-availability": "owner setup required",
    "source-files": [
      "apps/natter-relay/internal/relay/business_http.go",
      "apps/natter-relay/internal/relay/business_api.go",
      "apps/natter-relay/internal/relay/business_submission.go",
      "apps/natter-relay/internal/relay/business_webhook.go",
      "apps/natter-relay/internal/hostedbusiness/auth.go",
      "apps/natter-relay/internal/hostedbusiness/webhook_config.go",
      "apps/natter-relay/internal/hostedbusiness/webhook_http.go",
      "apps/natter-relay/internal/relay/business_reporting.go",
      "apps/natter-relay/internal/relay/business_reporting_http.go",
      "apps/natter-relay/internal/relay/business_profile_http.go",
      "apps/natter-relay/internal/hostedbusiness/profile_api.go",
      "apps/natter-relay/internal/relay/business_settings_http.go",
      "apps/natter-relay/internal/hostedbusiness/chat_settings.go",
      "apps/natter-relay/internal/hostedbusiness/appearance_assets.go",
      "apps/natter-relay/internal/relay/business_contacts.go",
      "apps/natter-relay/internal/relay/business_contacts_http.go",
      "apps/natter-relay/internal/relay/business_contacts_export.go",
      "apps/natter-relay/internal/relay/business_account_http.go",
      "apps/natter-relay/internal/relay/business_webhook_tools_http.go",
      "apps/natter-relay/internal/hostedbusiness/webhook_attempts.go",
      "apps/natter-relay/internal/relay/business_billing.go",
      "apps/natter-relay/internal/relay/business_billing_http.go"
    ]
  },
  "tags": [
    {
      "name": "Account and receipts"
    },
    {
      "name": "Messages"
    },
    {
      "name": "Webhook configuration"
    },
    {
      "name": "Appearance and features"
    }
  ],
  "paths": {
    "/v1/business/{environment}/account": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessAccount",
        "tags": ["Account and receipts"],
        "summary": "Read the authenticated Business account",
        "description": "Reads current account authority. Account reads remain available while paused, suspended or banned. This grants no permission to send.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "account:view",
        "responses": {
          "200": {
            "description": "Current account metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/profile": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessProfile",
        "tags": ["Account and receipts"],
        "summary": "Read your public Business profile",
        "description": "Returns the customer-facing display name, description, support contacts, address and links. The registered account name remains separate. This read never returns the private operational contact or asserts company verification. Keys require Profile & appearance (profile:manage) in their own environment. Live, Test and the owner portal share this saved profile and revision. The flat public body is unchanged; use the strong ETag header for a conditional PUT. Historical saved records remain readable even if they exceed the new write aggregate limit.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "responses": {
          "200": {
            "description": "Only public profile fields. Optional fields are empty strings or arrays.",
            "headers": {
              "ETag": {
                "description": "Opaque strong validator bound to this business and profile revision. Keep the quotes when sending If-Match.",
                "schema": {
                  "type": "string"
                },
                "example": "\"business-profile-00000000-0000-4000-8000-000000000001-3\""
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicBusinessProfile"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      },
      "put": {
        "operationId": "updateHostedBusinessProfile",
        "tags": ["Account and receipts"],
        "summary": "Update your saved public Business profile",
        "description": "Requires a full-access key or a restricted key with Profile & appearance (profile:manage). Live, Test and the owner portal edit the same saved profile. Send all public fields with the exact strong ETag returned by GET in If-Match; no weak, wildcard, multiple or wrong-business validators. Never include the private operational contact: it is rejected and cannot be changed by this API. Missing If-Match returns 428; stale revisions return 412. Read and reconcile before retrying after a conflict or lost response. Key, owner and selected verified sender authority are checked again in the transaction. JSON body limit: 32768 bytes. New public objects must serialize to at most 15360 UTF-8 bytes using compact JSON.stringify-compatible escaping, within the existing 16384-byte database record bound. Text bounds count UTF-16 code units; display name also has a 256 UTF-8-byte limit. HTTPS URLs require a nonempty host, no credentials, whitespace or backslashes, a valid explicit port 1-65535 if supplied, and at most 2048 UTF-8 bytes. All declared fields, address fields and link fields are required; optional values are empty strings or arrays. Nulls, duplicate or unknown fields and invalid Unicode are rejected.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "parameters": [
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "description": "Exact strong ETag from this business's last profile GET, including quotes.",
            "schema": {
              "type": "string"
            },
            "example": "\"business-profile-00000000-0000-4000-8000-000000000001-3\""
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicBusinessProfile"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved public profile. Private operational contact remains unchanged.",
            "headers": {
              "ETag": {
                "description": "New strong validator for the saved revision.",
                "schema": {
                  "type": "string"
                },
                "example": "\"business-profile-00000000-0000-4000-8000-000000000001-4\""
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicBusinessProfile"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks Profile & appearance (profile:manage), belongs to another environment, or current owner or selected sender authority no longer permits access.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "412": {
            "description": "The profile changed after the supplied ETag. Read, reconcile and retry with the new validator.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["business_profile_changed"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "422": {
            "description": "The display name uses a reserved name (Masons Mail, Natter, Idelego, an authority such as police or a tax agency, or official support for one of them) or a banned word. Nothing was saved or charged. Masons Mail can allow a name for a business on request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["business_name_reserved", "business_name_not_allowed"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "428": {
            "description": "If-Match is required. Read the profile to obtain its ETag.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["profile_revision_required"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/SaveLimited"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/messages": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "post": {
        "operationId": "submitHostedBusinessText",
        "tags": ["Messages"],
        "summary": "Queue a text or media reply",
        "description": "Requires the current customer relationship in this environment, an eligible customer message less than 183 elapsed days (4392 hours) ago, an active/throttled business and current key/endpoint/account/member authority. Merely opening a chat, receipts and background updates do not renew permission. Opt-out and independent blocks prevent a new send. The required runtime admission callback additionally controls commissioning and sandbox recipients; setup is still required for each environment. First acceptance creates sealed queued work, not a sent/delivered/read claim. Matching retries require the same API key and unchanged recipient/type/text/media_id within the 24-hour idempotency window and still-current permission; HTTP 200 returns the same operation ID with current state. A different API key cannot adopt old work. Retry never extends deadlines. A retained expired key returns idempotency_expired; after bounded idempotency cleanup the server can no longer deduplicate it, so never rely on replay protection beyond 24 hours. Media requests reference a ready upload from POST media; they do not contain file bytes or URLs. The upload must belong to the same key and contact, and its feature must still be enabled. Both variants return the same receipt and sent/failed/read status contract; no delivered status is emitted.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Matching idempotent replay; original operation identity and deadlines are preserved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageReceipt"
                }
              }
            }
          },
          "202": {
            "description": "New request durably queued. It has not yet been accepted by the relay.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageReceipt"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "402": {
            "description": "payment_required: the Live price is positive and this business's prepaid credit or invoice credit limit cannot cover the send.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["payment_required"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "403": {
            "description": "Permission/environment mismatch, paused or suspended/banned business, or current customer relationship/endpoint permission prevents this send. Banned sending uses business_suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "forbidden",
                        "business_paused",
                        "business_suspended",
                        "no_relationship",
                        "consent_required"
                      ]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The recipient/text differs under a retained request key, or its retained 24-hour replay deadline has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["idempotency_conflict", "idempotency_expired"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Exactly one header. 1–200 visible ASCII characters (0x21–0x7e); no spaces. Scoped to this Business/environment. Use a new value for a distinct send.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200,
              "pattern": "^[!-~]{1,200}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "At most 32,768 UTF-8 bytes for the complete JSON body; unknown/duplicate/case-aliased fields are invalid.",
          "x-max-body-bytes": 32768,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TextRequest"
                  },
                  {
                    "$ref": "#/components/schemas/MediaMessageRequest"
                  }
                ],
                "description": "Choose text with its body, or media with the ID of a completed upload and an optional caption."
              }
            }
          }
        }
      }
    },
    "/v1/business/{environment}/messages/{message_id}": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "message_id",
          "in": "path",
          "required": true,
          "description": "Canonical lowercase UUID returned by submission.",
          "schema": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessMessageReceipt",
        "tags": ["Account and receipts"],
        "summary": "Read one scoped operation receipt",
        "description": "Returns content-free state for this Business/environment, using a current key with Messaging & calls (messaging:manage, or an older key's converted messages:send). It need not be the original sending key. No message body, phone number, hosted device identity, history or cryptographic state is returned. Receipts remain available for at most 30 days from creation; removal of required account/contact parents can shorten availability. Unknown, expired and foreign operations share the same 404.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current operation receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageReceipt"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "404": {
            "description": "Unknown, expired, foreign or non-outbound operation, or noncanonical message UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["not_found"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/webhook": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessWebhook",
        "tags": ["Webhook configuration"],
        "summary": "Read webhook metadata",
        "description": "Requires the current scoped key. Maintenance/read access remains available while paused, suspended or banned. No signing secret is returned. A missing configuration returns HTTP 200 with webhook:null.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "webhooks:manage",
        "responses": {
          "200": {
            "description": "Current metadata or null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookRead"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      },
      "put": {
        "operationId": "rotateHostedBusinessWebhook",
        "tags": ["Webhook configuration"],
        "summary": "Set destination and rotate its signing secret",
        "description": "Every successful PUT, including an identical replay, rotates the secret and advances the database revision. This operation has no Idempotency-Key replay contract. The signing secret is returned once after commit; losing that response requires another rotation. GET never recovers it. The HTTPS destination is syntax/public-address validated and DNS checked before the write; no test message is posted. Dispatch resolves and pins public DNS again. Rotation does not readdress previously queued content to the new revision: old-revision payload is disposed by bounded cleanup. Maintenance remains allowed while paused/suspended/banned, without authorizing delivery.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "webhooks:manage",
        "responses": {
          "200": {
            "description": "Persisted destination metadata and one-time signing secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookWrite"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        },
        "requestBody": {
          "required": true,
          "description": "At most 16,384 bytes of strict UTF-8 JSON.",
          "x-max-body-bytes": 16384,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPut"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteHostedBusinessWebhook",
        "tags": ["Webhook configuration"],
        "summary": "Delete the scoped webhook configuration",
        "description": "Idempotent deletion of this Business/environment's configuration. Missing configuration also returns 204. Legacy and other environment rows are unchanged. Queued content is not rerouted; bounded cleanup disposes its now-missing destination. No response body.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "webhooks:manage",
        "responses": {
          "204": {
            "description": "Configuration is absent."
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/numbers": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "listHostedBusinessNumbers",
        "tags": ["Account and receipts"],
        "summary": "List the business's numbers",
        "description": "Read-only. Lists every number of the business with its verification state and the environments that send from it. Phone numbers are never returned. Add, verify, label or remove numbers in the Business portal.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "account:view",
        "responses": {
          "200": {
            "description": "Current numbers, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessNumberList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/keys": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "listHostedBusinessKeys",
        "tags": ["Account and receipts"],
        "summary": "List this environment's API keys",
        "description": "Read-only. Lists up to 100 keys of this environment, newest first, including revoked keys. Secrets are never returned. Create, replace or revoke keys in the Business portal.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "account:view",
        "responses": {
          "200": {
            "description": "Key metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessKeyList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/webhook/test": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "post": {
        "operationId": "sendHostedBusinessWebhookTest",
        "tags": ["Webhook configuration"],
        "summary": "Send a signed test event",
        "description": "Sends one test event to the configured endpoint through the normal delivery path: the same destination checks, X-Natter-Signature, X-Natter-Event-Id and ten-second limit as customer events. It is a single attempt and is not retried; the result is returned and recorded in the delivery log. Send no request body. At most ten tests per minute per environment. Maintenance remains allowed while paused, suspended or banned.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "webhooks:manage",
        "responses": {
          "200": {
            "description": "The attempt, delivered or not.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "No webhook is configured for this environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["webhook_not_configured"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "429": {
            "description": "Ten tests were already sent in the last minute.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["webhook_test_limited"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/webhook/deliveries": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "listHostedBusinessWebhookDeliveries",
        "tags": ["Webhook configuration"],
        "summary": "List recent webhook delivery attempts",
        "description": "The last 50 attempts of this environment from the last seven days, newest first: event kind and ID, attempt number, HTTP status, latency and failure code. Payloads are never stored.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "webhooks:manage",
        "responses": {
          "200": {
            "description": "Recent attempts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveries"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/usage": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessUsage",
        "tags": ["Account and receipts"],
        "summary": "Read usage and processing metrics",
        "description": "Read only this business and environment using Billing (billing:read). Month-to-date totals, seven-day processing metrics and monthly history (the current month unless month or from/to is given). Available during a price hold; never grants sending permission.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "billing:read",
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "required": false,
            "description": "One UTC month, YYYY-MM. Not in the future and at most 36 months back.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "First month of a range, YYYY-MM; use with to. A range covers at most 12 months.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last month of a range, YYYY-MM; not in the future.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read usage and processing metrics.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageReport"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/pricing": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessPricing",
        "tags": ["Account and receipts"],
        "summary": "Read current message pricing",
        "description": "Read only this business and environment using Billing (billing:read). Available during a price hold; never grants sending permission.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "billing:read",
        "responses": {
          "200": {
            "description": "Read current message pricing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PricingReport"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/billing": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessBilling",
        "tags": ["Account and receipts"],
        "summary": "Read the billing account",
        "description": "Mode (prepaid or monthly invoice), balance, credit limit, low-balance warning and whether payment is required, for this business and environment. Mode and limits cannot be changed through the API.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "billing:read",
        "responses": {
          "200": {
            "description": "Read the billing account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingRead"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/billing/ledger": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "listHostedBusinessLedger",
        "tags": ["Account and receipts"],
        "summary": "List balance movements",
        "description": "Newest first: one usage debit per accepted send at the price in force, plus top-ups, adjustments and invoice payments.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "billing:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1 to 100 (default 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "The next value from the previous page.",
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List balance movements.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LedgerRead"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/invoices": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "listHostedBusinessInvoices",
        "tags": ["Account and receipts"],
        "summary": "List invoices",
        "description": "Up to 36 monthly records for this business and environment, newest first. Empty until a month with usage or a billing account has closed.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "billing:read",
        "responses": {
          "200": {
            "description": "List invoices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/invoices/{invoice_id}": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "invoice_id",
          "in": "path",
          "required": true,
          "description": "Invoice id from the invoice list.",
          "schema": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessInvoice",
        "tags": ["Account and receipts"],
        "summary": "Read one invoice",
        "description": "One monthly record with its lines.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "billing:read",
        "responses": {
          "200": {
            "description": "Read one invoice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceRead"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "404": {
            "description": "not_found: no invoice with this id in this business/environment."
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/directory": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Only live. The listing does not exist for Test; /v1/business/sandbox/directory answers 404.",
          "schema": {
            "type": "string",
            "enum": ["live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessDirectory",
        "tags": ["Appearance and features"],
        "summary": "Read the Natter search listing",
        "description": "Whether the business is listed in the Natter app's business search (the owner's switch, on by default), whether Masons Mail has unlisted it, whether it is searchable now and, if not, what is missing. A name close to an earlier-verified business is marked similar_to_verified: it is still listed but labelled and ranked below that business.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "responses": {
          "200": {
            "description": "The current listing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessDirectory"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key scope, owner or business authority does not permit this request."
          },
          "404": {
            "description": "not_found: the environment is not live."
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        }
      },
      "put": {
        "operationId": "updateHostedBusinessDirectory",
        "tags": ["Appearance and features"],
        "summary": "List or unlist the business in Natter search",
        "description": "Send exactly {\"listed\": true} or {\"listed\": false}. A change counts as one of the day's text saves (shared with profile and settings saves); sending the current value changes and charges nothing. Masons Mail's own unlisting cannot be undone here. Unlisted businesses stay reachable by their link, QR code and exact business number.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": ["listed"],
                "properties": {
                  "listed": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved listing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessDirectory"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key scope, owner or business authority does not permit this request."
          },
          "404": {
            "description": "not_found: the environment is not live."
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/SaveLimited"
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/settings": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessSettings",
        "tags": ["Appearance and features"],
        "summary": "Read appearance, visibility and feature settings",
        "description": "Current settings for this business and environment. Missing configuration returns revision zero and media/call switches off. Private contacts are not part of this document.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "responses": {
          "200": {
            "description": "Current settings. Treat the ETag as opaque and keep it with this business/environment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessSettings"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Strong settings revision validator, for example \"business-settings-3\"."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner or selected sender authority does not permit this request."
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        }
      },
      "put": {
        "operationId": "updateHostedBusinessSettings",
        "tags": ["Appearance and features"],
        "summary": "Save appearance, visibility and feature settings",
        "description": "Updates the same saved settings as the portal. Send all fields, the expected_revision from GET, and that GET response ETag as If-Match. Hidden values are removed from consumer responses without erasing the owner's saved profile. An asset must belong to the exact business, environment and slot. Textual profile values are business-wide; these settings are environment-specific.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "responses": {
          "200": {
            "description": "Saved settings and the next revision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessSettings"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Strong settings revision validator, for example \"business-settings-3\"."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner or selected sender authority does not permit this request."
          },
          "404": {
            "description": "Selected asset is missing, expired or not owned by this business/environment/slot."
          },
          "409": {
            "description": "business_settings_changed: a concurrent edit changed the revision. Read and reconcile before a deliberate retry."
          },
          "412": {
            "description": "precondition_failed: If-Match does not exactly match expected_revision."
          },
          "428": {
            "description": "precondition_required: If-Match is missing."
          },
          "429": {
            "$ref": "#/components/responses/SaveLimited"
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        },
        "parameters": [
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The strong ETag returned by the corresponding settings GET."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Complete settings document; at most 24,576 UTF-8 bytes. No missing, duplicate, unknown or null flags.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BusinessSettingsWrite"
              }
            }
          }
        }
      }
    },
    "/v1/business/{environment}/welcome": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "getHostedBusinessWelcome",
        "tags": ["Appearance and features"],
        "summary": "Read the welcome and terms screen",
        "description": "The optional screen the Natter app shows after Natter's own fixed Business notice, for this business and environment. Missing configuration returns revision zero and welcome null.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "responses": {
          "200": {
            "description": "Current welcome screen. Keep the ETag with this business/environment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessWelcomeSettings"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Strong welcome revision validator, for example \"business-welcome-3\"."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key scope, owner or selected sender authority does not permit this request."
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        }
      },
      "put": {
        "operationId": "updateHostedBusinessWelcome",
        "tags": ["Appearance and features"],
        "summary": "Save the welcome and terms screen",
        "description": "Send expected_revision from GET and that response's ETag as If-Match. A save is one of the day's text saves. Changing the text or a link changes content_version; with reask_on_change, customers who agreed earlier must agree again before either side can send new messages.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "responses": {
          "200": {
            "description": "Saved welcome screen and the next revision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessWelcomeSettings"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Strong welcome revision validator, for example \"business-welcome-3\"."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key scope, owner or selected sender authority does not permit this request."
          },
          "409": {
            "description": "business_settings_changed: a concurrent edit changed the welcome revision. Read and reconcile before a deliberate retry."
          },
          "412": {
            "description": "precondition_failed: If-Match does not exactly match expected_revision."
          },
          "428": {
            "description": "precondition_required: If-Match is missing."
          },
          "429": {
            "$ref": "#/components/responses/SaveLimited"
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        },
        "parameters": [
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The strong ETag returned by the corresponding welcome GET."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Complete welcome document; at most 16,384 UTF-8 bytes. No missing, duplicate or unknown fields.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BusinessWelcomeWrite"
              }
            }
          }
        }
      }
    },
    "/v1/business/{environment}/assets/{asset}": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "asset",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "POST uses avatar, chat_wallpaper or profile_background. GET and DELETE use an uploaded asset UUID."
        }
      ],
      "post": {
        "operationId": "uploadHostedBusinessAsset",
        "tags": ["Appearance and features"],
        "summary": "Upload a profile image or wallpaper",
        "description": "POST raw JPEG/PNG bytes to assets/avatar, assets/chat_wallpaper or assets/profile_background. At most 2 MiB, 4 megapixels and 4096 pixels per side; normalized avatars at most 512 KiB. Metadata is stripped by decoding/re-encoding. Upload does not publish: set the returned id in appearance via PUT settings. Unselected uploads expire after 24 hours. Tenant/environment capacity is 512 assets or 256 MiB. No remote image URLs are accepted.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "responses": {
          "201": {
            "description": "Normalized asset metadata. Save its ID in the matching settings slot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BusinessAsset"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner or selected sender authority does not permit this request."
          },
          "413": {
            "description": "business_asset_limit: input, normalized image or tenant capacity exceeds its limit."
          },
          "429": {
            "$ref": "#/components/responses/SaveLimited"
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        },
        "requestBody": {
          "required": true,
          "description": "Raw image bytes; this is not JSON, base64 or multipart.",
          "content": {
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getHostedBusinessAsset",
        "tags": ["Appearance and features"],
        "summary": "Read an owned profile image or wallpaper",
        "description": "Returns an owned, unexpired normalized image. Customers use their authenticated conversation asset route and may read only currently selected images.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "parameters": [
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "description": "The ETag of a copy you already hold. Asset IDs are immutable, so a matching validator returns 304 without the image.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Raw normalized image. Asset IDs are immutable: a changed picture is a new ID, so the response may be kept privately for a year.",
            "headers": {
              "Cache-Control": {
                "description": "private, max-age=31536000, immutable. Private because every read is authorized for its caller; never store it in a shared cache.",
                "schema": {
                  "type": "string",
                  "const": "private, max-age=31536000, immutable"
                }
              },
              "ETag": {
                "description": "Strong validator for this asset ID, for example \"business-asset-<asset UUID>\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "304": {
            "description": "If-None-Match matched this asset's ETag after authorization. No body; keep using your copy.",
            "headers": {
              "Cache-Control": {
                "description": "private, max-age=31536000, immutable. Private because every read is authorized for its caller; never store it in a shared cache.",
                "schema": {
                  "type": "string",
                  "const": "private, max-age=31536000, immutable"
                }
              },
              "ETag": {
                "description": "Strong validator for this asset ID, for example \"business-asset-<asset UUID>\".",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner or selected sender authority does not permit this request."
          },
          "404": {
            "description": "business_asset_not_found: no matching owned, unexpired asset."
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        }
      },
      "delete": {
        "operationId": "deleteHostedBusinessAsset",
        "tags": ["Appearance and features"],
        "summary": "Delete an unused profile image or wallpaper",
        "description": "Deletes an owned asset only when no current or retained settings revision references it. Replacing a setting may retain its previous image for bounded rollback; no customer can read that image once it is no longer selected.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "profile:manage",
        "responses": {
          "204": {
            "description": "Unused asset deleted."
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner or selected sender authority does not permit this request."
          },
          "404": {
            "description": "business_asset_not_found."
          },
          "409": {
            "description": "business_asset_in_use: a retained settings revision still references the image."
          },
          "503": {
            "$ref": "#/components/responses/BusinessSettingsUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/media": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "post": {
        "operationId": "uploadHostedBusinessMedia",
        "tags": ["Message media"],
        "summary": "Upload media for one customer",
        "description": "Exactly two multipart parts: metadata JSON first, file binary second; reject extra parts and Content-Encoding. Metadata is at most 16,384 bytes. File limits: image/document/audio 26,214,400 bytes, video 47,185,920 bytes; complete HTTP body at most 47,218,688 bytes. Images must be JPEG/PNG with matching dimensions; MP4 must contain ftyp and mdat. For audio/mp4, duration/waveform are bounded caller metadata. A voice note may instead be uploaded as audio/ogg (Opus in Ogg, as WhatsApp and most TTS engines produce): the server checks that the bytes really are one complete Ogg/Opus stream, converts it once, before encryption, to mono AAC-LC m4a (24 kHz, 32 kbit/s), measures duration_ms and the 48-level waveform itself, and stores and delivers only the m4a; the response describes the converted file. Unsupported or corrupt audio answers 415 unsupported_audio; a note over 1 hour answers 422 audio_too_long. Upload is bound to this exact key, business, environment and contact. It does not send a message: POST messages with the returned media_id before expires_at. Two transfer slots per API runtime, within the 16-request pool; absolute 120-second transfer deadline.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "201": {
            "description": "Ready upload metadata. Use this exact key and recipient for the subsequent message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner, verified selected number, customer permission, block or feature setting does not permit this operation."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "415": {
            "description": "unsupported_audio: metadata.mime_type is audio/ogg but the file is not one complete, decodable Opus stream in Ogg (for example Vorbis or FLAC in Ogg, Opus in WebM, an MP4, a truncated or damaged file, or chained streams). Nothing was stored. Send real Ogg/Opus, or upload audio/mp4 instead.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["unsupported_audio"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "422": {
            "description": "audio_too_long: an audio/ogg voice note is longer than 3,600,000 ms (1 hour), measured from the stream and again from the decoded audio. Nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["audio_too_long"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        },
        "requestBody": {
          "required": true,
          "description": "Send metadata first as application/json, then file as binary with its declared MIME. Exactly two parts, in that order. No Content-Encoding.",
          "x-max-body-bytes": 47218688,
          "x-part-order": ["metadata", "file"],
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/MediaUpload"
              },
              "encoding": {
                "metadata": {
                  "contentType": "application/json"
                },
                "file": {
                  "contentType": "*/*"
                }
              }
            }
          }
        }
      }
    },
    "/v1/business/{environment}/media/{media_id}": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "media_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "description": "Canonical lowercase UUID returned by upload or in an inbound message event."
        }
      ],
      "get": {
        "operationId": "getHostedBusinessMedia",
        "tags": ["Message media"],
        "summary": "Read temporary media metadata",
        "description": "Current key, owner, selected verified number, business/environment, contact window, block and feature permissions are checked again before decrypted reads. Outbound handles are private to the exact uploading key; any current key with Messaging & calls (messaging:manage, or an older key's converted messages:send) in the same business/environment can read an admitted inbound handle. A handle does not grant permission or renew the contact window.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Ready media metadata; no plaintext file content.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner, verified selected number, customer permission, block or feature setting does not permit this operation."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/media/{media_id}/content": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "media_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "description": "Canonical lowercase UUID returned by upload or in an inbound message event."
        }
      ],
      "get": {
        "operationId": "downloadHostedBusinessMedia",
        "tags": ["Message media"],
        "summary": "Download received customer media",
        "description": "Current key, owner, selected verified number, business/environment, contact window, block and feature permissions are checked again before decrypted reads. Outbound handles are private to the exact uploading key; any current key with Messaging & calls (messaging:manage, or an older key's converted messages:send) in the same business/environment can read an admitted inbound handle. A handle does not grant permission or renew the contact window. Inbound only; outbound upload bytes are not exposed. Download before expires_at, validate the response and durably retain required bytes on your own server before ACK. Absolute 120-second deadline and two shared transfer slots. No redirect or public media URL.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Original inbound bytes with validated MIME and attachment disposition. Reading does not ACK the media.",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            },
            "headers": {
              "Content-Type": {
                "schema": {
                  "type": "string"
                },
                "description": "Validated original media MIME type, also present in media metadata."
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "attachment; filename is the validated media name."
              },
              "Content-Length": {
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 47185920
                },
                "description": "Plaintext byte count, matching the media metadata."
              },
              "X-Natter-Media-ID": {
                "schema": {
                  "type": "string",
                  "format": "uuid",
                  "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
                },
                "description": "Matches the requested temporary media handle."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner, verified selected number, customer permission, block or feature setting does not permit this operation."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/media/{media_id}/ack": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "media_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "description": "Canonical lowercase UUID returned by upload or in an inbound message event."
        }
      ],
      "post": {
        "operationId": "acknowledgeHostedBusinessMedia",
        "tags": ["Message media"],
        "summary": "Acknowledge received customer media",
        "description": "Current key, owner, selected verified number, business/environment, contact window, block and feature permissions are checked again before decrypted reads. Outbound handles are private to the exact uploading key; any current key with Messaging & calls (messaging:manage, or an older key's converted messages:send) in the same business/environment can read an admitted inbound handle. A handle does not grant permission or renew the contact window. Send no request body or transfer encoding. Inbound only; acknowledges the hosted endpoint target and removes its private descriptor/handle. It does not acknowledge other recipients. Webhook HTTP 2xx and a successful download are separate from this explicit media ACK. After ACK, the handle is unavailable; a repeat can return 404.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "204": {
            "description": "Inbound media acknowledged and private handle removed."
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "Current key permission, owner, verified selected number, customer permission, block or feature setting does not permit this operation."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/calls": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "listHostedBusinessCalls",
        "tags": ["Customer calls"],
        "summary": "List current customer calls",
        "description": "Only customer-initiated calls are supported. Requires a current full-access key or a restricted key with Messaging & calls (messaging:manage, or an older key's converted calls:manage), the same business/environment and a current consenting customer relationship; voice_calls/video_calls must allow the authenticated offer. No Business call creation, waiting, renegotiation or format-2 offer support. Returns at most eight current endpoint calls. Use this when no webhook is configured; not a call history. Listing authenticates the customer offers just like individual GET.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current call state or committed action receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "call_not_allowed or current Business authority: customer/contact/key/feature authority does not allow this action."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "409": {
            "description": "call_busy, call_changed or idempotency_conflict. Re-read current state; never change a body under an existing transaction."
          },
          "410": {
            "description": "call_ended: call is terminal or its current deadline expired. Stop local WebRTC and polling."
          },
          "422": {
            "description": "invalid_call_packet: native authenticated offer or signaling packet cannot be admitted."
          },
          "429": {
            "description": "call_limit: bounded call/signaling/TURN capacity reached.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "1"
                },
                "description": "Wait at least one second; retry only while the call remains active."
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/calls/{call_id}": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "call_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
            "maxLength": 96
          },
          "description": "Opaque call identifier returned by a call event or call list."
        }
      ],
      "get": {
        "operationId": "getHostedBusinessCall",
        "tags": ["Customer calls"],
        "summary": "Read a customer call and offer",
        "description": "Only customer-initiated calls are supported. Requires a current full-access key or a restricted key with Messaging & calls (messaging:manage, or an older key's converted calls:manage), the same business/environment and a current consenting customer relationship; voice_calls/video_calls must allow the authenticated offer. No Business call creation, waiting, renegotiation or format-2 offer support. Authenticates the native offer and records its native confirmation once before returning SDP. This does not answer the call. Your integration supplies its own WebRTC engine.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current call state or committed action receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "call_not_allowed or current Business authority: customer/contact/key/feature authority does not allow this action."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "409": {
            "description": "call_busy, call_changed or idempotency_conflict. Re-read current state; never change a body under an existing transaction."
          },
          "410": {
            "description": "call_ended: call is terminal or its current deadline expired. Stop local WebRTC and polling."
          },
          "422": {
            "description": "invalid_call_packet: native authenticated offer or signaling packet cannot be admitted."
          },
          "429": {
            "description": "call_limit: bounded call/signaling/TURN capacity reached.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "1"
                },
                "description": "Wait at least one second; retry only while the call remains active."
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/calls/{call_id}/answer": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "call_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
            "maxLength": 96
          },
          "description": "Opaque call identifier returned by a call event or call list."
        }
      ],
      "post": {
        "operationId": "answerHostedBusinessCall",
        "tags": ["Customer calls"],
        "summary": "Answer a customer call",
        "description": "Only customer-initiated calls are supported. Requires a current full-access key or a restricted key with Messaging & calls (messaging:manage, or an older key's converted calls:manage), the same business/environment and a current consenting customer relationship; voice_calls/video_calls must allow the authenticated offer. No Business call creation, waiting, renegotiation or format-2 offer support. Apply offer_sdp to your WebRTC engine and send its generated answer here. Reuse the exact transaction, key and body after a lost response. A changed request under the same transaction conflicts; no Idempotency-Key header is used for call actions.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current call state or committed action receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallReceipt"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "call_not_allowed or current Business authority: customer/contact/key/feature authority does not allow this action."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "409": {
            "description": "call_busy, call_changed or idempotency_conflict. Re-read current state; never change a body under an existing transaction."
          },
          "410": {
            "description": "call_ended: call is terminal or its current deadline expired. Stop local WebRTC and polling."
          },
          "422": {
            "description": "invalid_call_packet: native authenticated offer or signaling packet cannot be admitted."
          },
          "429": {
            "description": "call_limit: bounded call/signaling/TURN capacity reached.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "1"
                },
                "description": "Wait at least one second; retry only while the call remains active."
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        },
        "requestBody": {
          "required": true,
          "description": "JSON answer, at most 131,072 bytes; sdp itself at most 32,768 UTF-8 bytes.",
          "x-max-body-bytes": 131072,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CallAnswer"
              }
            }
          }
        }
      }
    },
    "/v1/business/{environment}/calls/{call_id}/signals": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "call_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
            "maxLength": 96
          },
          "description": "Opaque call identifier returned by a call event or call list."
        }
      ],
      "get": {
        "operationId": "getHostedBusinessCallSignals",
        "tags": ["Customer calls"],
        "summary": "Read customer ICE candidates",
        "description": "Only customer-initiated calls are supported. Requires a current full-access key or a restricted key with Messaging & calls (messaging:manage, or an older key's converted calls:manage), the same business/environment and a current consenting customer relationship; voice_calls/video_calls must allow the authenticated offer. No Business call creation, waiting, renegotiation or format-2 offer support. Returns at most ten signals. Always continue from next_after, including an empty page. No other query parameter is accepted; after must be canonical decimal 0..512 (no leading zero, sign, duplicate or percent encoding).",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current call state or committed action receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallSignals"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "call_not_allowed or current Business authority: customer/contact/key/feature authority does not allow this action."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "409": {
            "description": "call_busy, call_changed or idempotency_conflict. Re-read current state; never change a body under an existing transaction."
          },
          "410": {
            "description": "call_ended: call is terminal or its current deadline expired. Stop local WebRTC and polling."
          },
          "422": {
            "description": "invalid_call_packet: native authenticated offer or signaling packet cannot be admitted."
          },
          "429": {
            "description": "call_limit: bounded call/signaling/TURN capacity reached.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "1"
                },
                "description": "Wait at least one second; retry only while the call remains active."
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        },
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 512,
              "default": 0
            },
            "description": "Previous next_after cursor. Omit for the first page or send after=0; use the returned next_after even when signals is empty."
          }
        ]
      },
      "post": {
        "operationId": "sendHostedBusinessCallSignals",
        "tags": ["Customer calls"],
        "summary": "Send Business ICE candidates",
        "description": "Only customer-initiated calls are supported. Requires a current full-access key or a restricted key with Messaging & calls (messaging:manage, or an older key's converted calls:manage), the same business/environment and a current consenting customer relationship; voice_calls/video_calls must allow the authenticated offer. No Business call creation, waiting, renegotiation or format-2 offer support. Only after a successful answer. description_transaction must equal that answer transaction. At most 16 candidates; an empty array needs complete=true. Reuse the exact transaction/key/body after a lost response. At most 512 outgoing signaling frames per call.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current call state or committed action receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallReceipt"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "call_not_allowed or current Business authority: customer/contact/key/feature authority does not allow this action."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "409": {
            "description": "call_busy, call_changed or idempotency_conflict. Re-read current state; never change a body under an existing transaction."
          },
          "410": {
            "description": "call_ended: call is terminal or its current deadline expired. Stop local WebRTC and polling."
          },
          "422": {
            "description": "invalid_call_packet: native authenticated offer or signaling packet cannot be admitted."
          },
          "429": {
            "description": "call_limit: bounded call/signaling/TURN capacity reached.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "1"
                },
                "description": "Wait at least one second; retry only while the call remains active."
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        },
        "requestBody": {
          "required": true,
          "description": "JSON ICE batch, at most 32,768 bytes. The normalized ICE payload must fit 16,384 UTF-8 bytes.",
          "x-max-body-bytes": 32768,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CallSignalRequest"
              }
            }
          }
        }
      }
    },
    "/v1/business/{environment}/calls/{call_id}/heartbeat": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "call_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
            "maxLength": 96
          },
          "description": "Opaque call identifier returned by a call event or call list."
        }
      ],
      "post": {
        "operationId": "heartbeatHostedBusinessCall",
        "tags": ["Customer calls"],
        "summary": "Keep an answered call active",
        "description": "Only customer-initiated calls are supported. Requires a current full-access key or a restricted key with Messaging & calls (messaging:manage, or an older key's converted calls:manage), the same business/environment and a current consenting customer relationship; voice_calls/video_calls must allow the authenticated offer. No Business call creation, waiting, renegotiation or format-2 offer support. Send no body. Only the answered hosted endpoint can renew its 180-second lease. Send heartbeats before the returned expires_at; the customer lease and other call deadlines still apply.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current call state or committed action receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "call_not_allowed or current Business authority: customer/contact/key/feature authority does not allow this action."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "409": {
            "description": "call_busy, call_changed or idempotency_conflict. Re-read current state; never change a body under an existing transaction."
          },
          "410": {
            "description": "call_ended: call is terminal or its current deadline expired. Stop local WebRTC and polling."
          },
          "422": {
            "description": "invalid_call_packet: native authenticated offer or signaling packet cannot be admitted."
          },
          "429": {
            "description": "call_limit: bounded call/signaling/TURN capacity reached.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "1"
                },
                "description": "Wait at least one second; retry only while the call remains active."
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/calls/{call_id}/end": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "call_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
            "maxLength": 96
          },
          "description": "Opaque call identifier returned by a call event or call list."
        }
      ],
      "post": {
        "operationId": "endHostedBusinessCall",
        "tags": ["Customer calls"],
        "summary": "Decline or end a customer call",
        "description": "Only customer-initiated calls are supported. Requires a current full-access key or a restricted key with Messaging & calls (messaging:manage, or an older key's converted calls:manage), the same business/environment and a current consenting customer relationship; voice_calls/video_calls must allow the authenticated offer. No Business call creation, waiting, renegotiation or format-2 offer support. Use decline or hangup. Exact same-key transaction retries remain supported after private call state is erased, subject to retained receipts and current authority. Stop your WebRTC engine and heartbeats when terminal.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current call state or committed action receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallReceipt"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "call_not_allowed or current Business authority: customer/contact/key/feature authority does not allow this action."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "409": {
            "description": "call_busy, call_changed or idempotency_conflict. Re-read current state; never change a body under an existing transaction."
          },
          "410": {
            "description": "call_ended: call is terminal or its current deadline expired. Stop local WebRTC and polling."
          },
          "422": {
            "description": "invalid_call_packet: native authenticated offer or signaling packet cannot be admitted."
          },
          "429": {
            "description": "call_limit: bounded call/signaling/TURN capacity reached.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "1"
                },
                "description": "Wait at least one second; retry only while the call remains active."
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        },
        "requestBody": {
          "required": true,
          "description": "JSON end action, at most 1,024 bytes.",
          "x-max-body-bytes": 1024,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CallEnd"
              }
            }
          }
        }
      }
    },
    "/v1/business/{environment}/calls/{call_id}/turn": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        },
        {
          "name": "call_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
            "maxLength": 96
          },
          "description": "Opaque call identifier returned by a call event or call list."
        }
      ],
      "get": {
        "operationId": "getHostedBusinessCallTURN",
        "tags": ["Customer calls"],
        "summary": "Get short-lived TURN credentials",
        "description": "Only customer-initiated calls are supported. Requires a current full-access key or a restricted key with Messaging & calls (messaging:manage, or an older key's converted calls:manage), the same business/environment and a current consenting customer relationship; voice_calls/video_calls must allow the authenticated offer. No Business call creation, waiting, renegotiation or format-2 offer support. Provides TURN credentials for your WebRTC engine, scoped by current call authority. Keep credentials on the call integration; do not log or persist them beyond expiry.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "messaging:manage",
        "responses": {
          "200": {
            "description": "Current call state or committed action receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallTURN"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "call_not_allowed or current Business authority: customer/contact/key/feature authority does not allow this action."
          },
          "404": {
            "description": "not_found: no currently accessible resource in this business/environment."
          },
          "409": {
            "description": "call_busy, call_changed or idempotency_conflict. Re-read current state; never change a body under an existing transaction."
          },
          "410": {
            "description": "call_ended: call is terminal or its current deadline expired. Stop local WebRTC and polling."
          },
          "422": {
            "description": "invalid_call_packet: native authenticated offer or signaling packet cannot be admitted."
          },
          "429": {
            "description": "call_limit: bounded call/signaling/TURN capacity reached.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "const": "1"
                },
                "description": "Wait at least one second; retry only while the call remains active."
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/contacts": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "listHostedBusinessContacts",
        "tags": ["Account and receipts"],
        "summary": "List customers you can message",
        "description": "Read only this business and environment using Billing (billing:read). Lists customers who have messaged the business. A customer must message first; you may reply for 4392 hours (183 days) after their last message unless they opt out or block the business. Opening a chat does not count and such customers are not listed. Returns the per-business user_ref used by webhooks. phone is the customer's verified number exactly when their last message event delivered it to you (every business receives it, D412) and that identity is still current; null for a customer who opted out or whose last message event carried no number (only a message accepted before Business API 0.9.0). can_message reflects the window and opt-out only; a paused business or unavailable number can still refuse a send. Never grants sending permission. Paging: results are pages of at most limit customers in the requested order (last message or first contact, newest first; ties by user_ref). To read every customer, request without a cursor, then repeat the same request with cursor set to next_cursor until next_cursor is null; keep every other parameter unchanged, because a cursor belongs to its order. A customer who messages while you page moves to the front of the last-message order, so use first_contact order (which never moves) or the export to read a fixed set. counts covers every listed customer in this environment regardless of filter or page; for a business with 10,000 or more customers they may be up to 60 seconds older than as_of (see counts_as_of). match_count is how many customers match the status and any search or date filter; with a search or date filter it stops counting at 10,000 and match_count_exact is then false.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "customers:read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only customers with this status. Default all.",
            "schema": {
              "type": "string",
              "enum": ["all", "open", "expired", "opted_out"],
              "default": "all"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "last_inbound (default): most recent last message first. first_contact: most recent first contact first.",
            "schema": {
              "type": "string",
              "enum": ["last_inbound", "first_contact"],
              "default": "last_inbound"
            }
          },
          {
            "name": "user_ref_prefix",
            "in": "query",
            "required": false,
            "description": "Only customers whose user_ref starts with this value (case-sensitive). Pass a whole user_ref for an exact match.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 96,
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          },
          {
            "name": "phone",
            "in": "query",
            "required": false,
            "description": "Only the customer whose verified number is this E.164 number; finds only a customer whose number is returned in phone. URL-encode the leading + as %2B, or omit it.",
            "schema": {
              "type": "string",
              "pattern": "^\\+?[1-9][0-9]{6,14}$"
            }
          },
          {
            "name": "last_inbound_from",
            "in": "query",
            "required": false,
            "description": "Only customers whose last message is at or after this RFC 3339 instant (microsecond precision at most).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "last_inbound_to",
            "in": "query",
            "required": false,
            "description": "Only customers whose last message is before this RFC 3339 instant. Must be after last_inbound_from.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1-200 in canonical decimal. Default 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque next_cursor from the previous page of the same request. Repeat every other parameter unchanged; a cursor from one sort order is refused for the other.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256,
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Customers in this environment, with counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment (forbidden).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    },
    "/v1/business/{environment}/contacts/export": {
      "parameters": [
        {
          "name": "environment",
          "in": "path",
          "required": true,
          "description": "Exact environment partition. The key, contact, endpoint, configuration and operation must belong to it.",
          "schema": {
            "type": "string",
            "enum": ["sandbox", "live"]
          }
        }
      ],
      "get": {
        "operationId": "exportHostedBusinessContacts",
        "tags": ["Account and receipts"],
        "summary": "Download customers as CSV",
        "description": "Every customer matching the filter, as one streamed text/csv file, using account:read. Takes the same status, sort, user_ref_prefix, phone and last_inbound_from/last_inbound_to parameters as the list, but no limit or cursor. The file is one consistent snapshot: every customer matching at X-Natter-As-Of appears exactly once with the status at that instant. Columns: user_ref, status, last_inbound_at, window_expires_at (empty for an opted-out customer), first_contact_at, phone (empty where the customer's last message event carried no number, and for an opted-out customer). Timestamps are RFC 3339 UTC. A business may start 10 exports per UTC hour across Live, Test, the API and the owner portal; the eleventh is 429 export_limit with Retry-After. A download can take up to ten minutes; if anything fails after the first byte the connection is closed before the end of the file, so treat an incomplete transfer as a failed export.",
        "security": [
          {
            "businessApiKey": []
          }
        ],
        "x-required-permission": "customers:read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only customers with this status. Default all.",
            "schema": {
              "type": "string",
              "enum": ["all", "open", "expired", "opted_out"],
              "default": "all"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "last_inbound (default): most recent last message first. first_contact: most recent first contact first.",
            "schema": {
              "type": "string",
              "enum": ["last_inbound", "first_contact"],
              "default": "last_inbound"
            }
          },
          {
            "name": "user_ref_prefix",
            "in": "query",
            "required": false,
            "description": "Only customers whose user_ref starts with this value (case-sensitive). Pass a whole user_ref for an exact match.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 96,
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          },
          {
            "name": "phone",
            "in": "query",
            "required": false,
            "description": "Only the customer whose verified number is this E.164 number; finds only a customer whose number is returned in phone. URL-encode the leading + as %2B, or omit it.",
            "schema": {
              "type": "string",
              "pattern": "^\\+?[1-9][0-9]{6,14}$"
            }
          },
          {
            "name": "last_inbound_from",
            "in": "query",
            "required": false,
            "description": "Only customers whose last message is at or after this RFC 3339 instant (microsecond precision at most).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "last_inbound_to",
            "in": "query",
            "required": false,
            "description": "Only customers whose last message is before this RFC 3339 instant. Must be after last_inbound_from.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The CSV file, streamed.",
            "headers": {
              "Content-Disposition": {
                "description": "attachment; filename=\"natter-customers-<environment>-<YYYYMMDDTHHMMSSZ>.csv\"",
                "schema": {
                  "type": "string"
                }
              },
              "X-Natter-As-Of": {
                "description": "The instant every row's status was computed at (RFC 3339).",
                "schema": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "Header row user_ref,status,last_inbound_at,window_expires_at,first_contact_at,phone, then one row per customer."
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "The current key lacks the required permission or belongs to a different environment (forbidden).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["forbidden"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "description": "The business has started 10 exports in this UTC hour.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the next UTC hour.",
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 3600
                }
              },
              "X-Natter-Limit": {
                "description": "business_export_hour",
                "schema": {
                  "type": "string",
                  "const": "business_export_hour"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": ["export_limit"]
                    }
                  },
                  "required": ["error"]
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BusinessUnavailable"
          }
        }
      }
    }
  },
  "webhooks": {
    "hostedBusinessEvent": {
      "post": {
        "operationId": "receiveHostedBusinessEvent",
        "tags": ["Webhook configuration"],
        "summary": "Receive a signed message, status, incoming-call, consent or test event",
        "description": "Signed callback using the unchanged envelope and X-Natter-Signature scheme. Every inbound message event carries data.phone and data.identity_epoch, for every business, in sandbox and live; there is no per-business setting. A customer with no verified number on record cannot message a business (the Natter app is refused with number_verification_required and asks them to verify). Old queued message bodies remain unchanged across retries. No internal account/device IDs, private crypto material, Feeds or customer locations are exposed. Status and call events have no phone or identity_epoch fields.",
        "security": [
          {
            "webhookSignature": []
          }
        ],
        "parameters": [
          {
            "name": "X-Natter-Event-Id",
            "in": "header",
            "required": true,
            "description": "Matches body.id; stable across retry.",
            "schema": {
              "type": "string",
              "format": "uuid",
              "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "natter-webhooks/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/MessageEvent"
                  },
                  {
                    "$ref": "#/components/schemas/StatusEvent"
                  },
                  {
                    "$ref": "#/components/schemas/CallEvent"
                  },
                  {
                    "$ref": "#/components/schemas/ConsentEvent"
                  },
                  {
                    "$ref": "#/components/schemas/TestEvent"
                  }
                ],
                "discriminator": {
                  "propertyName": "kind",
                  "mapping": {
                    "message": "#/components/schemas/MessageEvent",
                    "status": "#/components/schemas/StatusEvent",
                    "call": "#/components/schemas/CallEvent",
                    "consent": "#/components/schemas/ConsentEvent",
                    "test": "#/components/schemas/TestEvent"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Event accepted by the customer's endpoint. Persist/deduplicate before replying; the response body is not consumed."
          },
          "default": {
            "description": "Not accepted. Non-2xx/transport failure may be retried within the absolute body lifetime; redirects are never followed."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "businessApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Exactly one Authorization header, at most 128 bytes, with case-sensitive prefix 'Bearer ' and raw key matching natter_<32 lowercase hex key id>_<43 unpadded base64url characters>. This is a Business API key, not a phone/device or Matrix bearer. Current environment, permissions, revocation, owner, selected number and Business status are read on every request. Every key is full access or restricted. A full-access key (the portal default) may call every operation; what it can do is limited only by the business's own state: feature switches (voice_calls/video_calls for calls, the media switches for attachments), status, environment, owner authority and selected number. A restricted key holds the permission groups the owner chose: messaging:manage (Messaging & calls), customers:read (Customers), billing:read (Billing), profile:manage (Profile & appearance), webhooks:manage (Webhook) and account:view (Account info). Every operation belongs to exactly one group, named in its x-required-permission. A restricted key from before the groups existed that could send messages but not answer calls (or the reverse) holds the converted messages:send (or calls:manage) and keeps exactly that half of Messaging & calls. Only the signed-in owner changes a key's access, in the Business portal; no API request changes access or permissions, and replayed creation requests never change them. Legacy/unclassified keys do not gain sandbox/live access. Key issuance uses the owner portal, not this public API."
      },
      "webhookSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Natter-Signature",
        "description": "Format t=<Unix seconds>,v1=<64 lowercase hex>. HMAC-SHA256 key is the UTF-8 signing_secret STRING returned by PUT, not its decoded 32 bytes. Signed bytes are ASCII decimal t, then '.', then the exact raw request body. Validate timestamp freshness under your replay policy and compare MAC in constant time; retain event ID deduplication independently. The secret is not a request bearer."
      }
    },
    "schemas": {
      "BusinessNumber": {
        "type": "object",
        "additionalProperties": false,
        "description": "A business phone number as metadata only. The number itself is never returned by the API.",
        "properties": {
          "number_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
            "description": "Opaque ID; sender_number_id on GET account refers to it."
          },
          "label": {
            "type": "string",
            "maxLength": 80,
            "description": "Owner label such as Support. Empty when none is set."
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Z]{2}$"
          },
          "status": {
            "type": "string",
            "enum": ["verified", "pending"],
            "description": "pending means the SMS check was started and not completed."
          },
          "verified_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "sender_environments": {
            "type": "array",
            "maxItems": 2,
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/Environment"
            },
            "description": "Environments that currently send from this number."
          }
        },
        "required": [
          "number_id",
          "label",
          "country",
          "status",
          "verified_at",
          "created_at",
          "sender_environments"
        ]
      },
      "BusinessNumberList": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "numbers": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/BusinessNumber"
            }
          }
        },
        "required": ["business_id", "environment", "numbers"]
      },
      "BusinessKey": {
        "type": "object",
        "additionalProperties": false,
        "description": "API key metadata. The secret, its hash and salt are never returned.",
        "properties": {
          "key_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "label": {
            "type": "string",
            "maxLength": 320
          },
          "prefix": {
            "type": "string",
            "pattern": "^natter_[0-9a-f]{8}$",
            "description": "The public start of the key (natter_ plus the first eight hex digits of key_id), for finding it in your configuration."
          },
          "access": {
            "type": "string",
            "enum": ["full", "restricted"],
            "description": "full: every operation, gated only by the business's feature switches and state. restricted: only the operations of the listed permission groups. Changed only by the owner in the Business portal."
          },
          "permissions": {
            "type": "array",
            "description": "The key's permission groups. A full-access key lists all six. A restricted key lists the groups the owner chose; an older restricted key may list a converted messages:send or calls:manage, half of Messaging & calls.",
            "minItems": 1,
            "maxItems": 6,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": [
                "messaging:manage",
                "customers:read",
                "billing:read",
                "profile:manage",
                "webhooks:manage",
                "account:view",
                "messages:send",
                "calls:manage"
              ]
            }
          },
          "scopes": {
            "type": "array",
            "description": "Compatibility list of the scope names used before permission groups. A name is listed only when the key may call every operation that scope used to allow: a full-access key lists all five; a key with Messaging & calls lists messages:send and calls:manage. account:read needs Account info, Billing, Customers, Profile & appearance and the messaging half of Messaging & calls. New integrations should read permissions. Whether calls can actually be answered also depends on the voice_calls/video_calls features reported by GET /account.",
            "minItems": 0,
            "maxItems": 5,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": [
                "account:read",
                "messages:send",
                "webhooks:manage",
                "profile:write",
                "calls:manage"
              ]
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Null when no use has been recorded."
          },
          "revoked_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "current": {
            "type": "boolean",
            "description": "True for the key that made this request."
          }
        },
        "required": [
          "key_id",
          "label",
          "prefix",
          "access",
          "permissions",
          "scopes",
          "created_at",
          "last_used_at",
          "revoked_at",
          "current"
        ]
      },
      "BusinessKeyList": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "keys": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/BusinessKey"
            }
          }
        },
        "required": ["business_id", "environment", "keys"]
      },
      "WebhookAttempt": {
        "type": "object",
        "additionalProperties": false,
        "description": "One POST to your endpoint. No payload, URL or response body is kept.",
        "properties": {
          "attempted_at": {
            "type": "string",
            "format": "date-time"
          },
          "event_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
            "description": "The X-Natter-Event-Id that was sent."
          },
          "kind": {
            "type": "string",
            "enum": ["message", "status", "call", "test", "consent"]
          },
          "attempt": {
            "type": "integer",
            "minimum": 1,
            "description": "1 for the first try of an event; retries count up."
          },
          "http_status": {
            "type": ["integer", "null"],
            "minimum": 100,
            "maximum": 599,
            "description": "Null when no HTTP response arrived."
          },
          "latency_ms": {
            "type": "integer",
            "minimum": 0
          },
          "error": {
            "type": ["string", "null"],
            "enum": [null, "http_failed", "transport_failed"],
            "description": "Null when delivered (2xx). http_failed: a non-2xx response, including redirects. transport_failed: DNS, refused destination, TLS, connection or timeout."
          },
          "delivered": {
            "type": "boolean"
          }
        },
        "required": [
          "attempted_at",
          "event_id",
          "kind",
          "attempt",
          "http_status",
          "latency_ms",
          "error",
          "delivered"
        ]
      },
      "WebhookDeliveries": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "deliveries": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/WebhookAttempt"
            },
            "description": "Newest first: at most the last 50 attempts of this environment from the last seven days."
          }
        },
        "required": ["deliveries"]
      },
      "WebhookTestResult": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "test": {
            "$ref": "#/components/schemas/WebhookAttempt"
          }
        },
        "required": ["test"]
      },
      "TestEvent": {
        "type": "object",
        "additionalProperties": false,
        "description": "Signed test event, sent only when you ask for one. It carries no customer, conversation or message: verify the signature, return 2xx and discard it.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "type": "string",
            "const": "test"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "environment": {
                "$ref": "#/components/schemas/Environment"
              }
            },
            "required": ["environment"]
          }
        },
        "required": ["id", "kind", "created_at", "business_id", "data"]
      },
      "Environment": {
        "type": "string",
        "enum": ["sandbox", "live"]
      },
      "BusinessStatus": {
        "type": "string",
        "enum": ["active", "throttled", "paused", "suspended", "banned"]
      },
      "ScopedUserRef": {
        "type": "string",
        "description": "Opaque reference scoped to this Business/environment/customer incarnation. Operation contact references are bounded to 256 UTF-8 bytes; the current POST recipient parser accepts only 1-96 ASCII identifier characters. References created by the private customer-entry primitive fit that parser.",
        "minLength": 1,
        "maxLength": 256,
        "x-max-utf8-bytes": 256
      },
      "ScopedConversationRef": {
        "type": "string",
        "description": "Opaque Business/environment conversation reference. This is not the internal relay conversation ID.",
        "minLength": 1
      },
      "Account": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "country": {
            "type": ["string", "null"],
            "pattern": "^[A-Z]{2}$",
            "description": "ISO 3166-1 alpha-2 country of the saved public profile address, or null when none is saved."
          },
          "environments": {
            "type": "array",
            "maxItems": 2,
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/Environment"
            },
            "description": "Environments this business has commissioned (live, sandbox). A key still works only in its own environment."
          },
          "features": {
            "$ref": "#/components/schemas/BusinessFeatures",
            "description": "This environment's saved feature switches, the same values as GET settings. capabilities is derived from them."
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/BusinessStatus"
          },
          "status_reason": {
            "type": "string",
            "description": "Optional current reason; omitted when empty."
          },
          "sender_number_id": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "Opaque ID of the selected verified sender for this environment, not the phone number."
          },
          "capabilities": {
            "type": "array",
            "minItems": 1,
            "maxItems": 7,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": [
                "text",
                "image",
                "document",
                "audio",
                "video",
                "voice_calls",
                "video_calls"
              ]
            },
            "description": "Always includes text. Additional values reflect this environment's currently enabled feature settings: images -> image, files -> document, voice_messages -> audio, videos -> video, voice_calls and video_calls. This reports enabled features, not the caller key's permissions or current contact permission; calls still require Messaging & calls."
          },
          "verified": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/BusinessVerified"
              }
            ],
            "description": "Read-only. An object only after a Masons Mail admin approved this business as verified; null otherwise. Automated checks alone never set it, and no API call changes it."
          },
          "customer_link": {
            "type": "object",
            "additionalProperties": false,
            "required": ["url", "active"],
            "properties": {
              "url": {
                "type": "string",
                "format": "uri",
                "description": "The permanent customer link for this environment: https://natterapp.com/business/{business_id}/{environment}. It never changes. Share it, print it as a QR code or put it on your website: on a phone with Natter it opens the conversation with you (after the customer reviews and accepts); without Natter it shows your display name, visible description and picture with the way to the App Store or Google Play."
              },
              "active": {
                "type": "boolean",
                "description": "Whether the link opens a conversation right now: the business is active or throttled, its owner authority is current, and this environment has a sender with a usable selected number. False while setup is incomplete, paused or suspended; the link itself stays the same."
              }
            },
            "description": "D423: the customer link of this environment (Business API 0.10.0)."
          }
        },
        "required": [
          "business_id",
          "environment",
          "name",
          "status",
          "sender_number_id",
          "capabilities",
          "country",
          "environments",
          "features",
          "verified",
          "customer_link"
        ]
      },
      "BusinessVerified": {
        "type": "object",
        "additionalProperties": false,
        "description": "The verified-business badge (Natter D405): when Masons Mail approved it and the plain facts it confirmed. Natter apps that read presentation level 8 receive the same value.",
        "properties": {
          "at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$",
            "description": "UTC approval time, second precision."
          },
          "checks": {
            "type": "array",
            "minItems": 1,
            "maxItems": 16,
            "items": {
              "type": "string",
              "pattern": "^(company_registration|domain:[a-z0-9.-]{3,253})$"
            },
            "description": "company_registration: Masons Mail confirmed the company's registration. domain:<domain>: the business proved control of that lower-case domain with a DNS TXT record."
          }
        },
        "required": ["at", "checks"]
      },
      "TextRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "to": {
            "type": "string",
            "description": "Use a scoped user_ref received from this Business's inbound message event. Arbitrary phone numbers are not resolved.",
            "minLength": 10,
            "maxLength": 105,
            "pattern": "^user_ref:[A-Za-z0-9_-]{1,96}$"
          },
          "type": {
            "type": "string",
            "const": "text"
          },
          "text": {
            "type": "string",
            "description": "Nonempty valid Unicode text, at most 4,096 UTF-16 code units. An astral character counts as two units. JSON Schema maxLength alone cannot express this exact runtime rule.",
            "minLength": 1,
            "maxLength": 4096,
            "x-max-utf16-code-units": 4096
          }
        },
        "required": ["to", "type", "text"],
        "title": "Text message"
      },
      "MessageReceipt": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "message_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "conversation_id": {
            "$ref": "#/components/schemas/ScopedConversationRef"
          },
          "user_ref": {
            "$ref": "#/components/schemas/ScopedUserRef"
          },
          "state": {
            "type": "string",
            "enum": ["queued", "accepted", "failed"],
            "description": "queued includes private preparation. accepted means the relay accepted the encrypted send. failed includes cancelled work."
          },
          "status": {
            "type": "string",
            "enum": ["sent", "read", "failed"],
            "description": "Omitted until a Business status exists. sent means relay acceptance, read means authenticated customer read. There is no Business delivered status."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "idempotency_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Original created_at plus 24 hours, unchanged by replay; receipt metadata may remain for up to 30 days."
          }
        },
        "required": [
          "message_id",
          "conversation_id",
          "user_ref",
          "state",
          "created_at",
          "idempotency_expires_at"
        ]
      },
      "WebhookPut": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "url": {
            "type": "string",
            "description": "HTTPS destination, at most 2,048 UTF-8 bytes, no user-info, fragment or opaque URI. Host must resolve only to permitted public addresses; private/local/special ranges and configured stack addresses are rejected. Path/query are preserved; canonical authority is stored. Explicit ports 1–65535 are supported.",
            "format": "uri",
            "minLength": 1,
            "maxLength": 2048,
            "x-max-utf8-bytes": 2048
          }
        },
        "required": ["url"]
      },
      "WebhookMetadata": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "url": {
            "type": "string",
            "description": "Canonical stored HTTPS destination; may contain customer query parameters.",
            "format": "uri"
          },
          "config_revision": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "description": "Monotonically increasing database revision. Every successful PUT advances it; delete/recreate does not reuse it."
          },
          "rotated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": ["url", "config_revision", "rotated_at"]
      },
      "WebhookRead": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "webhook": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/WebhookMetadata"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": ["webhook"]
      },
      "WebhookWrite": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "webhook": {
            "$ref": "#/components/schemas/WebhookMetadata"
          },
          "signing_secret": {
            "type": "string",
            "description": "Returned once after successful rotation. Save securely; unavailable from GET. It is the HMAC key as a string.",
            "minLength": 43,
            "maxLength": 43,
            "pattern": "^[A-Za-z0-9_-]{43}$",
            "readOnly": true
          }
        },
        "required": ["webhook", "signing_secret"]
      },
      "MessageEvent": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/TextMessageEvent"
          },
          {
            "$ref": "#/components/schemas/MediaMessageEvent"
          },
          {
            "$ref": "#/components/schemas/VerifiedTextMessageEvent"
          },
          {
            "$ref": "#/components/schemas/VerifiedMediaMessageEvent"
          }
        ],
        "description": "Sandbox and live use the same exact shapes. data.phone and data.identity_epoch are always present together, for every business: a customer without a verified number on record is refused before Natter accepts the message. The shape without both fields remains only for a message accepted before Business API 0.9.0. One field never appears alone. Media events expose a temporary handle and safe metadata, never file bytes, URLs, descriptors or keys. Status and call events are unchanged."
      },
      "StatusEvent": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "type": "string",
            "const": "status"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Equals data.at; sent/read use their retained transition time. Failed uses the authenticated failure projection's time."
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "message_id": {
                "type": "string",
                "format": "uuid",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
                "description": "Outbound Business operation ID returned by POST messages."
              },
              "conversation_id": {
                "$ref": "#/components/schemas/ScopedConversationRef"
              },
              "status": {
                "type": "string",
                "enum": ["sent", "read", "failed"],
                "description": "No delivered value. Historical status can be delivered after pause, STOP, an expired reply window or original key/device retirement; banned dispatch is denied. This does not authorize fresh content."
              },
              "at": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": ["message_id", "conversation_id", "status", "at"]
          }
        },
        "required": ["id", "kind", "created_at", "business_id", "data"]
      },
      "PublicBusinessProfile": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "display_name",
          "description",
          "website",
          "support_email",
          "support_phone",
          "address",
          "links"
        ],
        "properties": {
          "display_name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 128,
            "description": "At most 256 UTF-8 bytes. Separate from the registered business name."
          },
          "description": {
            "type": "string",
            "maxLength": 1000
          },
          "website": {
            "type": "string",
            "maxLength": 2048,
            "description": "Empty or an HTTPS URL without username or password."
          },
          "support_email": {
            "type": "string",
            "maxLength": 254
          },
          "support_phone": {
            "type": "string",
            "pattern": "^(\\+[1-9][0-9]{6,14})?$",
            "description": "Public support contact only; this does not prove number ownership or enable messaging."
          },
          "address": {
            "type": "object",
            "additionalProperties": false,
            "required": ["street", "city", "postal_code", "country"],
            "properties": {
              "street": {
                "type": "string",
                "maxLength": 200
              },
              "city": {
                "type": "string",
                "maxLength": 100
              },
              "postal_code": {
                "type": "string",
                "maxLength": 32
              },
              "country": {
                "type": "string",
                "pattern": "^([A-Z]{2})?$"
              }
            }
          },
          "links": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": ["label", "url"],
              "properties": {
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40
                },
                "url": {
                  "type": "string",
                  "maxLength": 2048,
                  "description": "Empty or an HTTPS URL without username or password.",
                  "minLength": 1
                }
              }
            }
          }
        }
      },
      "Pricing": {
        "type": "object",
        "properties": {
          "currency": {
            "type": "string",
            "const": "EUR"
          },
          "outbound_message_micro_eur": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 2147483647
              },
              {
                "type": "null"
              }
            ],
            "description": "Current unit price in millionths of one euro; null means unavailable, not free."
          },
          "revision": {
            "anyOf": [
              {
                "type": "integer",
                "exclusiveMinimum": 0,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ]
          },
          "sending_enabled": {
            "type": "boolean",
            "description": "A fresh price permits sending: zero always, a positive price while this business's prepaid credit or invoice credit limit covers a whole send. Account and customer permission checks still apply."
          },
          "payment_required": {
            "type": "boolean",
            "description": "True while the price is positive and this business's available funds cannot cover one send."
          }
        },
        "required": [
          "currency",
          "outbound_message_micro_eur",
          "revision",
          "sending_enabled",
          "payment_required"
        ],
        "additionalProperties": false
      },
      "UsageReport": {
        "type": "object",
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "environment": {
            "type": "string",
            "enum": ["sandbox", "live"]
          },
          "usage_period": {
            "type": "object",
            "properties": {
              "from": {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
              },
              "until": {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
              }
            },
            "required": ["from", "until"],
            "additionalProperties": false,
            "description": "Month-to-date UTC ledger period."
          },
          "metrics_period": {
            "type": "object",
            "properties": {
              "from": {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
              },
              "until": {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
              }
            },
            "required": ["from", "until"],
            "additionalProperties": false,
            "description": "Rolling seven-day operational period."
          },
          "pricing": {
            "type": "object",
            "properties": {
              "currency": {
                "type": "string",
                "const": "EUR"
              },
              "outbound_message_micro_eur": {
                "anyOf": [
                  {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 2147483647
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "revision": {
                "anyOf": [
                  {
                    "type": "integer",
                    "exclusiveMinimum": 0,
                    "maximum": 9007199254740991
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "sending_enabled": {
                "type": "boolean"
              },
              "payment_required": {
                "type": "boolean"
              }
            },
            "required": [
              "currency",
              "outbound_message_micro_eur",
              "revision",
              "sending_enabled",
              "payment_required"
            ],
            "additionalProperties": false
          },
          "usage": {
            "type": "object",
            "properties": {
              "outbound_messages": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "amount_micro_eur": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": ["outbound_messages", "amount_micro_eur"],
            "additionalProperties": false,
            "description": "Durable accepted outbound units and recorded price; zero-price events count."
          },
          "traffic": {
            "type": "object",
            "properties": {
              "incoming_messages": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "submitted_messages": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "accepted_messages": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "failed_messages": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "read_messages": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "pending_messages": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "retries": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "incoming_messages",
              "submitted_messages",
              "accepted_messages",
              "failed_messages",
              "read_messages",
              "pending_messages",
              "retries"
            ],
            "additionalProperties": false,
            "description": "Content-free operation counts in the operational period."
          },
          "processing": {
            "type": "object",
            "properties": {
              "samples": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "average_ms": {
                "anyOf": [
                  {
                    "type": "number",
                    "minimum": 0
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "p95_ms": {
                "anyOf": [
                  {
                    "type": "number",
                    "minimum": 0
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "maximum_ms": {
                "anyOf": [
                  {
                    "type": "number",
                    "minimum": 0
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": ["samples", "average_ms", "p95_ms", "maximum_ms"],
            "additionalProperties": false,
            "description": "Submission to server acceptance for accepted outbound samples; milliseconds, null without samples. Not phone delivery latency."
          },
          "webhooks": {
            "type": "object",
            "properties": {
              "pending": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "failed": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "retries": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": ["pending", "failed", "retries"],
            "additionalProperties": false,
            "description": "Current pending queue, failures retained within seven days, and retry attempts among those retained rows. Not lifetime webhook totals."
          },
          "daily": {
            "maxItems": 31,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "day": {
                  "type": "string",
                  "format": "date",
                  "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
                },
                "outbound_messages": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "amount_micro_eur": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                }
              },
              "required": ["day", "outbound_messages", "amount_micro_eur"],
              "additionalProperties": false
            },
            "description": "Month-to-date UTC daily ledger entries."
          },
          "history": {
            "$ref": "#/components/schemas/UsageHistory"
          }
        },
        "required": [
          "business_id",
          "environment",
          "usage_period",
          "metrics_period",
          "pricing",
          "usage",
          "traffic",
          "processing",
          "webhooks",
          "daily"
        ],
        "additionalProperties": false
      },
      "PricingReport": {
        "type": "object",
        "additionalProperties": false,
        "required": ["business_id", "environment", "pricing"],
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid"
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          }
        }
      },
      "UsageMonth": {
        "type": "object",
        "properties": {
          "month": {
            "type": "string",
            "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$",
            "description": "UTC calendar month."
          },
          "from": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "until": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
            "description": "Month end, or now for the current month."
          },
          "complete": {
            "type": "boolean",
            "description": "The month has ended; its figures are final."
          },
          "currency": {
            "type": "string",
            "const": "EUR"
          },
          "outbound_messages": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Accepted outbound sends from the durable usage ledger."
          },
          "amount_micro_eur": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000000,
            "description": "Sum of each send's price in force at acceptance, in millionths of one euro."
          },
          "outbound_by_type": {
            "type": "object",
            "properties": {
              "text": {
                "type": "object",
                "properties": {
                  "messages": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9007199254740991
                  },
                  "amount_micro_eur": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1000000000000000
                  }
                },
                "required": ["messages", "amount_micro_eur"],
                "additionalProperties": false
              },
              "image": {
                "type": "object",
                "properties": {
                  "messages": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9007199254740991
                  },
                  "amount_micro_eur": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1000000000000000
                  }
                },
                "required": ["messages", "amount_micro_eur"],
                "additionalProperties": false
              },
              "document": {
                "type": "object",
                "properties": {
                  "messages": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9007199254740991
                  },
                  "amount_micro_eur": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1000000000000000
                  }
                },
                "required": ["messages", "amount_micro_eur"],
                "additionalProperties": false
              },
              "audio": {
                "type": "object",
                "properties": {
                  "messages": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9007199254740991
                  },
                  "amount_micro_eur": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1000000000000000
                  }
                },
                "required": ["messages", "amount_micro_eur"],
                "additionalProperties": false
              },
              "video": {
                "type": "object",
                "properties": {
                  "messages": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9007199254740991
                  },
                  "amount_micro_eur": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1000000000000000
                  }
                },
                "required": ["messages", "amount_micro_eur"],
                "additionalProperties": false
              }
            },
            "required": ["text", "image", "document", "audio", "video"],
            "additionalProperties": false
          },
          "outbound_media_messages": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "outbound_media_bytes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "inbound_messages": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ],
            "description": "Customer messages counted from operation receipts, which are kept 720 hours. Null when the month began before that horizon. Inbound messages and calls are not metered."
          }
        },
        "required": [
          "month",
          "from",
          "until",
          "complete",
          "currency",
          "outbound_messages",
          "amount_micro_eur",
          "outbound_by_type",
          "outbound_media_messages",
          "outbound_media_bytes",
          "inbound_messages"
        ],
        "additionalProperties": false,
        "description": "One month of metered usage."
      },
      "UsageHistory": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"
          },
          "to": {
            "type": "string",
            "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"
          },
          "months": {
            "type": "array",
            "maxItems": 12,
            "items": {
              "$ref": "#/components/schemas/UsageMonth"
            }
          }
        },
        "required": ["from", "to", "months"],
        "additionalProperties": false,
        "description": "Monthly usage for the requested months, oldest first."
      },
      "BillingAccount": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": ["prepaid", "invoice"],
            "description": "prepaid (the default) spends credit; invoice is enabled by Masons Mail and accrues to a monthly invoice up to the credit limit."
          },
          "currency": {
            "type": "string",
            "const": "EUR"
          },
          "balance_micro_eur": {
            "type": "integer",
            "minimum": -1000000000000000,
            "maximum": 1000000000000000,
            "description": "Credits minus priced usage. Negative only in invoice mode."
          },
          "credit_limit_micro_eur": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000000,
            "description": "Invoice mode only; zero in prepaid mode."
          },
          "available_micro_eur": {
            "type": "integer",
            "minimum": -1000000000000000,
            "maximum": 1000000000000000,
            "description": "The balance, plus the credit limit in invoice mode."
          },
          "low_balance_threshold_micro_eur": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000000
          },
          "low_balance": {
            "type": "boolean",
            "description": "The price is positive and available funds are below the threshold."
          },
          "payment_required": {
            "type": "boolean",
            "description": "The price is positive and available funds cannot cover one send: Live sends return 402 payment_required."
          },
          "sending_enabled": {
            "type": "boolean"
          },
          "revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Settings revision; 0 means the untouched default."
          }
        },
        "required": [
          "mode",
          "currency",
          "balance_micro_eur",
          "credit_limit_micro_eur",
          "available_micro_eur",
          "low_balance_threshold_micro_eur",
          "low_balance",
          "payment_required",
          "sending_enabled",
          "revision"
        ],
        "additionalProperties": false,
        "description": "Billing account for this business and environment. Mode and limits are changed by Masons Mail or in the portal, never through the API."
      },
      "BillingRead": {
        "type": "object",
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "environment": {
            "type": "string",
            "enum": ["sandbox", "live"]
          },
          "billing": {
            "$ref": "#/components/schemas/BillingAccount"
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          }
        },
        "required": ["business_id", "environment", "billing", "pricing"],
        "additionalProperties": false
      },
      "LedgerEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^(?:usage|credit):[1-9][0-9]{0,18}$"
          },
          "kind": {
            "type": "string",
            "enum": ["usage", "top_up", "adjustment", "invoice_payment"]
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "amount_micro_eur": {
            "type": "integer",
            "minimum": -1000000000000000,
            "maximum": 1000000000000000,
            "description": "Signed: usage is a debit (zero while the price is zero), credits are positive, adjustments either sign."
          },
          "message_type": {
            "anyOf": [
              {
                "type": "string",
                "enum": ["text", "image", "document", "audio", "video"]
              },
              {
                "type": "null"
              }
            ],
            "description": "Usage entries only."
          },
          "invoice_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ],
            "description": "Invoice payment entries only."
          },
          "reason": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 500
              },
              {
                "type": "null"
              }
            ],
            "description": "Adjustment reason recorded by Masons Mail."
          },
          "balance_after_micro_eur": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -1000000000000000,
                "maximum": 1000000000000000
              },
              {
                "type": "null"
              }
            ],
            "description": "Credit entries only."
          }
        },
        "required": [
          "id",
          "kind",
          "at",
          "amount_micro_eur",
          "message_type",
          "invoice_id",
          "reason",
          "balance_after_micro_eur"
        ],
        "additionalProperties": false,
        "description": "One balance movement."
      },
      "LedgerRead": {
        "type": "object",
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "environment": {
            "type": "string",
            "enum": ["sandbox", "live"]
          },
          "ledger": {
            "type": "object",
            "properties": {
              "currency": {
                "type": "string",
                "const": "EUR"
              },
              "entries": {
                "type": "array",
                "maxItems": 100,
                "items": {
                  "$ref": "#/components/schemas/LedgerEntry"
                }
              },
              "next": {
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 64
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Pass as before= for the next (older) page; null at the end."
              }
            },
            "required": ["currency", "entries", "next"],
            "additionalProperties": false
          }
        },
        "required": ["business_id", "environment", "ledger"],
        "additionalProperties": false
      },
      "InvoiceLine": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string",
            "const": "outbound_message"
          },
          "message_type": {
            "type": "string",
            "enum": ["text", "image", "document", "audio", "video"]
          },
          "quantity": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "unit_price_micro_eur": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647
          },
          "amount_micro_eur": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000000
          }
        },
        "required": [
          "category",
          "message_type",
          "quantity",
          "unit_price_micro_eur",
          "amount_micro_eur"
        ],
        "additionalProperties": false
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "period": {
            "type": "string",
            "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$"
          },
          "period_start": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "period_end": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "mode": {
            "type": "string",
            "enum": ["prepaid", "invoice"],
            "description": "Prepaid records are issued paid: the usage was already paid from credit."
          },
          "currency": {
            "type": "string",
            "const": "EUR"
          },
          "outbound_messages": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "amount_micro_eur": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000000
          },
          "status": {
            "type": "string",
            "enum": ["open", "paid"]
          },
          "issued_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "due_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
              },
              {
                "type": "null"
              }
            ]
          },
          "paid_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
              },
              {
                "type": "null"
              }
            ]
          },
          "lines": {
            "type": "array",
            "maxItems": 16,
            "items": {
              "$ref": "#/components/schemas/InvoiceLine"
            }
          }
        },
        "required": [
          "id",
          "period",
          "period_start",
          "period_end",
          "mode",
          "currency",
          "outbound_messages",
          "amount_micro_eur",
          "status",
          "issued_at",
          "due_at",
          "paid_at",
          "lines"
        ],
        "additionalProperties": false,
        "description": "One record per closed UTC month, totalled from the same usage ledger as /usage. Issued about one hour after the month ends."
      },
      "InvoiceList": {
        "type": "object",
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "environment": {
            "type": "string",
            "enum": ["sandbox", "live"]
          },
          "invoices": {
            "type": "array",
            "maxItems": 36,
            "items": {
              "$ref": "#/components/schemas/Invoice"
            }
          }
        },
        "required": ["business_id", "environment", "invoices"],
        "additionalProperties": false
      },
      "InvoiceRead": {
        "type": "object",
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "environment": {
            "type": "string",
            "enum": ["sandbox", "live"]
          },
          "invoice": {
            "$ref": "#/components/schemas/Invoice"
          }
        },
        "required": ["business_id", "environment", "invoice"],
        "additionalProperties": false
      },
      "BusinessFeatures": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "images",
          "videos",
          "files",
          "voice_messages",
          "voice_calls",
          "video_calls"
        ],
        "properties": {
          "images": {
            "type": "boolean",
            "description": "Current permission. Defaults to false."
          },
          "videos": {
            "type": "boolean",
            "description": "Current permission. Defaults to false."
          },
          "files": {
            "type": "boolean",
            "description": "Current permission. Defaults to false."
          },
          "voice_messages": {
            "type": "boolean",
            "description": "Current permission. Defaults to false."
          },
          "voice_calls": {
            "type": "boolean",
            "description": "Current permission. Defaults to false."
          },
          "video_calls": {
            "type": "boolean",
            "description": "Current permission. Defaults to false."
          }
        }
      },
      "BusinessVisibility": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "description",
          "website",
          "support_email",
          "support_phone",
          "address",
          "country",
          "links"
        ],
        "properties": {
          "description": {
            "type": "boolean",
            "description": "Show this public section to customers. Defaults to true. Hidden values remain editable by the owner."
          },
          "website": {
            "type": "boolean",
            "description": "Show this public section to customers. Defaults to true. Hidden values remain editable by the owner."
          },
          "support_email": {
            "type": "boolean",
            "description": "Show this public section to customers. Defaults to true. Hidden values remain editable by the owner."
          },
          "support_phone": {
            "type": "boolean",
            "description": "Show this public section to customers. Defaults to true. Hidden values remain editable by the owner."
          },
          "address": {
            "type": "boolean",
            "description": "Show street, city and postal code. Country has its own independent switch."
          },
          "country": {
            "type": "boolean",
            "description": "Show this public section to customers. Defaults to true. Hidden values remain editable by the owner."
          },
          "links": {
            "type": "boolean",
            "description": "Show this public section to customers. Defaults to true. Hidden values remain editable by the owner."
          }
        }
      },
      "BusinessAppearance": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "avatar_asset_id",
          "chat_wallpaper_asset_id",
          "profile_background_asset_id",
          "accent_color",
          "background_color",
          "incoming_bubble_color",
          "outgoing_bubble_color"
        ],
        "properties": {
          "avatar_asset_id": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "A current asset uploaded for this exact business, environment and slot, or null."
          },
          "chat_wallpaper_asset_id": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "A current asset uploaded for this exact business, environment and slot, or null."
          },
          "profile_background_asset_id": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "A current asset uploaded for this exact business, environment and slot, or null."
          },
          "accent_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Six-digit RGB color."
          },
          "background_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Six-digit RGB color."
          },
          "incoming_bubble_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Six-digit RGB color."
          },
          "outgoing_bubble_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Six-digit RGB color."
          }
        }
      },
      "BusinessChatSettings": {
        "type": "object",
        "additionalProperties": false,
        "required": ["features", "appearance", "visibility"],
        "properties": {
          "features": {
            "$ref": "#/components/schemas/BusinessFeatures"
          },
          "appearance": {
            "$ref": "#/components/schemas/BusinessAppearance"
          },
          "visibility": {
            "$ref": "#/components/schemas/BusinessVisibility"
          }
        }
      },
      "BusinessDirectory": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "version",
          "business_id",
          "listed",
          "admin_unlisted",
          "searchable",
          "missing",
          "similar_to_verified"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "enum": [1]
          },
          "business_id": {
            "type": "string",
            "format": "uuid"
          },
          "listed": {
            "type": "boolean",
            "description": "The owner's switch (default true)."
          },
          "admin_unlisted": {
            "type": "boolean",
            "description": "Masons Mail removed the business from search."
          },
          "searchable": {
            "type": "boolean",
            "description": "Every condition below is met now."
          },
          "missing": {
            "type": "array",
            "maxItems": 7,
            "description": "What keeps the business out of name search now, in this order.",
            "items": {
              "type": "string",
              "enum": [
                "not_verified",
                "profile",
                "profile_picture",
                "live_number",
                "business_inactive",
                "owner_unlisted",
                "admin_unlisted"
              ]
            }
          },
          "similar_to_verified": {
            "type": "boolean",
            "description": "The display name is close to an earlier-verified business; shown with a warning and ranked below it."
          }
        }
      },
      "BusinessSettings": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "version",
          "business_id",
          "environment",
          "revision",
          "updated_at",
          "settings"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "business_id": {
            "type": "string",
            "format": "uuid"
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "updated_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "settings": {
            "$ref": "#/components/schemas/BusinessChatSettings"
          }
        }
      },
      "BusinessSettingsWrite": {
        "type": "object",
        "additionalProperties": false,
        "required": ["expected_revision", "settings"],
        "properties": {
          "expected_revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740990
          },
          "settings": {
            "$ref": "#/components/schemas/BusinessChatSettings"
          }
        }
      },
      "BusinessAsset": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "kind", "content_type", "bytes", "width", "height"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": ["avatar", "chat_wallpaper", "profile_background"]
          },
          "content_type": {
            "type": "string",
            "enum": ["image/png", "image/jpeg"]
          },
          "bytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2097152
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4096
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4096
          }
        }
      },
      "MediaMessageRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "to": {
            "type": "string",
            "description": "Use a scoped user_ref received from this Business's inbound message event. Arbitrary phone numbers are not resolved.",
            "minLength": 10,
            "maxLength": 105,
            "pattern": "^user_ref:[A-Za-z0-9_-]{1,96}$"
          },
          "type": {
            "type": "string",
            "enum": ["image", "document", "audio", "video"],
            "description": "Must match the uploaded media type and an enabled feature."
          },
          "media_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
            "description": "Ready outbound upload created by this exact API key for this exact business, environment and contact. Temporary; use before expires_at."
          },
          "text": {
            "type": "string",
            "maxLength": 4096,
            "x-max-utf16-code-units": 4096,
            "description": "Optional caption, at most 4,096 UTF-16 code units. Omit or send an empty string for no caption."
          }
        },
        "required": ["to", "type", "media_id"],
        "title": "Media message"
      },
      "MediaUploadImage": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "to": {
            "type": "string",
            "description": "Use a scoped user_ref received from this Business's inbound message event. Arbitrary phone numbers are not resolved.",
            "minLength": 10,
            "maxLength": 105,
            "pattern": "^user_ref:[A-Za-z0-9_-]{1,96}$"
          },
          "type": {
            "type": "string",
            "const": "image"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "x-max-utf8-bytes": 255,
            "description": "Safe filename, at most 255 UTF-8 bytes. No surrounding whitespace, slash, backslash, control characters, bidi overrides/isolates, or the names . and .. ."
          },
          "mime_type": {
            "type": "string",
            "enum": ["image/jpeg", "image/png"]
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "description": "Width in pixels. Width times height must not exceed 40,000,000 pixels; metadata must match the uploaded image."
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "description": "Height in pixels. Width times height must not exceed 40,000,000 pixels; metadata must match the uploaded image."
          },
          "duration_ms": {
            "type": "integer",
            "const": 0,
            "description": "Omit for this media type."
          },
          "waveform": {
            "type": "array",
            "maxItems": 0,
            "items": {
              "type": "integer"
            },
            "description": "Omit for this media type."
          }
        },
        "required": ["to", "type", "name", "mime_type", "width", "height"],
        "title": "Image upload metadata",
        "description": "File body must contain 1..26,214,400 bytes. Send the metadata JSON part before the file part."
      },
      "MediaUploadDocument": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "to": {
            "type": "string",
            "description": "Use a scoped user_ref received from this Business's inbound message event. Arbitrary phone numbers are not resolved.",
            "minLength": 10,
            "maxLength": 105,
            "pattern": "^user_ref:[A-Za-z0-9_-]{1,96}$"
          },
          "type": {
            "type": "string",
            "const": "document"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "x-max-utf8-bytes": 255,
            "description": "Safe filename, at most 255 UTF-8 bytes. No surrounding whitespace, slash, backslash, control characters, bidi overrides/isolates, or the names . and .. ."
          },
          "mime_type": {
            "type": "string",
            "maxLength": 127,
            "pattern": "^[a-z0-9][a-z0-9!#$&^_.+\\-]*/[a-z0-9][a-z0-9!#$&^_.+\\-]*$",
            "description": "Lowercase MIME type with no parameters, at most 127 bytes. image/, audio/ and video/ types must use their dedicated media variant.",
            "not": {
              "pattern": "^(image|audio|video)/"
            }
          },
          "width": {
            "type": "integer",
            "const": 0,
            "description": "Omit for this media type."
          },
          "height": {
            "type": "integer",
            "const": 0,
            "description": "Omit for this media type."
          },
          "duration_ms": {
            "type": "integer",
            "const": 0,
            "description": "Omit for this media type."
          },
          "waveform": {
            "type": "array",
            "maxItems": 0,
            "items": {
              "type": "integer"
            },
            "description": "Omit for this media type."
          }
        },
        "required": ["to", "type", "name", "mime_type"],
        "title": "Document upload metadata",
        "description": "File body must contain 1..26,214,400 bytes. Send the metadata JSON part before the file part."
      },
      "MediaUploadAudio": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "to": {
            "type": "string",
            "description": "Use a scoped user_ref received from this Business's inbound message event. Arbitrary phone numbers are not resolved.",
            "minLength": 10,
            "maxLength": 105,
            "pattern": "^user_ref:[A-Za-z0-9_-]{1,96}$"
          },
          "type": {
            "type": "string",
            "const": "audio"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "x-max-utf8-bytes": 255,
            "description": "Safe filename, at most 255 UTF-8 bytes. No surrounding whitespace, slash, backslash, control characters, bidi overrides/isolates, or the names . and .. ."
          },
          "mime_type": {
            "type": "string",
            "const": "audio/mp4"
          },
          "width": {
            "type": "integer",
            "const": 0,
            "description": "Omit for this media type."
          },
          "height": {
            "type": "integer",
            "const": 0,
            "description": "Omit for this media type."
          },
          "duration_ms": {
            "type": "integer",
            "minimum": 1,
            "maximum": 3600000,
            "description": "Clip duration in milliseconds, supplied by the caller."
          },
          "waveform": {
            "type": "array",
            "minItems": 48,
            "maxItems": 48,
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1024
            },
            "description": "Required audio waveform: exactly 48 integer levels, each 0..1024."
          }
        },
        "required": ["to", "type", "name", "mime_type", "duration_ms", "waveform"],
        "title": "Audio upload metadata",
        "description": "File body must contain 1..26,214,400 bytes. Send the metadata JSON part before the file part."
      },
      "MediaUploadVoiceOgg": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "to": {
            "type": "string",
            "description": "Use a scoped user_ref received from this Business's inbound message event. Arbitrary phone numbers are not resolved.",
            "minLength": 10,
            "maxLength": 105,
            "pattern": "^user_ref:[A-Za-z0-9_-]{1,96}$"
          },
          "type": {
            "type": "string",
            "const": "audio"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "x-max-utf8-bytes": 255,
            "description": "Safe filename, at most 255 UTF-8 bytes. No surrounding whitespace, slash, backslash, control characters, bidi overrides/isolates, or the names . and .. . The stored name ends in .m4a: a trailing .ogg, .opus or .oga is replaced, otherwise .m4a is appended (voice.m4a if that would exceed 255 bytes)."
          },
          "mime_type": {
            "type": "string",
            "enum": ["audio/ogg", "audio/ogg; codecs=opus"],
            "description": "Opus in Ogg. The declared type does not select the parser: the bytes are inspected as Ogg/Opus, and anything else answers 415 unsupported_audio."
          }
        },
        "required": ["to", "type", "name", "mime_type"],
        "title": "Voice note (Ogg/Opus) upload metadata",
        "description": "File body must contain 1..26,214,400 bytes of one complete Ogg/Opus stream, at most 1 hour long. Omit duration_ms, waveform, width and height: the server converts the note to mono AAC-LC audio/mp4 and measures duration and waveform from the decoded audio. Send the metadata JSON part before the file part."
      },
      "MediaUploadVideo": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "to": {
            "type": "string",
            "description": "Use a scoped user_ref received from this Business's inbound message event. Arbitrary phone numbers are not resolved.",
            "minLength": 10,
            "maxLength": 105,
            "pattern": "^user_ref:[A-Za-z0-9_-]{1,96}$"
          },
          "type": {
            "type": "string",
            "const": "video"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "x-max-utf8-bytes": 255,
            "description": "Safe filename, at most 255 UTF-8 bytes. No surrounding whitespace, slash, backslash, control characters, bidi overrides/isolates, or the names . and .. ."
          },
          "mime_type": {
            "type": "string",
            "const": "video/mp4"
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "description": "Width in pixels. Width times height must not exceed 40,000,000 pixels; metadata must match the uploaded image."
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "description": "Height in pixels. Width times height must not exceed 40,000,000 pixels; metadata must match the uploaded image."
          },
          "duration_ms": {
            "type": "integer",
            "minimum": 1,
            "maximum": 60000,
            "description": "Clip duration in milliseconds, supplied by the caller."
          },
          "waveform": {
            "type": "array",
            "maxItems": 0,
            "items": {
              "type": "integer"
            },
            "description": "Omit for this media type."
          }
        },
        "required": ["to", "type", "name", "mime_type", "width", "height", "duration_ms"],
        "title": "Video upload metadata",
        "description": "File body must contain 1..47,185,920 bytes. Send the metadata JSON part before the file part."
      },
      "MediaUploadMetadata": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/MediaUploadImage"
          },
          {
            "$ref": "#/components/schemas/MediaUploadDocument"
          },
          {
            "$ref": "#/components/schemas/MediaUploadAudio"
          },
          {
            "$ref": "#/components/schemas/MediaUploadVoiceOgg"
          },
          {
            "$ref": "#/components/schemas/MediaUploadVideo"
          }
        ],
        "description": "Metadata JSON, at most 16,384 UTF-8 bytes. Exactly one variant; fields depend on type."
      },
      "MediaUpload": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "metadata": {
            "$ref": "#/components/schemas/MediaUploadMetadata"
          },
          "file": {
            "type": "string",
            "format": "binary",
            "description": "Second and final multipart part, named file. Raw bytes matching metadata.mime_type; 25 MiB for image/document/audio or 45 MiB for video. An audio/ogg voice note is converted to audio/mp4 before it is stored. No base64 or remote URL."
          }
        },
        "required": ["metadata", "file"],
        "title": "Ordered multipart media upload"
      },
      "MediaView": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "media_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
            "description": "Opaque temporary handle; not a public URL or underlying encrypted blob ID."
          },
          "type": {
            "type": "string",
            "enum": ["image", "document", "audio", "video"]
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "x-max-utf8-bytes": 255,
            "description": "Safe filename, at most 255 UTF-8 bytes. No surrounding whitespace, slash, backslash, control characters, bidi overrides/isolates, or the names . and .. ."
          },
          "mime_type": {
            "type": "string",
            "description": "Validated original MIME. Uploaded images are JPEG/PNG; received images can also be WebP. Audio is audio/mp4, video is video/mp4, documents exclude image/audio/video MIME types."
          },
          "bytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 47185920,
            "description": "Plaintext byte count. At most 26,214,400 for image/document/audio, or 47,185,920 for video."
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "description": "Width in pixels. Width times height must not exceed 40,000,000 pixels; metadata must match the uploaded image."
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "description": "Height in pixels. Width times height must not exceed 40,000,000 pixels; metadata must match the uploaded image."
          },
          "duration_ms": {
            "type": "integer",
            "minimum": 1,
            "maximum": 3600000,
            "description": "Present only for audio/video. Audio at most 3,600,000 ms; video at most 60,000 ms."
          },
          "state": {
            "type": "string",
            "const": "ready"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Absolute handle deadline, no later than 24 hours or underlying media/upload expiry. Sending or ACK can remove it sooner."
          }
        },
        "required": [
          "media_id",
          "type",
          "name",
          "mime_type",
          "bytes",
          "state",
          "expires_at"
        ],
        "description": "Width and height occur only for image/video; duration_ms only for audio/video. No waveform, content URL, descriptor or encryption key is returned. An audio/ogg voice upload is described as the converted file: mime_type audio/mp4, the .m4a name, the converted byte count and the measured duration_ms."
      },
      "CallTransaction": {
        "type": "string",
        "pattern": "^(?!bc_)[A-Za-z0-9_-]{1,96}$",
        "minLength": 1,
        "maxLength": 96,
        "description": "Unique transaction for one call action. Reuse the exact value, key and unchanged body after a lost response; never reuse it for another action. Values starting bc_ are reserved."
      },
      "CallView": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "call_id": {
            "type": "string",
            "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
            "maxLength": 96
          },
          "user_ref": {
            "$ref": "#/components/schemas/ScopedUserRef"
          },
          "conversation_id": {
            "$ref": "#/components/schemas/ScopedConversationRef"
          },
          "state": {
            "type": "string",
            "enum": ["ringing", "answering", "answered", "ended"]
          },
          "media": {
            "type": "string",
            "enum": ["audio", "video"]
          },
          "offer_sdp": {
            "type": "string",
            "maxLength": 32768,
            "x-max-utf8-bytes": 32768,
            "description": "Authenticated customer SDP, when available. Pass it to your WebRTC implementation; never log or expose it in a webhook."
          },
          "offer_transaction": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,96}$",
            "minLength": 1,
            "maxLength": 96,
            "description": "Customer offer transaction. Incoming signal description_transaction refers to this value."
          },
          "answer_transaction": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,96}$",
            "minLength": 1,
            "maxLength": 96,
            "description": "Present after an answer; reference it when sending Business ICE candidates."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Call creation time."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Current ring, setup or active-call deadline. End the local session when this expires; no indefinite ringing or call lease."
          }
        },
        "required": [
          "call_id",
          "user_ref",
          "conversation_id",
          "state",
          "media",
          "offer_transaction",
          "created_at",
          "expires_at"
        ]
      },
      "CallList": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "calls": {
            "type": "array",
            "maxItems": 8,
            "items": {
              "$ref": "#/components/schemas/CallView"
            },
            "description": "At most eight current calls for this hosted endpoint, ordered by creation and call ID. No historical call archive."
          }
        },
        "required": ["calls"]
      },
      "CallAnswer": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "transaction": {
            "$ref": "#/components/schemas/CallTransaction"
          },
          "sdp": {
            "type": "string",
            "minLength": 3,
            "maxLength": 32768,
            "x-max-utf8-bytes": 32768,
            "pattern": "^v=0",
            "description": "Your WebRTC SDP answer: valid UTF-8, starts v=0, no NUL, at most 32,768 bytes. This endpoint does not generate the answer for you."
          }
        },
        "required": ["transaction", "sdp"]
      },
      "CallICECandidate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "candidate": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096,
            "x-max-utf8-bytes": 4096,
            "description": "Nonempty ICE candidate; valid UTF-8 with no control characters."
          },
          "sdp_mid": {
            "type": ["string", "null"],
            "minLength": 1,
            "maxLength": 64,
            "x-max-utf8-bytes": 64,
            "description": "SDP media identifier. Null or omitted when absent; a supplied string must be nonempty and contain no control characters."
          },
          "sdp_mline_index": {
            "type": ["integer", "null"],
            "minimum": 0,
            "maximum": 31,
            "description": "SDP media-line index; null or omitted when absent."
          },
          "username_fragment": {
            "type": ["string", "null"],
            "minLength": 1,
            "maxLength": 256,
            "x-max-utf8-bytes": 256,
            "description": "ICE username fragment. Null or omitted when absent; a supplied string must be nonempty and contain no control characters."
          }
        },
        "required": ["candidate"]
      },
      "CallSignalRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "transaction": {
            "$ref": "#/components/schemas/CallTransaction"
          },
          "description_transaction": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,96}$",
            "minLength": 1,
            "maxLength": 96,
            "description": "For Business POST, the successful answer transaction; for received customer signals, the customer offer transaction."
          },
          "candidates": {
            "type": "array",
            "maxItems": 16,
            "items": {
              "$ref": "#/components/schemas/CallICECandidate"
            },
            "description": "At most 16 candidates. An empty array is valid only with complete=true."
          },
          "complete": {
            "type": "boolean",
            "description": "True when ICE gathering for this description has completed."
          }
        },
        "required": ["transaction", "description_transaction", "candidates", "complete"],
        "description": "Complete request at most 32,768 bytes; the normalized ICE payload (description transaction, candidate fields and completion flag) must fit 16,384 UTF-8 bytes. No renegotiation; description_transaction must equal the successful answer transaction.",
        "anyOf": [
          {
            "properties": {
              "candidates": {
                "minItems": 1
              }
            }
          },
          {
            "properties": {
              "complete": {
                "const": true
              }
            }
          }
        ]
      },
      "CallSignal": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "sequence": {
            "type": "integer",
            "minimum": 1,
            "maximum": 512
          },
          "transaction": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,96}$",
            "minLength": 1,
            "maxLength": 96,
            "description": "Authenticated customer signal transaction."
          },
          "description_transaction": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,96}$",
            "minLength": 1,
            "maxLength": 96,
            "description": "For Business POST, the successful answer transaction; for received customer signals, the customer offer transaction."
          },
          "candidates": {
            "type": "array",
            "maxItems": 16,
            "items": {
              "$ref": "#/components/schemas/CallICECandidate"
            },
            "description": "At most 16 candidates. An empty array is valid only with complete=true."
          },
          "complete": {
            "type": "boolean",
            "description": "True when ICE gathering for this description has completed."
          }
        },
        "required": [
          "sequence",
          "transaction",
          "description_transaction",
          "candidates",
          "complete"
        ]
      },
      "CallSignals": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "signals": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/CallSignal"
            },
            "description": "At most ten authenticated customer signals per page."
          },
          "next_after": {
            "type": "integer",
            "minimum": 0,
            "maximum": 512,
            "description": "Always use this cursor for your next GET, even when signals is empty; it can advance past discarded entries."
          }
        },
        "required": ["signals", "next_after"]
      },
      "CallEnd": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "transaction": {
            "$ref": "#/components/schemas/CallTransaction"
          },
          "reason": {
            "type": "string",
            "enum": ["decline", "hangup"],
            "description": "Decline a ringing call or hang up an accepted call."
          }
        },
        "required": ["transaction", "reason"]
      },
      "CallReceipt": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "call_id": {
            "type": "string",
            "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
            "maxLength": 96
          },
          "transaction": {
            "$ref": "#/components/schemas/CallTransaction"
          },
          "action": {
            "type": "string",
            "enum": ["answer", "signal", "end"]
          },
          "applied_at": {
            "type": "string",
            "format": "date-time",
            "description": "Time the action was committed; retries preserve it."
          },
          "ring_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Original ringing deadline; active call lifetime is in CallView.expires_at."
          },
          "sequence": {
            "type": "integer",
            "minimum": 0
          },
          "replayed": {
            "type": "boolean",
            "description": "True for a matching transaction retry. A different key/body/action under the transaction conflicts."
          },
          "claim_transaction": {
            "type": ["string", "null"],
            "description": "Native answer-claim receipt, when applicable; otherwise null. Do not invent or modify it."
          },
          "setup_expires_at": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Setup deadline when applicable; otherwise null."
          }
        },
        "required": [
          "call_id",
          "transaction",
          "action",
          "applied_at",
          "ring_expires_at",
          "sequence",
          "replayed",
          "claim_transaction",
          "setup_expires_at"
        ]
      },
      "CallTURN": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "uris": {
            "type": "array",
            "minItems": 2,
            "maxItems": 2,
            "items": {
              "type": "string"
            },
            "description": "TURN URIs for UDP and TCP transports."
          },
          "username": {
            "type": "string",
            "description": "Short-lived TURN credential; treat as secret."
          },
          "password": {
            "type": "string",
            "description": "Short-lived TURN credential; treat as secret."
          },
          "ttl": {
            "type": "integer",
            "minimum": 60,
            "maximum": 600,
            "description": "Credential lifetime in seconds, subject to server configuration."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Absolute TURN credential expiry."
          }
        },
        "required": ["uris", "username", "password", "ttl", "expires_at"]
      },
      "TextMessageEvent": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "type": "string",
            "const": "message"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Original relay accepted timestamp, equal to data.received_at. It is not webhook handoff or delivery-attempt time."
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "message_id": {
                "type": "string",
                "format": "uuid",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
                "description": "Opaque inbound Business operation reference, not the internal/native message ID."
              },
              "conversation_id": {
                "$ref": "#/components/schemas/ScopedConversationRef"
              },
              "user_ref": {
                "$ref": "#/components/schemas/ScopedUserRef"
              },
              "type": {
                "type": "string",
                "const": "text"
              },
              "text": {
                "type": "string",
                "description": "Native-accepted customer text, at most 16,384 UTF-8 bytes. Inbound capacity differs from the 4,096 UTF-16-unit outbound API limit.",
                "maxLength": 16384,
                "x-max-utf8-bytes": 16384
              },
              "received_at": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "message_id",
              "conversation_id",
              "user_ref",
              "type",
              "text",
              "received_at"
            ]
          }
        },
        "required": ["id", "kind", "created_at", "business_id", "data"],
        "title": "Text message event (customer without a verified number)"
      },
      "MediaMessageEvent": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "type": "string",
            "const": "message"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Original relay accepted timestamp, equal to data.received_at. It is not webhook handoff or delivery-attempt time."
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "message_id": {
                "type": "string",
                "format": "uuid",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
                "description": "Opaque inbound Business operation reference, not the internal/native message ID."
              },
              "conversation_id": {
                "$ref": "#/components/schemas/ScopedConversationRef"
              },
              "user_ref": {
                "$ref": "#/components/schemas/ScopedUserRef"
              },
              "type": {
                "type": "string",
                "enum": ["image", "document", "audio", "video"]
              },
              "text": {
                "type": "string",
                "description": "Optional caption represented as a string, empty when absent; at most 16,384 UTF-8 bytes.",
                "maxLength": 16384,
                "x-max-utf8-bytes": 16384
              },
              "received_at": {
                "type": "string",
                "format": "date-time"
              },
              "media": {
                "$ref": "#/components/schemas/MediaView"
              }
            },
            "required": [
              "message_id",
              "conversation_id",
              "user_ref",
              "type",
              "text",
              "received_at",
              "media"
            ]
          }
        },
        "required": ["id", "kind", "created_at", "business_id", "data"],
        "title": "Media message event (customer without a verified number)"
      },
      "CallEvent": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "type": "string",
            "const": "call"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Original call creation time."
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "call_id": {
                "type": "string",
                "pattern": "^call_[A-Za-z0-9_-]{1,91}$",
                "maxLength": 96
              },
              "conversation_id": {
                "$ref": "#/components/schemas/ScopedConversationRef"
              },
              "user_ref": {
                "$ref": "#/components/schemas/ScopedUserRef"
              },
              "state": {
                "type": "string",
                "const": "ringing"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "description": "Ringing deadline. Stale or ended notifications are discarded before dispatch."
              }
            },
            "required": ["call_id", "conversation_id", "user_ref", "state", "expires_at"]
          }
        },
        "required": ["id", "kind", "created_at", "business_id", "data"],
        "description": "Signed incoming-call notification. No SDP, ICE candidates, media encryption keys, native account/device IDs or phone numbers. Read the call over authenticated API before acting."
      },
      "VerifiedTextMessageEvent": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "type": "string",
            "const": "message"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Original relay accepted timestamp, equal to data.received_at. It is not webhook handoff or delivery-attempt time."
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "message_id": {
                "type": "string",
                "format": "uuid",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
                "description": "Opaque inbound Business operation reference, not the internal/native message ID."
              },
              "conversation_id": {
                "$ref": "#/components/schemas/ScopedConversationRef"
              },
              "user_ref": {
                "$ref": "#/components/schemas/ScopedUserRef"
              },
              "type": {
                "type": "string",
                "const": "text"
              },
              "text": {
                "type": "string",
                "description": "Native-accepted customer text, at most 16,384 UTF-8 bytes. Inbound capacity differs from the 4,096 UTF-16-unit outbound API limit.",
                "maxLength": 16384,
                "x-max-utf8-bytes": 16384
              },
              "received_at": {
                "type": "string",
                "format": "date-time"
              },
              "phone": {
                "type": "string",
                "pattern": "^\\+[1-9][0-9]{6,14}$",
                "maxLength": 16,
                "description": "Current sender number in E.164, obtained only from Natter-verified SMS or WhatsApp proof. Never user-entered profile data. Always present, together with identity_epoch."
              },
              "identity_epoch": {
                "type": "string",
                "format": "uuid",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
                "description": "Opaque identity-continuity token. Compare for equality only; a changed value invalidates any account link previously inferred from the same phone number. Rotates for a new account or successful number verification on a different device. Reconnect and a trusted device link do not rotate it."
              }
            },
            "required": [
              "message_id",
              "conversation_id",
              "user_ref",
              "type",
              "text",
              "received_at",
              "phone",
              "identity_epoch"
            ]
          }
        },
        "required": ["id", "kind", "created_at", "business_id", "data"],
        "title": "Text message event with verified identity"
      },
      "VerifiedMediaMessageEvent": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "type": "string",
            "const": "message"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Original relay accepted timestamp, equal to data.received_at. It is not webhook handoff or delivery-attempt time."
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "message_id": {
                "type": "string",
                "format": "uuid",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
                "description": "Opaque inbound Business operation reference, not the internal/native message ID."
              },
              "conversation_id": {
                "$ref": "#/components/schemas/ScopedConversationRef"
              },
              "user_ref": {
                "$ref": "#/components/schemas/ScopedUserRef"
              },
              "type": {
                "type": "string",
                "enum": ["image", "document", "audio", "video"]
              },
              "text": {
                "type": "string",
                "description": "Optional caption represented as a string, empty when absent; at most 16,384 UTF-8 bytes.",
                "maxLength": 16384,
                "x-max-utf8-bytes": 16384
              },
              "received_at": {
                "type": "string",
                "format": "date-time"
              },
              "media": {
                "$ref": "#/components/schemas/MediaView"
              },
              "phone": {
                "type": "string",
                "pattern": "^\\+[1-9][0-9]{6,14}$",
                "maxLength": 16,
                "description": "Current sender number in E.164, obtained only from Natter-verified SMS or WhatsApp proof. Never user-entered profile data. Always present, together with identity_epoch."
              },
              "identity_epoch": {
                "type": "string",
                "format": "uuid",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
                "description": "Opaque identity-continuity token. Compare for equality only; a changed value invalidates any account link previously inferred from the same phone number. Rotates for a new account or successful number verification on a different device. Reconnect and a trusted device link do not rotate it."
              }
            },
            "required": [
              "message_id",
              "conversation_id",
              "user_ref",
              "type",
              "text",
              "received_at",
              "media",
              "phone",
              "identity_epoch"
            ]
          }
        },
        "required": ["id", "kind", "created_at", "business_id", "data"],
        "title": "Media message event with verified identity"
      },
      "BusinessWelcome": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "text",
          "terms_url",
          "privacy_url",
          "require_agreement",
          "reask_on_change"
        ],
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000,
            "description": "Plain text shown after Natter's fixed Business notice. At most 1000 UTF-16 code units, no leading or trailing white space, no control characters other than line breaks and tabs. Not markdown or HTML."
          },
          "terms_url": {
            "type": ["string", "null"],
            "format": "uri",
            "pattern": "^https://",
            "maxLength": 2048,
            "description": "Optional link to your terms, validated like the profile website (https only); null when absent."
          },
          "privacy_url": {
            "type": ["string", "null"],
            "format": "uri",
            "pattern": "^https://",
            "maxLength": 2048,
            "description": "Optional link to your privacy policy, validated like the profile website (https only); null when absent."
          },
          "require_agreement": {
            "type": "boolean",
            "description": "true: the customer must tap Agree and continue before chatting, and both sides' new messages are refused until they do. false: shown once, for information."
          },
          "reask_on_change": {
            "type": "boolean",
            "description": "When a later save changes the text or a link, customers who already agreed must agree again."
          }
        }
      },
      "BusinessWelcomeSettings": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "version",
          "business_id",
          "environment",
          "revision",
          "updated_at",
          "welcome",
          "content_version",
          "agreements_valid_from"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "business_id": {
            "type": "string",
            "format": "uuid"
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "updated_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "welcome": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/BusinessWelcome"
              },
              {
                "type": "null"
              }
            ],
            "description": "null: no welcome screen (never set or turned off)."
          },
          "content_version": {
            "type": ["string", "null"],
            "pattern": "^[0-9a-f]{64}$",
            "description": "hex(SHA-256(\"natter-welcome-v1\" NUL text NUL terms_url NUL privacy_url)), an absent link being empty. The version customers agree to; null without a welcome."
          },
          "agreements_valid_from": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Agreements recorded at or after this instant count. It moves when the welcome is first set (or set again after being off) and when the content changes with reask_on_change."
          }
        }
      },
      "BusinessWelcomeWrite": {
        "type": "object",
        "additionalProperties": false,
        "required": ["expected_revision", "welcome"],
        "properties": {
          "expected_revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740990
          },
          "welcome": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/BusinessWelcome"
              },
              {
                "type": "null"
              }
            ],
            "description": "null turns the welcome screen off."
          }
        }
      },
      "ContactConsent": {
        "type": "object",
        "additionalProperties": false,
        "required": ["version", "agreed_at"],
        "properties": {
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "The welcome content_version the customer agreed to."
          },
          "agreed_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ConsentEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "kind", "created_at", "business_id", "data"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "type": "string",
            "const": "consent"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Equal to data.agreed_at."
          },
          "business_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "conversation_id",
              "user_ref",
              "phone",
              "identity_epoch",
              "agreed_at",
              "version",
              "welcome_text_hash",
              "terms_url",
              "privacy_url"
            ],
            "properties": {
              "conversation_id": {
                "$ref": "#/components/schemas/ScopedConversationRef"
              },
              "user_ref": {
                "$ref": "#/components/schemas/ScopedUserRef"
              },
              "phone": {
                "type": "string",
                "pattern": "^\\+[1-9][0-9]{6,14}$",
                "description": "The customer's verified number (E.164), as on message events."
              },
              "identity_epoch": {
                "type": "string",
                "format": "uuid",
                "description": "The verified identity epoch, as on message events. A changed epoch means a different holder."
              },
              "profile_name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 64,
                "description": "Present only when the customer's app shared a display name."
              },
              "agreed_at": {
                "type": "string",
                "format": "date-time"
              },
              "version": {
                "type": "string",
                "pattern": "^[0-9a-f]{64}$",
                "description": "The welcome content_version agreed to."
              },
              "welcome_text_hash": {
                "type": "string",
                "pattern": "^[0-9a-f]{64}$",
                "description": "hex(SHA-256(UTF-8 welcome text))."
              },
              "terms_url": {
                "type": ["string", "null"],
                "description": "The terms link shown, or null."
              },
              "privacy_url": {
                "type": ["string", "null"],
                "description": "The privacy link shown, or null."
              }
            }
          }
        },
        "description": "Signed record of a customer agreeing to your welcome terms. Natter keeps only {version, agreed_at}; store this event if you need the record. Same signature, retries, event-ID deduplication and delivery log as other events. A repeat agreement to the same counting version emits nothing."
      },
      "Contact": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "user_ref",
          "first_contact_at",
          "last_inbound_at",
          "window_expires_at",
          "status",
          "can_message",
          "opted_out",
          "opted_out_at",
          "phone",
          "consent"
        ],
        "properties": {
          "user_ref": {
            "$ref": "#/components/schemas/ScopedUserRef"
          },
          "first_contact_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this customer first opened the chat with the business."
          },
          "last_inbound_at": {
            "type": "string",
            "format": "date-time",
            "description": "This customer's last message to you."
          },
          "window_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "last_inbound_at plus 4392 hours. At or after this instant you can no longer message this customer until they message again."
          },
          "status": {
            "type": "string",
            "enum": ["open", "expired", "opted_out"],
            "description": "open: window_expires_at is after as_of and the customer has not opted out. opted_out takes precedence over the window."
          },
          "can_message": {
            "type": "boolean",
            "description": "True exactly when status is open."
          },
          "opted_out": {
            "type": "boolean",
            "description": "The customer typed a stop word, turned replies off or blocked the business."
          },
          "opted_out_at": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the customer opted out; null when not opted out or for a block."
          },
          "phone": {
            "type": ["string", "null"],
            "pattern": "^\\+[1-9][0-9]{6,14}$",
            "description": "The verified number this customer's last message event delivered to you, while that identity is still current; null otherwise and always null for an opted-out customer."
          },
          "consent": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ContactConsent"
              },
              {
                "type": "null"
              }
            ],
            "description": "The customer's last agreement to your welcome terms, or null. Natter keeps only this marker; the full record was sent to you as the consent webhook event. Compare version with the current content_version."
          }
        }
      },
      "ContactCounts": {
        "type": "object",
        "additionalProperties": false,
        "required": ["open", "expiring_within_7_days", "expired", "opted_out", "total"],
        "properties": {
          "open": {
            "type": "integer",
            "minimum": 0
          },
          "expiring_within_7_days": {
            "type": "integer",
            "minimum": 0,
            "description": "Open customers whose window closes within 168 hours of as_of."
          },
          "expired": {
            "type": "integer",
            "minimum": 0
          },
          "opted_out": {
            "type": "integer",
            "minimum": 0
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "open + expired + opted_out."
          }
        }
      },
      "ContactList": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "business_id",
          "environment",
          "as_of",
          "window_hours",
          "status",
          "sort",
          "counts",
          "counts_as_of",
          "match_count",
          "match_count_exact",
          "contacts",
          "next_cursor"
        ],
        "properties": {
          "business_id": {
            "type": "string",
            "format": "uuid"
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "The single instant used for every status in this response."
          },
          "window_hours": {
            "type": "integer",
            "const": 4392
          },
          "status": {
            "type": "string",
            "enum": ["all", "open", "expired", "opted_out"]
          },
          "sort": {
            "type": "string",
            "enum": ["last_inbound", "first_contact"]
          },
          "counts": {
            "$ref": "#/components/schemas/ContactCounts"
          },
          "counts_as_of": {
            "type": "string",
            "format": "date-time",
            "description": "When counts were computed: as_of, or up to 60 seconds earlier for a business with 10,000 or more customers."
          },
          "match_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Customers matching status plus any search or date filter; at most 10,000 when match_count_exact is false."
          },
          "match_count_exact": {
            "type": "boolean",
            "description": "False when a search or date filter matched more than 10,000 customers."
          },
          "contacts": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/Contact"
            }
          },
          "next_cursor": {
            "type": ["string", "null"],
            "description": "Pass as cursor for the next page; null on the last page."
          }
        }
      }
    },
    "responses": {
      "SaveLimited": {
        "description": "business_save_limit: the business has used today’s saves. Per UTC calendar day a business may make 5 text saves (profile, colours, features and visibility together) and 2 picture uploads (avatar, chat wallpaper, profile background), 1 once it has more than 100,000 customers. Live, Test and the owner portal share these budgets. Nothing was written. Wait Retry-After seconds (until the next UTC midnight), then send the same request again.",
        "headers": {
          "Retry-After": {
            "description": "Whole seconds until the next UTC midnight, when the budget resets (1 to 86400).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 86400
            }
          },
          "X-Natter-Limit": {
            "description": "Which daily budget refused the save.",
            "schema": {
              "type": "string",
              "enum": ["business_text_day", "business_picture_day"]
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": ["error", "retry_after_s"],
              "properties": {
                "error": {
                  "type": "string",
                  "const": "business_save_limit"
                },
                "retry_after_s": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 86400
                }
              }
            }
          }
        }
      },
      "InvalidRequest": {
        "description": "Invalid request body, header or noncanonical path. Oversized JSON also uses this response.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "error": {
                  "type": "string",
                  "enum": ["invalid_request"]
                }
              },
              "required": ["error"]
            }
          }
        }
      },
      "InvalidApiKey": {
        "description": "Missing, malformed, unknown or revoked Business API key.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "error": {
                  "type": "string",
                  "enum": ["invalid_api_key"]
                }
              },
              "required": ["error"]
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "The method is not supported for this exact route.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "error": {
                  "type": "string",
                  "enum": ["method_not_allowed"]
                }
              },
              "required": ["error"]
            }
          }
        },
        "headers": {
          "Allow": {
            "description": "The supported method(s).",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "BusinessUnavailable": {
        "description": "Candidate private HTTP resource controls: PostgreSQL validation and runtime publication are pending. The 16-request and 20-second limits below are not guarantees of the current deployed service. A dependency, transport deadline capability or required admission policy is unavailable, or the composed API's shared per-instance cap of 16 active requests is full. Local capacity refusal happens before authentication/body work and supplies Retry-After: 1. An early HTTP/1 refusal closes its connection without waiting for the remaining upload; HTTP/2 refusal is stream-scoped. This is not a tenant quota or paid throughput tier. Requests have an absolute 20-second read/write deadline, shortened by an earlier caller deadline; cancellation interrupts admitted I/O. A timeout does not prove that an action failed to commit: preserve the original send idempotency key/body on retry. Webhook PUT retry rotates its secret again. Distributed tenant/key/recipient rate limits, queue limits and fair scheduling remain commissioning gates. No success is implied by this error. temporarily_unavailable (D393): the key, scope, environment and number are allowed, but the business owner authority lease lapsed moments ago and has not been renewed yet (for example while a Natter or Masons Mail service restarts). Nothing was accepted. It always carries Retry-After: 5; retry the same request (a send with its original idempotency key and body) after that delay. A revoked or suspended owner, or a lease lapsed for more than two minutes, is refused with 403 forbidden instead and is not retryable.",
        "headers": {
          "Retry-After": {
            "description": "1 for local resource or deadline-capability refusal, 5 for temporarily_unavailable. Wait at least this many seconds before retrying; it is not a distributed quota reset time. Other dependency refusals may omit this header.",
            "schema": {
              "type": "string",
              "enum": ["1", "5"]
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "error": {
                  "type": "string",
                  "enum": ["business_unavailable", "temporarily_unavailable"]
                }
              },
              "required": ["error"]
            }
          }
        }
      },
      "BusinessSettingsUnavailable": {
        "description": "Business settings are temporarily unavailable (business_settings_unavailable or business_unavailable). temporarily_unavailable (D393): the business owner authority lease lapsed moments ago and is being renewed; it always carries Retry-After: 5. Nothing was written. Retry the same request after Retry-After when it is present.",
        "headers": {
          "Retry-After": {
            "description": "5 with temporarily_unavailable; 1 when the shared per-instance request cap refused the request. Wait at least this many seconds before retrying.",
            "schema": {
              "type": "string",
              "enum": ["1", "5"]
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "business_settings_unavailable",
                    "business_unavailable",
                    "temporarily_unavailable"
                  ]
                }
              },
              "required": ["error"]
            }
          }
        }
      }
    }
  },
  "x-permission-groups": [
    {
      "name": "messaging:manage",
      "title": "Messaging & calls",
      "description": "Send text, media and voice messages, read message receipts and inbound media, and answer and end customer calls."
    },
    {
      "name": "customers:read",
      "title": "Customers",
      "description": "List, search and export your customers. Includes their phone numbers."
    },
    {
      "name": "billing:read",
      "title": "Billing",
      "description": "Read usage, pricing, billing, the ledger and invoices."
    },
    {
      "name": "profile:manage",
      "title": "Profile & appearance",
      "description": "Read and edit the business profile, chat settings and images."
    },
    {
      "name": "webhooks:manage",
      "title": "Webhook",
      "description": "Read, set, test and delete the webhook, and read its deliveries."
    },
    {
      "name": "account:view",
      "title": "Account info",
      "description": "Read the account, its numbers and its API keys."
    }
  ]
}
