{
    "openapi": "3.0.3",
    "info": {
        "title": "MyLiveChat /v1 API",
        "version": "1.0.0",
        "description": "Block 269 — curated OpenAPI 3.0 specification for the MyLiveChat /v1 surface. Covers the most-used endpoints (auth, tickets, customer, CSAT, portal, business calendar, migration, health) so partners can run openapi-generator (or stoplight, or swagger-codegen) to scaffold a typed SDK client. The full surface is wider than what's listed here — see https://www.mylivechat.com/v1/docs.aspx for the complete reference."
    },
    "servers": [
        { "url": "https://www.mylivechat.com/v1", "description": "Production" }
    ],
    "security": [
        { "bearerAuth": [] }
    ],
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "description": "Token issued at /dashboard/config_api_tokens.ascx. Format: mlc_xxxxx..."
            }
        },
        "schemas": {
            "OkEnvelope": {
                "type": "object",
                "required": ["ok"],
                "properties": {
                    "ok":   { "type": "boolean", "example": true },
                    "data": { "description": "Response payload — shape varies per endpoint." }
                }
            },
            "ErrorEnvelope": {
                "type": "object",
                "required": ["ok", "error"],
                "properties": {
                    "ok":    { "type": "boolean", "example": false },
                    "error": {
                        "type": "object",
                        "properties": {
                            "code":    { "type": "string", "example": "missing_field" },
                            "message": { "type": "string" }
                        }
                    }
                }
            },
            "Whoami": {
                "type": "object",
                "properties": {
                    "token_id":     { "type": "integer" },
                    "token_prefix": { "type": "string" },
                    "site_id":      { "type": "integer" },
                    "label":        { "type": "string" },
                    "scopes":       { "type": "array", "items": { "type": "string" } },
                    "request_count":{ "type": "integer" },
                    "created_utc":  { "type": "string", "format": "date-time" },
                    "last_used_utc":{ "type": "string", "format": "date-time", "nullable": true },
                    "capabilities": {
                        "type": "object",
                        "properties": {
                            "api_version":        { "type": "string", "example": "v1" },
                            "rate_limit_per_min": { "type": "integer", "example": 60 },
                            "supports_patch":     { "type": "boolean" },
                            "supports_bulk":      { "type": "boolean" },
                            "auth_methods":       { "type": "array", "items": { "type": "string" } },
                            "csv_exporters":      { "type": "array", "items": { "type": "string" } },
                            "deployed":           { "type": "object", "additionalProperties": { "type": "boolean" } }
                        }
                    }
                }
            },
            "Health": {
                "type": "object",
                "properties": {
                    "as_of_utc":                    { "type": "string", "format": "date-time" },
                    "open_tickets":                 { "type": "integer" },
                    "pending_tickets":              { "type": "integer" },
                    "unassigned_open_pending":      { "type": "integer" },
                    "snoozed":                      { "type": "integer" },
                    "stale_no_first_reply":         { "type": "integer" },
                    "stale_threshold":              { "type": "string" },
                    "webhook_queue_pending":        { "type": "integer" },
                    "webhook_failed_24h":           { "type": "integer" },
                    "last_inbound_message_utc":     { "type": "string", "format": "date-time", "nullable": true },
                    "is_now_within_business_hours": { "type": "boolean" },
                    "business_calendar_active":     { "type": "boolean" },
                    "portal_active_tokens":         { "type": "integer" },
                    "portal_total_uses":            { "type": "integer" },
                    "customer_identities":          { "type": "integer" },
                    "bound_channel_handles":        { "type": "integer", "description": "Block 274 — total rows in dbo.CustomerIdentityChannel for this tenant. Zero pre-deploy." },
                    "channel_msg_inbound_30d":      { "type": "integer", "description": "Block 274 — sum of inbound counts across all non-web channels for the trailing month + previous month buckets." },
                    "ai_calls_24h":                 { "type": "integer", "description": "Block 284 — AI calls logged in the trailing 24h. Zero pre-deploy or pre-gateway-wired." },
                    "ai_errors_24h":                { "type": "integer", "description": "Block 284 — AI calls in the trailing 24h with status != 'ok'. Alertable when climbing." },
                    "ai_budget_exceeded":           { "type": "boolean", "description": "Block 284 — true when any AI guardrail cap (budget / daily tokens / RPM) is currently exceeded for this tenant." }
                }
            },
            "Ticket": {
                "type": "object",
                "properties": {
                    "id":              { "type": "integer", "format": "int64" },
                    "ticket_number":   { "type": "integer" },
                    "subject":         { "type": "string" },
                    "status":          { "type": "string", "enum": ["open", "pending", "resolved", "closed"] },
                    "priority":        { "type": "string", "enum": ["low", "normal", "high", "urgent"] },
                    "channel":         { "type": "string" },
                    "requester_name":  { "type": "string", "nullable": true },
                    "requester_email": { "type": "string", "nullable": true },
                    "requester_phone": { "type": "string", "nullable": true },
                    "tags":            { "type": "string", "nullable": true, "description": "Comma-separated tag list." },
                    "first_response_utc": { "type": "string", "format": "date-time", "nullable": true },
                    "resolved_utc":       { "type": "string", "format": "date-time", "nullable": true },
                    "created_utc":        { "type": "string", "format": "date-time" },
                    "customer_identity_id": { "type": "integer", "format": "int64", "nullable": true,
                        "description": "Stable identity handle (Block 249). Null on pre-deploy / un-emailed tickets." }
                }
            },
            "TicketCreateRequest": {
                "type": "object",
                "required": ["subject"],
                "properties": {
                    "subject":         { "type": "string", "maxLength": 300 },
                    "channel":         { "type": "string" },
                    "priority":        { "type": "string", "enum": ["low", "normal", "high", "urgent"] },
                    "requester_name":  { "type": "string" },
                    "requester_email": { "type": "string", "format": "email" },
                    "requester_phone": { "type": "string" },
                    "initial_comment": { "type": "string" },
                    "required_skills": { "type": "array", "items": { "type": "string" } }
                }
            },
            "CustomerIdentity": {
                "type": "object",
                "properties": {
                    "id":             { "type": "integer", "format": "int64" },
                    "email":          { "type": "string", "nullable": true },
                    "phone":          { "type": "string", "nullable": true },
                    "display_name":   { "type": "string", "nullable": true },
                    "ticket_count":   { "type": "integer" },
                    "first_seen_utc": { "type": "string", "format": "date-time" },
                    "last_seen_utc":  { "type": "string", "format": "date-time" }
                }
            },
            "PortalTokenIssueResponse": {
                "type": "object",
                "properties": {
                    "id":              { "type": "integer", "format": "int64" },
                    "requester_email": { "type": "string" },
                    "token":           { "type": "string", "description": "Raw token. Treat as a secret. Returned only on issue." },
                    "token_url":       { "type": "string", "format": "uri" },
                    "expires_utc":     { "type": "string", "format": "date-time" },
                    "use_count":       { "type": "integer" }
                }
            },
            "MigrateStatus": {
                "type": "object",
                "properties": {
                    "tickets":  { "$ref": "#/components/schemas/MigrateAsset" },
                    "articles": { "$ref": "#/components/schemas/MigrateAsset" },
                    "macros":   { "$ref": "#/components/schemas/MigrateAsset" },
                    "portal_tokens": {
                        "type": "object",
                        "properties": {
                            "active_tokens":  { "type": "integer" },
                            "expired_tokens": { "type": "integer" },
                            "revoked_tokens": { "type": "integer" },
                            "total_uses":     { "type": "integer" }
                        }
                    },
                    "customer_identities": {
                        "type": "object",
                        "properties": {
                            "total_identified": { "type": "integer" },
                            "with_email":       { "type": "integer" },
                            "with_phone":       { "type": "integer" }
                        }
                    },
                    "channel_foundation": {
                        "type": "object",
                        "description": "Block 274 — per-channel adoption rollup. Empty channels[] + zero total when foundation not deployed.",
                        "properties": {
                            "total_bound_handles": { "type": "integer" },
                            "channels": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "channel":            { "type": "string", "enum": ["whatsapp","messenger","instagram","sms","email"] },
                                        "bound_handles":      { "type": "integer" },
                                        "msg_inbound_month":  { "type": "integer" },
                                        "msg_outbound_month": { "type": "integer" }
                                    }
                                }
                            }
                        }
                    },
                    "ai_usage": {
                        "type": "object",
                        "description": "Block 288 — Section 4 AI adoption rollup. Zero counters pre-deploy or pre-gateway-wire-up.",
                        "properties": {
                            "calls_month":      { "type": "integer" },
                            "tokens_month":     { "type": "integer", "format": "int64" },
                            "cost_month_usd":   { "type": "number" },
                            "errors_month":     { "type": "integer" },
                            "cache_hits_month": { "type": "integer" }
                        }
                    }
                }
            },
            "AiCapState": {
                "type": "object",
                "description": "Block 283 — per-cap usage state. Null limit = no cap configured (no enforcement). headroom + percent_used present only when limit is set.",
                "properties": {
                    "limit":        { "nullable": true, "description": "Configured cap (decimal for budget, integer for tokens/calls). Null = no cap." },
                    "current":      { "description": "Current usage in matching units." },
                    "exceeded":     { "type": "boolean" },
                    "headroom":     { "description": "limit - current, clamped to 0. Absent when limit is null." },
                    "percent_used": { "type": "number", "minimum": 0, "maximum": 100, "description": "Absent when limit is null." }
                }
            },
            "MigrateAsset": {
                "type": "object",
                "properties": {
                    "imported_rows": { "type": "integer" },
                    "skipped_rows":  { "type": "integer" },
                    "failed_rows":   { "type": "integer" },
                    "run_count":     { "type": "integer" },
                    "last_run_utc":  { "type": "string", "format": "date-time", "nullable": true }
                }
            }
        }
    },
    "paths": {
        "/api.ashx?resource=whoami": {
            "get": {
                "summary": "Token introspection",
                "description": "Validates the token and returns scopes + capabilities (csv_exporters, deployed schemas). Use as the first call in any setup wizard.",
                "responses": {
                    "200": {
                        "description": "Token info",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Whoami" } } }
                            ]
                        } } }
                    },
                    "401": { "description": "Invalid or revoked token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
                }
            }
        },
        "/api.ashx?resource=health": {
            "get": {
                "summary": "Operational + adoption snapshot",
                "description": "Cheap one-call rollup for external monitors. Includes tenant ticket counts, webhook health, business-hours awareness, and customer-facing portal adoption.",
                "responses": {
                    "200": {
                        "description": "Snapshot",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Health" } } }
                            ]
                        } } }
                    }
                }
            }
        },
        "/api.ashx?resource=tickets": {
            "get": {
                "summary": "List tickets",
                "parameters": [
                    { "name": "status",   "in": "query", "schema": { "type": "string", "enum": ["open", "pending", "resolved", "closed"] } },
                    { "name": "assignee", "in": "query", "schema": { "type": "string" } },
                    { "name": "priority", "in": "query", "schema": { "type": "string", "enum": ["urgent", "high", "normal", "low"] } },
                    { "name": "q",        "in": "query", "schema": { "type": "string" } },
                    { "name": "limit",    "in": "query", "schema": { "type": "integer", "default": 25, "maximum": 200 } },
                    { "name": "skip",     "in": "query", "schema": { "type": "integer", "default": 0 } },
                    { "name": "format",   "in": "query", "schema": { "type": "string", "enum": ["json", "csv"] } }
                ],
                "responses": {
                    "200": {
                        "description": "Filtered ticket list",
                        "content": {
                            "application/json": { "schema": {
                                "type": "object",
                                "properties": {
                                    "items":     { "type": "array", "items": { "$ref": "#/components/schemas/Ticket" } },
                                    "total_count": { "type": "integer" }
                                }
                            } },
                            "text/csv": { "schema": { "type": "string", "format": "binary" } }
                        }
                    }
                }
            },
            "post": {
                "summary": "Create a ticket",
                "requestBody": {
                    "required": true,
                    "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TicketCreateRequest" } } }
                },
                "responses": {
                    "200": { "description": "Ticket created" },
                    "400": { "description": "Invalid request body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
                }
            }
        },
        "/api.ashx?resource=ticket": {
            "get": {
                "summary": "Ticket detail",
                "parameters": [
                    { "name": "id", "in": "query", "required": true, "schema": { "type": "integer", "format": "int64" } },
                    { "name": "include", "in": "query", "schema": { "type": "string", "description": "Comma-separated extras: links, activity, comments." } }
                ],
                "responses": { "200": { "description": "Ticket" }, "404": { "description": "Not found" } }
            },
            "patch": {
                "summary": "Mutate ticket (status/priority/assignee)",
                "requestBody": {
                    "required": true,
                    "content": { "application/json": { "schema": {
                        "type": "object",
                        "properties": {
                            "id":                 { "type": "integer", "format": "int64" },
                            "status":             { "type": "string" },
                            "priority":           { "type": "string" },
                            "assignee_agent_id":  { "type": "string" }
                        }
                    } } }
                },
                "responses": { "200": { "description": "Mutation applied" } }
            }
        },
        "/api.ashx?resource=customer": {
            "get": {
                "summary": "Per-customer rollup",
                "parameters": [
                    { "name": "email", "in": "query", "schema": { "type": "string", "format": "email" } },
                    { "name": "phone", "in": "query", "schema": { "type": "string" } }
                ],
                "responses": { "200": { "description": "Customer rollup with identity row inline" } }
            }
        },
        "/api.ashx?resource=customer_identities": {
            "get": {
                "summary": "List customer identities",
                "parameters": [
                    { "name": "order_by", "in": "query", "schema": { "type": "string", "enum": ["tickets", "recent", "name"], "default": "tickets" } },
                    { "name": "limit",    "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } },
                    { "name": "skip",     "in": "query", "schema": { "type": "integer", "default": 0 } },
                    { "name": "format",   "in": "query", "schema": { "type": "string", "enum": ["json", "csv"] } }
                ],
                "responses": {
                    "200": {
                        "description": "Customer identity list",
                        "content": { "application/json": { "schema": {
                            "type": "object",
                            "properties": {
                                "items": { "type": "array", "items": { "$ref": "#/components/schemas/CustomerIdentity" } }
                            }
                        } } }
                    }
                }
            }
        },
        "/api.ashx?resource=csat": {
            "get": {
                "summary": "CSAT summary + recent responses",
                "parameters": [
                    { "name": "days",      "in": "query", "schema": { "type": "integer", "default": 30, "maximum": 365 } },
                    { "name": "limit",     "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } },
                    { "name": "score_min", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 5 } },
                    { "name": "score_max", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 5 } },
                    { "name": "include",   "in": "query", "schema": { "type": "string", "description": "Comma-separated extras: trend." } }
                ],
                "responses": { "200": { "description": "Summary + recent + optional trend" } }
            }
        },
        "/api.ashx?resource=business_calendar": {
            "get": {
                "summary": "Business calendar week + holidays",
                "responses": { "200": { "description": "Week + holidays + flag_on + is_now_within_hours" } }
            },
            "patch": {
                "summary": "Replace weekly hours",
                "requestBody": {
                    "required": true,
                    "content": { "application/json": { "schema": {
                        "type": "object",
                        "required": ["week"],
                        "properties": {
                            "week": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "day_of_week":  { "type": "integer", "minimum": 0, "maximum": 6 },
                                        "open_minute":  { "type": "integer", "minimum": 0, "maximum": 1440 },
                                        "close_minute": { "type": "integer", "minimum": 0, "maximum": 1440 }
                                    }
                                }
                            }
                        }
                    } } }
                },
                "responses": { "200": { "description": "Persisted state" } }
            }
        },
        "/api.ashx?resource=portal_token": {
            "post": {
                "summary": "Issue customer-facing portal token",
                "requestBody": {
                    "required": true,
                    "content": { "application/json": { "schema": {
                        "type": "object",
                        "required": ["requester_email"],
                        "properties": {
                            "requester_email": { "type": "string", "format": "email" },
                            "expiry_days":     { "type": "integer", "default": 30, "minimum": 1, "maximum": 365 },
                            "reuse_active":    { "type": "boolean", "default": true }
                        }
                    } } }
                },
                "responses": {
                    "200": {
                        "description": "Token minted (or reused)",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/PortalTokenIssueResponse" } } }
                            ]
                        } } }
                    }
                }
            },
            "delete": {
                "summary": "Revoke portal token",
                "parameters": [{ "name": "id", "in": "query", "required": true, "schema": { "type": "integer", "format": "int64" } }],
                "responses": { "200": { "description": "Revoked" }, "404": { "description": "Not found in this site" } }
            }
        },
        "/api.ashx?resource=migrate_status": {
            "get": {
                "summary": "Aggregate migration progress",
                "description": "Per-asset import counters + portal token + customer identity rollups. Pre-deploy tenants get zeros.",
                "responses": {
                    "200": {
                        "description": "Migration snapshot",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/MigrateStatus" } } }
                            ]
                        } } }
                    }
                }
            }
        },
        "/api.ashx?resource=bind_channel": {
            "post": {
                "summary": "Bind external channel handle to a customer identity",
                "description": "Idempotent. Resolves identity by id, email, or phone (in that order); mints a fresh identity row when at least email or phone is supplied. Backed by dbo.CustomerIdentityChannel (Section 2 channel foundation).",
                "requestBody": {
                    "required": true,
                    "content": { "application/json": { "schema": {
                        "type": "object",
                        "required": ["channel", "external_user_id"],
                        "properties": {
                            "identity_id":      { "type": "integer", "format": "int64", "description": "Explicit identity id; takes precedence over email/phone resolution." },
                            "email":            { "type": "string",  "format": "email" },
                            "phone":            { "type": "string" },
                            "channel":          { "type": "string",  "enum": ["whatsapp","messenger","instagram","sms","email"] },
                            "external_user_id": { "type": "string",  "maxLength": 200, "description": "WhatsApp BSUID / Messenger PSID / IG user id / E.164 / addr-spec." },
                            "display_handle":   { "type": "string",  "maxLength": 200, "description": "Human-readable form (WA profile name, Messenger first+last, IG @username)." },
                            "display_name":     { "type": "string",  "description": "Used only when minting a fresh identity row." }
                        }
                    } } }
                },
                "responses": {
                    "200": {
                        "description": "Bound (or refreshed)",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "type": "object", "properties": {
                                    "ok":               { "type": "boolean" },
                                    "identity_id":     { "type": "integer", "format": "int64" },
                                    "channel":          { "type": "string" },
                                    "external_user_id": { "type": "string" },
                                    "display_handle":   { "type": "string", "nullable": true }
                                } } } }
                            ]
                        } } }
                    },
                    "400": { "description": "Missing field, bad channel value, or no_identity (provide identity_id, email, or phone)." },
                    "500": { "description": "bind_failed — likely the channel foundation deploy SQL has not been applied for this tenant." }
                }
            },
            "delete": {
                "summary": "Unbind external channel handle",
                "description": "Idempotent — already-absent rows return ok=true. No webhook event fires (consumers detect via absence in /v1/customer's identity.channels[]).",
                "parameters": [
                    { "name": "channel",          "in": "query", "required": true, "schema": { "type": "string", "enum": ["whatsapp","messenger","instagram","sms","email"] } },
                    { "name": "external_user_id", "in": "query", "required": true, "schema": { "type": "string", "maxLength": 200 } }
                ],
                "responses": {
                    "200": {
                        "description": "Unbound (or already absent)",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "type": "object", "properties": {
                                    "ok":               { "type": "boolean" },
                                    "channel":          { "type": "string" },
                                    "external_user_id": { "type": "string" }
                                } } } }
                            ]
                        } } }
                    },
                    "400": { "description": "Missing or bad channel/external_user_id." },
                    "500": { "description": "unbind_failed — channel foundation schema not deployed." }
                }
            }
        },
        "/api.ashx?resource=ai_usage": {
            "get": {
                "summary": "Per-tenant AI usage rollup (Section 4 substrate)",
                "description": "Block 280 — call counts, token volume, cost (USD), cache hits, errors, mean latency. summary covers today/yesterday/month-to-date; by_feature[] is trailing-N-days breakdown. Pre-deploy tenants get zeroed shape, never an error. Aliases: ai_spend.",
                "parameters": [
                    { "name": "days",    "in": "query", "required": false, "schema": { "type": "integer", "default": 7, "minimum": 1, "maximum": 90 } },
                    { "name": "include", "in": "query", "required": false, "schema": { "type": "string", "enum": ["trend", "series"] }, "description": "Block 282 — opt-in per-day series for sparklines / line charts." }
                ],
                "responses": {
                    "200": {
                        "description": "Rollup",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "type": "object", "properties": {
                                    "summary": {
                                        "type": "object",
                                        "properties": {
                                            "calls_today":       { "type": "integer" },
                                            "calls_yesterday":   { "type": "integer" },
                                            "calls_month":       { "type": "integer" },
                                            "tokens_today":      { "type": "integer", "format": "int64" },
                                            "tokens_month":      { "type": "integer", "format": "int64" },
                                            "cost_month_usd":    { "type": "number" },
                                            "cache_hits_month":  { "type": "integer" },
                                            "errors_month":      { "type": "integer" }
                                        }
                                    },
                                    "by_feature": {
                                        "type": "array",
                                        "items": {
                                            "type": "object",
                                            "properties": {
                                                "feature":         { "type": "string", "enum": ["bot","copilot","summary","intent","tone","kb_search"] },
                                                "calls":           { "type": "integer" },
                                                "tokens":          { "type": "integer", "format": "int64" },
                                                "cache_hits":      { "type": "integer" },
                                                "errors":          { "type": "integer" },
                                                "cost_usd":        { "type": "number" },
                                                "mean_latency_ms": { "type": "number" },
                                                "cache_hit_rate":  { "type": "number", "minimum": 0, "maximum": 1 },
                                                "error_rate":      { "type": "number", "minimum": 0, "maximum": 1 }
                                            }
                                        }
                                    },
                                    "trend": {
                                        "type": "array",
                                        "description": "Present only when ?include=trend. One entry per UTC day in the window, including zero-call days (pre-filled).",
                                        "items": {
                                            "type": "object",
                                            "properties": {
                                                "date":     { "type": "string", "format": "date" },
                                                "calls":    { "type": "integer" },
                                                "tokens":   { "type": "integer", "format": "int64" },
                                                "cost_usd": { "type": "number" }
                                            }
                                        }
                                    },
                                    "applied_filters": {
                                        "type": "object",
                                        "properties": {
                                            "days":    { "type": "integer" },
                                            "include": { "type": "string" }
                                        }
                                    }
                                } } } }
                            ]
                        } } }
                    }
                }
            }
        },
        "/api.ashx?resource=ai_insights": {
            "get": {
                "summary": "AI Insights aggregates (top questions / handoff reasons / status)",
                "description": "Block 323 — three actionable dimensions over the existing AI tables: top customer questions clustered on first 80 chars of user messages, top handoff reasons, and handoff status breakdown. Drives the dashboard ai_insights.ascx and the SDK partners' own dashboards. Aliases: insights. CSV export via ?format=csv.",
                "parameters": [
                    { "name": "days",   "in": "query", "required": false, "schema": { "type": "integer", "default": 30, "minimum": 1, "maximum": 90 } },
                    { "name": "take",   "in": "query", "required": false, "schema": { "type": "integer", "default": 10, "minimum": 1, "maximum": 50 } },
                    { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": ["json", "csv"] } }
                ],
                "responses": {
                    "200": {
                        "description": "Insights",
                        "content": { "application/json": { "schema": {
                            "type": "object",
                            "properties": {
                                "ok":                  { "type": "boolean" },
                                "days":                { "type": "integer" },
                                "top_questions":       { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string" }, "count": { "type": "integer" } } } },
                                "top_handoff_reasons": { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string" }, "count": { "type": "integer" } } } },
                                "handoff_status":      { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string" }, "count": { "type": "integer" } } } }
                            }
                        } } }
                    }
                }
            }
        },
        "/api.ashx?resource=ai_copilot": {
            "post": {
                "summary": "Agent Copilot - draft reply or summary",
                "description": "Block 322 - single-shot Anthropic call producing a draft reply (mode=reply) or a 3-6 bullet summary (mode=summary). Caller passes anthropic_api_key in the body since token-auth context has no session-bound config access. Logs to AiUsageRepository under feature=copilot or feature=copilot_summary so cost telemetry stays accurate. Aliases: ai_suggest_reply, copilot.",
                "requestBody": {
                    "required": true,
                    "content": { "application/json": { "schema": {
                        "type": "object",
                        "required": ["transcript", "anthropic_api_key"],
                        "properties": {
                            "mode":              { "type": "string", "enum": ["reply", "summary"], "default": "reply" },
                            "transcript":        { "type": "string", "maxLength": 8000 },
                            "tone_hint":         { "type": "string", "maxLength": 500 },
                            "anthropic_api_key": { "type": "string" },
                            "model":             { "type": "string", "default": "claude-sonnet-4-5" }
                        }
                    } } }
                },
                "responses": {
                    "200": { "description": "ok" },
                    "400": { "description": "missing transcript or anthropic_api_key" },
                    "502": { "description": "copilot_failed - see ErrorMessage" }
                }
            }
        },
        "/api.ashx?resource=self_test": {
            "get": {
                "summary": "Composite substrate self-test runner",
                "description": "Block 295 — runs ai_usage_self_test + channel_self_test sequentially. Aliases: self_test_all. Returns per-test ok/error/round_trip_ms plus a rolled-up overall ok (AND of all sub-tests). Partner CI's one-curl 'is everything deployed and healthy' probe; <30ms typical total round-trip.",
                "responses": {
                    "200": {
                        "description": "Aggregate test report",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "type": "object", "properties": {
                                    "ok":               { "type": "boolean" },
                                    "total_runtime_ms": { "type": "integer" },
                                    "tests": {
                                        "type": "array",
                                        "items": {
                                            "type": "object",
                                            "properties": {
                                                "name":          { "type": "string", "enum": ["ai_usage", "channel_foundation"] },
                                                "ok":            { "type": "boolean" },
                                                "round_trip_ms": { "type": "integer" },
                                                "error":         { "type": "string", "nullable": true }
                                            }
                                        }
                                    }
                                } } } }
                            ]
                        } } }
                    }
                }
            }
        },
        "/api.ashx?resource=ai_usage_self_test": {
            "get": {
                "summary": "AI usage substrate self-test (write+read round-trip)",
                "description": "Block 293 — writes a synthetic _self_test row via AiUsageRepository.LogCall and verifies the rollup counter incremented. Aliases: ai_self_test. Idempotent. Reserved feature name keeps test data segregated from real metrics.",
                "responses": { "200": { "description": "Test report" } }
            }
        },
        "/api.ashx?resource=channel_self_test": {
            "get": {
                "summary": "Channel foundation substrate self-test (read-only)",
                "description": "Block 294 — verifies 5 schema components + exercises GetByExternalUser via UUID-suffixed sentinel. Aliases: channel_foundation_self_test. No state mutation.",
                "responses": { "200": { "description": "Test report" } }
            }
        },
        "/api.ashx?resource=ai_limits": {
            "get": {
                "summary": "Per-tenant AI guardrail state",
                "description": "Block 283 — three independent caps (monthly USD, daily tokens, RPM). Null limit = no cap configured. Aliases: ai_guardrails. Future gateway short-circuits when any_exceeded && action=hard.",
                "responses": {
                    "200": {
                        "description": "Cap state",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "type": "object", "properties": {
                                    "budget":       { "$ref": "#/components/schemas/AiCapState" },
                                    "daily_tokens": { "$ref": "#/components/schemas/AiCapState" },
                                    "minute_calls": { "$ref": "#/components/schemas/AiCapState" },
                                    "any_exceeded": { "type": "boolean" },
                                    "action":       { "type": "string", "enum": ["soft", "hard", "none"] }
                                } } } }
                            ]
                        } } }
                    }
                }
            },
            "patch": {
                "summary": "Set / clear AI guardrail caps",
                "description": "Block 287 — each field optional; omit to leave unchanged, pass null to clear. Auto-upserts the AiSiteBilling row for tenants without one. Returns the post-patch state via the same shape as GET.",
                "requestBody": {
                    "required": true,
                    "content": { "application/json": { "schema": {
                        "type": "object",
                        "minProperties": 1,
                        "properties": {
                            "monthly_budget_usd": { "type": "number",  "minimum": 0, "nullable": true },
                            "daily_token_limit":  { "type": "integer", "minimum": 0, "nullable": true },
                            "minute_call_limit":  { "type": "integer", "minimum": 0, "nullable": true },
                            "action":             { "type": "string",  "enum": ["soft", "hard"] }
                        }
                    } } }
                },
                "responses": {
                    "200": { "description": "Updated; returns post-patch state (same shape as GET)." },
                    "400": { "description": "empty_patch / bad_field / bad_request." },
                    "500": { "description": "patch_failed — likely the ai_usage_deploy.sql columns are missing." }
                }
            }
        },
        "/api.ashx?resource=bind_channels_bulk": {
            "post": {
                "summary": "Bulk-bind channel handles from a CSV body",
                "description": "Block 278 — same shape as tickets_import. Per-row idempotent. Optional ?dry_run=1 for preview. Header columns: channel, external_user_id (required); email, display_handle, phone (optional). No webhook events fire (would melt queue at bulk scale).",
                "parameters": [
                    { "name": "dry_run", "in": "query", "required": false, "schema": { "type": "string", "enum": ["1"] } }
                ],
                "requestBody": {
                    "required": true,
                    "content": { "text/csv": { "schema": { "type": "string", "example": "channel,external_user_id,email,display_handle\nwhatsapp,wa+15551234567,alice@example.com,Alice\nmessenger,PSID-9876543210,alice@example.com,\nsms,+15551234567,,\n" } } }
                },
                "responses": {
                    "200": {
                        "description": "Per-row summary",
                        "content": { "application/json": { "schema": {
                            "allOf": [
                                { "$ref": "#/components/schemas/OkEnvelope" },
                                { "type": "object", "properties": { "data": { "type": "object", "properties": {
                                    "total_rows":      { "type": "integer" },
                                    "bound":           { "type": "integer" },
                                    "refreshed":       { "type": "integer" },
                                    "minted_identity": { "type": "integer" },
                                    "bad_channel":     { "type": "integer" },
                                    "no_identity":     { "type": "integer" },
                                    "failed":          { "type": "integer" },
                                    "error_sample":    { "type": "array", "items": { "type": "string" } },
                                    "dry_run":         { "type": "boolean" }
                                } } } }
                            ]
                        } } }
                    },
                    "400": { "description": "Bad CSV header, empty body, or no rows." },
                    "413": { "description": "CSV body exceeds 16 MB." }
                }
            }
        }
    }
}
