{
  "openapi": "3.1.0",
  "info": {
    "title": "ReachClaw API",
    "version": "1.0.0",
    "description": "AI Sales SaaS API for managing email automation, leads, campaigns, and knowledge bases.",
    "contact": {
      "email": "yaniv@hacx.org"
    }
  },
  "servers": [
    {
      "url": "https://reachclaw.com",
      "description": "Production"
    }
  ],
  "components": {
    "securitySchemes": {
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "better-auth.session_token",
        "description": "Better Auth session cookie"
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Bearer token for cron endpoints (CRON_SECRET)"
      },
      "liveChatConversation": {
        "type": "http",
        "scheme": "bearer",
        "description": "Conversation token returned by POST /api/widget/{widgetId}/conversations. Signed by ReachClaw; valid 7 days."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Lead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "company": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "contacted",
              "qualified",
              "disqualified",
              "replied",
              "converted",
              "unsubscribed"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "sourceUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastContactedAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix timestamp"
          },
          "lastActivityAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix timestamp of last activity (e.g. reply to outbound email)"
          },
          "viewedAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix timestamp when user last viewed this lead"
          },
          "score": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "AI-generated lead fit score (0-100)"
          },
          "unsubscribedAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix timestamp when lead unsubscribed"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "scheduled",
              "generating",
              "ready",
              "sending",
              "sent",
              "completed",
              "error"
            ]
          },
          "automationMode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "one_time",
              "scheduled",
              "ongoing",
              null
            ]
          },
          "totalLeads": {
            "type": "integer"
          },
          "draftsCompleted": {
            "type": "integer"
          },
          "draftsApproved": {
            "type": "integer",
            "description": "Emails approved and waiting to send. Counted live from the campaign's emails on GET /api/outreach and GET /api/outreach/{id}; drops as they are sent."
          },
          "emailsSent": {
            "type": "integer"
          },
          "templateHints": {
            "type": [
              "string",
              "null"
            ]
          },
          "scheduledAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601 datetime when campaign should execute"
          },
          "timezone": {
            "type": [
              "string",
              "null"
            ],
            "description": "IANA timezone string (e.g., America/New_York)"
          },
          "pausedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When campaign was paused (null if not paused)"
          },
          "ongoingEnabled": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "ongoingIntervalDays": {
            "type": [
              "integer",
              "null"
            ],
            "deprecated": true,
            "description": "Legacy days-only interval. Deprecated in favor of ongoingIntervalValue + ongoingIntervalUnit."
          },
          "ongoingIntervalValue": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Recurring interval numeric value."
          },
          "ongoingIntervalUnit": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "minutes",
              "hours",
              "days",
              null
            ],
            "description": "Recurring interval unit for ongoing campaigns."
          },
          "ongoingDiscoveryEnabled": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "ongoingStopAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ongoingLastRunAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ongoingNextRunAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ongoingConsecutiveFailures": {
            "type": [
              "integer",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "templateIds": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Selected template IDs (1-5) used for draft generation"
          }
        }
      },
      "InboundEmail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "fromEmail": {
            "type": "string"
          },
          "fromName": {
            "type": [
              "string",
              "null"
            ]
          },
          "subject": {
            "type": [
              "string",
              "null"
            ]
          },
          "bodyText": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "drafting",
              "drafted",
              "pending",
              "approved",
              "sent",
              "rejected",
              "skipped",
              "failed",
              "blocked_limit"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ]
          },
          "draftText": {
            "type": [
              "string",
              "null"
            ]
          },
          "draftHtml": {
            "type": [
              "string",
              "null"
            ]
          },
          "replyMode": {
            "type": [
              "string",
              "null"
            ]
          },
          "processingAttempts": {
            "type": "integer",
            "default": 0
          },
          "failureReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "viewedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the email was first viewed by the user"
          }
        }
      },
      "Template": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "subject": {
            "type": [
              "string",
              "null"
            ]
          },
          "body": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "personalizeWithAi": {
            "type": "boolean",
            "description": "When true, this template can be AI-personalized per lead; when false, it's used verbatim with variable replacement only."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TemplateAnalytics": {
        "type": "object",
        "properties": {
          "templateId": {
            "type": "string"
          },
          "templateName": {
            "type": "string"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "angle": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalSent": {
            "type": "integer",
            "description": "Total emails sent using this template"
          },
          "totalOpened": {
            "type": "integer",
            "description": "Total emails opened"
          },
          "totalReplied": {
            "type": "integer",
            "description": "Total emails that received replies"
          },
          "openRate": {
            "type": "number",
            "description": "Percentage of sent emails that were opened (0-100)"
          },
          "replyRate": {
            "type": "number",
            "description": "Percentage of sent emails that received replies (0-100)"
          },
          "avgTimeToOpenHours": {
            "type": [
              "number",
              "null"
            ],
            "description": "Average hours from send to open"
          },
          "avgTimeToReplyHours": {
            "type": [
              "number",
              "null"
            ],
            "description": "Average hours from send to reply"
          }
        }
      },
      "KnowledgeItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "contentMd": {
            "type": [
              "string",
              "null"
            ],
            "description": "Markdown content (null for PDF/TXT documents)"
          },
          "contentType": {
            "type": "string",
            "description": "Content type: manual, pricing, feature, pdf, txt, csv, etc."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source URL or R2 download URL"
          },
          "anthropicFileId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Anthropic Files API file_id for AI document access"
          },
          "sizeBytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "File size in bytes (for uploaded documents)"
          },
          "crawledAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PlanRequiredError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "A paid plan is required to use this feature. Upgrade in Settings → Billing."
          },
          "code": {
            "type": "string",
            "enum": [
              "PLAN_REQUIRED"
            ],
            "example": "PLAN_REQUIRED"
          }
        },
        "required": [
          "error",
          "code"
        ]
      },
      "ApiKeyRequiredError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "An Anthropic API key is required. Add your key in Settings → API Key."
          },
          "code": {
            "type": "string",
            "enum": [
              "API_KEY_REQUIRED"
            ],
            "example": "API_KEY_REQUIRED"
          }
        },
        "required": [
          "error",
          "code"
        ]
      },
      "AdminBillingCustomer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "companyName": {
            "type": "string",
            "nullable": true
          },
          "plan": {
            "type": "string",
            "enum": [
              "none",
              "solo",
              "growth",
              "scale"
            ]
          },
          "onboardingStatus": {
            "type": "string"
          },
          "stripeCustomerId": {
            "type": "string",
            "nullable": true
          },
          "stripeSubscriptionId": {
            "type": "string",
            "nullable": true
          },
          "subscriptionStatus": {
            "type": "string",
            "enum": [
              "none",
              "active",
              "canceled",
              "past_due"
            ]
          },
          "currentPeriodEnd": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "cancelAtPeriodEnd": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "BillingAnalytics": {
        "type": "object",
        "properties": {
          "mrr": {
            "type": "number",
            "description": "Monthly Recurring Revenue in USD"
          },
          "arr": {
            "type": "number",
            "description": "Annual Recurring Revenue in USD"
          },
          "totalActiveSubscribers": {
            "type": "integer"
          },
          "planCounts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "statusCounts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "newThisMonth": {
            "type": "integer"
          },
          "churnedThisMonth": {
            "type": "integer"
          },
          "pastDueCount": {
            "type": "integer"
          },
          "planBreakdown": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "plan": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "mrr": {
                  "type": "number"
                }
              }
            }
          },
          "totalCustomers": {
            "type": "integer"
          }
        }
      }
    },
    "parameters": {
      "limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 100,
          "maximum": 500
        },
        "description": "Max results to return"
      },
      "offset": {
        "name": "offset",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 0
        },
        "description": "Number of results to skip"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Authentication required or session is invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Unauthorized"
            }
          }
        }
      },
      "Error": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Internal server error"
            }
          }
        }
      },
      "RateLimitExceeded": {
        "description": "Too many requests. Rate limit exceeded.",
        "headers": {
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            },
            "description": "Maximum number of requests allowed in the time window"
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            },
            "description": "Number of requests remaining in the current window"
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "integer"
            },
            "description": "Unix timestamp (seconds) when the rate limit window resets"
          },
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Number of seconds to wait before retrying"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Too many requests. Please try again later."
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "cookieAuth": []
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Health check",
        "security": [],
        "responses": {
          "200": {
            "description": "Service healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "healthy"
                      ]
                    },
                    "timestamp": {
                      "type": "string"
                    },
                    "checks": {
                      "type": "object",
                      "properties": {
                        "database": {
                          "type": "string",
                          "enum": [
                            "ok",
                            "error"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Service unhealthy"
          }
        }
      }
    },
    "/api/jobs": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "List queue jobs for current customer",
        "parameters": [
          {
            "name": "statuses",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated statuses"
          },
          {
            "name": "types",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "knowledge.crawl,knowledge.crawl.single,inbox.sync"
            },
            "description": "Comma-separated job types. Keep this list in sync with src/lib/job-types.ts and this OpenAPI spec whenever new job types are added."
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Job list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Queue list endpoint for customer-scoped jobs. Use limit+offset pagination for large datasets."
      },
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Enqueue a customer job",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jobType": {
                    "type": "string",
                    "enum": [
                      "knowledge.crawl",
                      "knowledge.crawl.single",
                      "inbox.sync",
                      "leads.discover",
                      "outreach.generate",
                      "outreach.send"
                    ],
                    "description": "Queue job type. When adding a new job type, update src/lib/job-types.ts and this enum in OpenAPI."
                  },
                  "payload": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "low",
                      "normal",
                      "high",
                      "critical"
                    ]
                  },
                  "tag": {
                    "type": "string"
                  },
                  "dedupeKey": {
                    "type": "string"
                  },
                  "maxRetries": {
                    "type": "integer"
                  }
                },
                "required": [
                  "jobType"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Job enqueued"
          }
        }
      },
      "delete": {
        "tags": [
          "Jobs"
        ],
        "summary": "Cancel jobs by tag",
        "parameters": [
          {
            "name": "tag",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tagged jobs cancelled"
          }
        }
      }
    },
    "/api/jobs/{id}": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "Get a queue job",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job detail"
          },
          "404": {
            "description": "Job not found"
          }
        }
      },
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Retry a queue job",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "retry"
                    ]
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job retried"
          },
          "404": {
            "description": "Job not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Jobs"
        ],
        "summary": "Cancel a queue job",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job cancelled"
          },
          "404": {
            "description": "Job not found"
          }
        }
      }
    },
    "/api/unsubscribe/{token}": {
      "get": {
        "tags": [
          "Public"
        ],
        "summary": "Unsubscribe from email list",
        "description": "Public endpoint to unsubscribe a lead from the mailing list. Returns an HTML page.",
        "security": [],
        "parameters": [
          {
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Unsubscribe token (base64url encoded with HMAC signature)"
          }
        ],
        "responses": {
          "200": {
            "description": "Successfully unsubscribed (HTML page)",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid token (HTML page)",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Server error (HTML page)",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Too many unsubscribe requests from this IP (10 per minute). Returns a plain HTML page asking the visitor to try again.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Admin dashboard data",
        "description": "Returns system stats, user list, plan distribution, email pipeline health, lead discovery and website chat counts. Requires admin email match. Website chat numbers are counts only; no chat content or visitor details. Pipeline health, lead discovery and website chat counts are each cached for 60 seconds.",
        "responses": {
          "200": {
            "description": "Admin data with pipeline health",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "customers": {
                          "type": "integer"
                        }
                      }
                    },
                    "plans": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "onboardingStatuses": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "customers": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "The 10 most recent signups (newest first). users.customers has the full count; use GET /api/admin/billing for the full paginated, searchable list.",
                      "maxItems": 10
                    },
                    "pipelineHealth": {
                      "type": "object",
                      "description": "Email pipeline health metrics (cached 60s)",
                      "properties": {
                        "emails24h": {
                          "type": "integer",
                          "description": "Emails processed in last 24 hours"
                        },
                        "emails7d": {
                          "type": "integer",
                          "description": "Emails processed in last 7 days"
                        },
                        "autoSent24h": {
                          "type": "integer",
                          "description": "Auto-sent replies in last 24 hours"
                        },
                        "failed24h": {
                          "type": "integer",
                          "description": "Failed emails in last 24 hours"
                        },
                        "skipped24h": {
                          "type": "integer",
                          "description": "Skipped (non-actionable) emails in last 24 hours"
                        },
                        "pendingDrafts": {
                          "type": "integer",
                          "description": "Drafts awaiting human review"
                        },
                        "avgAiProcessingMs": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "Average AI processing time in ms"
                        },
                        "customersWithActiveWatch": {
                          "type": "integer",
                          "description": "Customers with active Gmail Push watch"
                        },
                        "totalActiveConnections": {
                          "type": "integer",
                          "description": "Total active email connections"
                        },
                        "connectionsNeedingReconnect": {
                          "type": "integer",
                          "description": "Customers whose Gmail access Google stopped accepting (connection status reauth_required and no active connection). Counts only."
                        }
                      }
                    },
                    "system": {
                      "type": "object",
                      "properties": {
                        "uptimeSeconds": {
                          "type": "integer"
                        },
                        "memoryMB": {
                          "type": "integer"
                        },
                        "memoryTotalMB": {
                          "type": "integer"
                        },
                        "memoryFreeMB": {
                          "type": "integer"
                        },
                        "dbSizeMB": {
                          "type": "number"
                        },
                        "nodeVersion": {
                          "type": "string"
                        },
                        "platform": {
                          "type": "string"
                        },
                        "cpus": {
                          "type": "integer"
                        }
                      }
                    },
                    "websiteChat": {
                      "type": "object",
                      "description": "Website chat counts across all customers (cached 60s). Only real visitor chats (source 'widget'); test-page and preview chats are left out. Never includes messages, visitor emails or anything visitors typed.",
                      "properties": {
                        "widgetsOn": {
                          "type": "integer",
                          "description": "Chats switched on"
                        },
                        "widgetsInstalled": {
                          "type": "integer",
                          "description": "Switched-on chats that have loaded on an allowed website at least once"
                        },
                        "lookUpsOn": {
                          "type": "integer",
                          "description": "Switched-on chats with MCP look-ups enabled (Scale)"
                        },
                        "chats24h": {
                          "type": "integer",
                          "description": "Visitor chats started in the last 24 hours"
                        },
                        "chats7d": {
                          "type": "integer",
                          "description": "Visitor chats started in the last 7 days"
                        },
                        "answers24h": {
                          "type": "integer",
                          "description": "Completed AI answers in the last 24 hours"
                        },
                        "answers7d": {
                          "type": "integer",
                          "description": "Completed AI answers in the last 7 days"
                        },
                        "failedAnswers24h": {
                          "type": "integer",
                          "description": "AI answers that failed in the last 24 hours"
                        },
                        "lookUps7d": {
                          "type": "integer",
                          "description": "AI answers in the last 7 days that used an MCP tool"
                        },
                        "handedOver7d": {
                          "type": "integer",
                          "description": "Chats started in the last 7 days that were handed to a person"
                        },
                        "waitingForPerson": {
                          "type": "integer",
                          "description": "Chats currently waiting for the owner (status escalated)"
                        },
                        "topCustomers": {
                          "type": "array",
                          "maxItems": 5,
                          "description": "Up to 5 customers with the most visitor chats in the last 7 days",
                          "items": {
                            "type": "object",
                            "properties": {
                              "customerId": {
                                "type": "string"
                              },
                              "customerName": {
                                "type": "string"
                              },
                              "chats7d": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "voiceOn": {
                          "type": "integer",
                          "description": "Turned-on chats where visitors may talk instead of typing (voice)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not an admin"
          }
        }
      }
    },
    "/api/analytics": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Get analytics data",
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "30",
              "enum": [
                "7",
                "30",
                "90",
                "365"
              ]
            },
            "description": "Number of days for historical data"
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics data including stats, timelines, breakdowns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/analytics/export": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Export analytics data as CSV",
        "description": "Exports email analytics data within a date range as a CSV file. Includes date, sender, subject, category, confidence, status, and mode. Maximum 10,000 rows per export.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Start date (ISO 8601 format)"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "End date (ISO 8601 format)"
          }
        ],
        "responses": {
          "200": {
            "description": "CSV file with analytics data",
            "headers": {
              "Content-Type": {
                "schema": {
                  "type": "string",
                  "example": "text/csv; charset=utf-8"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string",
                  "example": "attachment; filename=\"analytics-2026-01-01-to-2026-01-31.csv\""
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV formatted data with headers: Date, From, Subject, Category, Confidence, Status, Mode. Includes CSV injection protection and truncation warning if data exceeds 10,000 rows."
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid date parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Growth plan or higher required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 5 requests per minute per customer"
          }
        }
      }
    },
    "/api/dashboard": {
      "get": {
        "tags": [
          "Dashboard"
        ],
        "summary": "Get dashboard overview data",
        "responses": {
          "200": {
            "description": "Dashboard summary with stats, charts, recent activity. liveChat is null unless the customer set up the website chat; then it has enabled, needsPerson (chats waiting for the owner) and last7Days {conversations, resolvedPct, handedOver, openUnanswered (distinct questions)}. stats.outboundSent is the number of outreach emails sent (sentAt set) and stats.openRate the percentage of those with a recorded open (likely-bot opens excluded). recentActivity outbound items carry the campaign name as subtext.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/leads": {
      "get": {
        "tags": [
          "Leads"
        ],
        "summary": "List leads",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search name, email, company, role"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by status"
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by source (manual, csv, ai_search)"
          },
          {
            "name": "scoreRange",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "70-100",
                "30-69",
                "0-29"
              ]
            },
            "description": "Filter by score range"
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "name",
                "company",
                "score",
                "newest",
                "oldest",
                "score_desc",
                "score_asc"
              ]
            },
            "description": "Sort field. Supports legacy values newest/oldest/score_desc/score_asc."
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            },
            "description": "Sort direction when sort is a field name."
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "name",
                "company",
                "score"
              ]
            },
            "description": "Preferred sort field alias."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            },
            "description": "Preferred sort direction alias."
          },
          {
            "name": "excludeUnsubscribed",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "When true, excludes leads marked unsubscribed."
          }
        ],
        "responses": {
          "200": {
            "description": "Lead list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Lead"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Leads"
        ],
        "summary": "Add lead or import CSV",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "name": {
                        "type": "string"
                      },
                      "company": {
                        "type": "string"
                      },
                      "role": {
                        "type": "string"
                      },
                      "source": {
                        "type": "string"
                      },
                      "notes": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "email"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "csv": {
                        "type": "string",
                        "description": "CSV data with header row"
                      }
                    },
                    "required": [
                      "csv"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lead created or CSV imported"
          },
          "409": {
            "$ref": "#/components/responses/Error",
            "description": "Lead with this email already exists"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 30 requests per minute by IP address"
          }
        }
      },
      "put": {
        "tags": [
          "Leads"
        ],
        "summary": "Update a lead",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "company": {
                    "type": "string"
                  },
                  "role": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "new",
                      "contacted",
                      "qualified",
                      "disqualified",
                      "replied",
                      "converted",
                      "unsubscribed"
                    ]
                  },
                  "notes": {
                    "type": "string"
                  }
                },
                "required": [
                  "id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lead updated"
          },
          "409": {
            "$ref": "#/components/responses/Error",
            "description": "Lead with this email already exists"
          }
        }
      },
      "delete": {
        "tags": [
          "Leads"
        ],
        "summary": "Delete leads",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1
                  }
                },
                "required": [
                  "ids"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Leads deleted"
          }
        }
      }
    },
    "/api/leads/search": {
      "get": {
        "tags": [
          "Leads"
        ],
        "summary": "Get AI lead discovery status and auto-run settings",
        "responses": {
          "200": {
            "description": "Current lead discovery state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string",
                      "nullable": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "none",
                        "pending",
                        "running",
                        "paused",
                        "completed",
                        "error"
                      ]
                    },
                    "leadsFound": {
                      "type": "integer"
                    },
                    "queriesCompleted": {
                      "type": "integer"
                    },
                    "totalQueries": {
                      "type": "integer"
                    },
                    "autoEnabled": {
                      "type": "boolean"
                    },
                    "autoIntervalMinutes": {
                      "type": "integer"
                    },
                    "nextRunAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "discoveryEnabled": {
                      "type": "boolean"
                    },
                    "activeOwners": {
                      "type": "integer"
                    },
                    "campaignOwnerCount": {
                      "type": "integer"
                    },
                    "campaignOwners": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "campaignId": {
                            "type": "string"
                          },
                          "campaignName": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "pausedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "intervalMinutes": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Leads"
        ],
        "summary": "Enable AI lead discovery and enqueue a discovery run",
        "description": "Turns on periodic AI lead discovery for the customer and immediately starts (or reuses) a queue-backed leads.discover job.",
        "responses": {
          "200": {
            "description": "Lead discovery enabled and started (or already running)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string",
                      "nullable": true
                    },
                    "queueJobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "started",
                        "already_running",
                        "disabled"
                      ]
                    },
                    "totalQueries": {
                      "type": "integer"
                    },
                    "autoEnabled": {
                      "type": "boolean"
                    },
                    "autoIntervalMinutes": {
                      "type": "integer"
                    },
                    "nextRunAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "discoveryEnabled": {
                      "type": "boolean"
                    },
                    "activeOwners": {
                      "type": "integer"
                    },
                    "campaignOwnerCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "424": {
            "description": "Anthropic API key required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyRequiredError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 2 requests per minute per customer"
          }
        }
      },
      "delete": {
        "tags": [
          "Leads"
        ],
        "summary": "Disable leads-page AI discovery and optionally disable campaign discovery owners",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "disableCampaignIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional campaign IDs to disable campaign-owned discovery at the same time."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Leads toggle disabled; campaign-owned discovery may remain active",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "paused",
                        "running"
                      ]
                    },
                    "autoEnabled": {
                      "type": "boolean"
                    },
                    "autoIntervalMinutes": {
                      "type": "integer"
                    },
                    "nextRunAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "discoveryEnabled": {
                      "type": "boolean"
                    },
                    "activeOwners": {
                      "type": "integer"
                    },
                    "campaignOwnerCount": {
                      "type": "integer"
                    },
                    "cancelled": {
                      "type": "integer"
                    },
                    "disabledCampaignCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/leads/search/filter": {
      "get": {
        "tags": [
          "Leads"
        ],
        "summary": "Get global lead discovery filter",
        "responses": {
          "200": {
            "description": "Current global lead discovery filter",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "filter": {
                      "type": "object",
                      "properties": {
                        "instructions": {
                          "type": "string"
                        },
                        "blockedDomainSuffixes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "blockedCompanyKeywords": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "blockedIndustries": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "version": {
                          "type": "integer",
                          "enum": [
                            1
                          ]
                        }
                      }
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "instructionsCount": {
                          "type": "integer"
                        },
                        "blockedDomainSuffixes": {
                          "type": "integer"
                        },
                        "blockedCompanyKeywords": {
                          "type": "integer"
                        },
                        "blockedIndustries": {
                          "type": "integer"
                        },
                        "totalRules": {
                          "type": "integer"
                        },
                        "hasFilters": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Leads"
        ],
        "summary": "Update global lead discovery filter",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "instructions": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "blockedDomainSuffixes": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "string"
                    }
                  },
                  "blockedCompanyKeywords": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "string"
                    }
                  },
                  "blockedIndustries": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Filter saved",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Maximum number of requests allowed in the time window"
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Number of requests remaining in the current window"
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix timestamp (seconds) when the rate limit window resets"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "filter": {
                      "type": "object"
                    },
                    "summary": {
                      "type": "object"
                    },
                    "restarted": {
                      "type": "boolean"
                    },
                    "restartStatus": {
                      "type": "string",
                      "enum": [
                        "none",
                        "started",
                        "already_running",
                        "disabled"
                      ]
                    },
                    "restartedJobId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 3 requests per minute per customer"
          }
        }
      }
    },
    "/api/leads/unviewed-count": {
      "get": {
        "tags": [
          "Leads"
        ],
        "summary": "Get count of unviewed new leads",
        "description": "Returns the count of leads with status='new' and viewed_at=NULL. Used for sidebar badge.",
        "responses": {
          "200": {
            "description": "Unviewed lead count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "description": "Number of unviewed new leads"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/leads/mark-viewed": {
      "post": {
        "tags": [
          "Leads"
        ],
        "summary": "Mark leads as viewed",
        "description": "Mark specific leads or all new+unviewed leads as viewed (sets viewed_at timestamp). Used to update sidebar badge when user visits leads page.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "leadIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional. If provided, mark these specific leads. If not provided, mark all new+unviewed leads."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Leads marked as viewed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/leads/score": {
      "post": {
        "tags": [
          "Leads"
        ],
        "summary": "Score leads based on ICP fit and engagement",
        "description": "AI scores leads 0-100 based on company size, role relevance, engagement signals, and source quality. Auto-qualifies (score ≥70) or disqualifies (score <30) leads when autoQualify is true.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "leadIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional. If provided, scores only these leads. If omitted, scores all leads."
                  },
                  "autoQualify": {
                    "type": "boolean",
                    "default": true,
                    "description": "Auto-update lead status based on score: ≥70 → qualified, <30 → disqualified"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Leads scored successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scored": {
                      "type": "integer",
                      "description": "Number of leads scored"
                    },
                    "qualified": {
                      "type": "integer",
                      "description": "Number of leads auto-qualified (score ≥70)"
                    },
                    "disqualified": {
                      "type": "integer",
                      "description": "Number of leads auto-disqualified (score <30)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "424": {
            "description": "Anthropic API key required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyRequiredError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 10 requests per minute per customer"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/outreach": {
      "get": {
        "tags": [
          "Outreach"
        ],
        "summary": "List campaigns",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search campaigns by name"
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaigns": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Campaign"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "hasMore": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Outreach"
        ],
        "summary": "Create a campaign (one-time, scheduled, or ongoing)",
        "description": "Creates a new campaign with specified leads/templates. Supports one-time runs, scheduled runs, and ongoing recurring automation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "leadIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1
                  },
                  "templateHints": {
                    "type": "string",
                    "description": "Optional AI guidance for email generation"
                  },
                  "scheduledAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO 8601 datetime when campaign should execute (future only)"
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA timezone string (e.g., America/New_York). Required if scheduledAt is provided."
                  },
                  "templateIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "maxItems": 5,
                    "description": "Selected existing template IDs to use for this campaign"
                  },
                  "automationMode": {
                    "type": "string",
                    "enum": [
                      "one_time",
                      "scheduled",
                      "ongoing"
                    ]
                  },
                  "ongoingIntervalDays": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365,
                    "deprecated": true,
                    "description": "Legacy days-only interval. Deprecated in favor of ongoingIntervalValue + ongoingIntervalUnit."
                  },
                  "ongoingIntervalValue": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Recurring interval numeric value. For minutes, minimum effective value is 5."
                  },
                  "ongoingIntervalUnit": {
                    "type": "string",
                    "enum": [
                      "minutes",
                      "hours",
                      "days"
                    ],
                    "description": "Recurring interval unit."
                  },
                  "ongoingStopAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "ongoingDiscoveryEnabled": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "name",
                  "leadIds",
                  "templateIds"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Campaign created (immediate campaigns enqueue draft generation; scheduled campaigns wait for scheduler).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaign": {
                      "$ref": "#/components/schemas/Campaign"
                    },
                    "queueJobId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (e.g., some leads not found, missing schedule date)"
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited"
          }
        }
      },
      "delete": {
        "tags": [
          "Outreach"
        ],
        "summary": "Delete campaigns",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1
                  }
                },
                "required": [
                  "ids"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Campaigns deleted"
          }
        }
      }
    },
    "/api/outreach/{id}": {
      "get": {
        "tags": [
          "Outreach"
        ],
        "summary": "Get campaign detail with emails",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign detail with outbound emails and selected templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaign": {
                      "$ref": "#/components/schemas/Campaign"
                    },
                    "selectedTemplates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "category": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "generatedBy": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "angle": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "personalizeWithAi": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "emails": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "subject": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "bodyText": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "bodyHtml": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "confidence": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "leadId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sentAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "Timestamp when the email was sent"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Timestamp when the outbound email record was created"
                          },
                          "lead": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "name": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "email": {
                                "type": "string"
                              },
                              "company": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "role": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "status": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Campaign not found"
          }
        }
      }
    },
    "/api/outreach/{id}/generate": {
      "post": {
        "tags": [
          "Outreach"
        ],
        "summary": "Generate AI drafts for campaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Generation started"
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "424": {
            "description": "Anthropic API key required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyRequiredError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 5 requests per minute per customer"
          }
        }
      }
    },
    "/api/outreach/{id}/approve": {
      "post": {
        "tags": [
          "Outreach"
        ],
        "summary": "Approve campaign drafts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emailIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "approveAll": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Drafts approved"
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          }
        },
        "description": "With `emailIds`, only emails in status draft or error/failed are approved (approving a failed email again is how it is retried; sent, opened or replied emails are never re-approved, so they can't be sent twice). Others in the list are ignored and not counted in `approved`."
      }
    },
    "/api/outreach/{id}/send": {
      "post": {
        "tags": [
          "Outreach"
        ],
        "summary": "Send approved campaign emails",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emailIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "sendAll": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sending started"
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited"
          }
        }
      }
    },
    "/api/outreach/{id}/pause": {
      "post": {
        "tags": [
          "Outreach"
        ],
        "summary": "Pause a scheduled campaign",
        "description": "Pauses a campaign that is currently scheduled for future execution. Only campaigns with status='scheduled' and pausedAt=null can be paused.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Campaign ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign paused successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Campaign is not scheduled or already paused",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Campaign not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/outreach/{id}/resume": {
      "post": {
        "tags": [
          "Outreach"
        ],
        "summary": "Resume a paused campaign",
        "description": "Resumes a campaign that was previously paused. Only campaigns with pausedAt != null can be resumed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Campaign ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign resumed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Campaign is not paused",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Campaign not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/outreach/{id}/metrics": {
      "get": {
        "tags": [
          "Outreach"
        ],
        "summary": "Get campaign performance metrics",
        "description": "Returns detailed metrics for a campaign: total sent, total replies, reply rate, average time to reply, and lead activity timeline. Useful for tracking campaign effectiveness and engagement.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Campaign ID"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Page size for leadActivity"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Offset into leadActivity"
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign metrics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaignId": {
                      "type": "string"
                    },
                    "campaignName": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "totalSent": {
                      "type": "integer",
                      "description": "Total number of emails sent"
                    },
                    "totalReplies": {
                      "type": "integer",
                      "description": "Number of emails that received a reply"
                    },
                    "replyRate": {
                      "type": "number",
                      "description": "Percentage of emails that got replies (0-100)",
                      "example": 15.5
                    },
                    "avgTimeToReply": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Average time to reply in hours",
                      "example": 24.5
                    },
                    "leadActivity": {
                      "type": "array",
                      "description": "Leads this campaign emailed that have activity, most recent first; one page (limit/offset)",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "email": {
                            "type": "string"
                          },
                          "company": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "lastActivityAt": {
                            "type": "integer",
                            "description": "Unix timestamp of last activity"
                          }
                        }
                      }
                    },
                    "leadActivityTotal": {
                      "type": "integer",
                      "description": "All campaign leads with activity"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Campaign not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/outreach/scheduled": {
      "get": {
        "tags": [
          "Outreach"
        ],
        "summary": "List scheduled campaigns",
        "description": "Returns all campaigns with status='scheduled', including both active and paused ones. Results are sorted by scheduledAt ascending.",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of scheduled campaigns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaigns": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Campaign"
                      },
                      "description": "All scheduled campaigns"
                    },
                    "upcoming": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Campaign"
                      },
                      "description": "Scheduled campaigns that are not paused"
                    },
                    "paused": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Campaign"
                      },
                      "description": "Scheduled campaigns that are paused"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/drafts": {
      "get": {
        "tags": [
          "Inbox"
        ],
        "summary": "List inbound emails/drafts",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status. Supports comma-separated values (e.g. pending,drafted).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Search by sender name, email, subject, or category.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Inbound email list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emails": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InboundEmail"
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Trigger manual email sync",
        "responses": {
          "200": {
            "description": "Sync queued or already running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "already_running"
                      ]
                    },
                    "jobId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited"
          }
        }
      }
    },
    "/api/drafts/redraft": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Re-draft emails with updated knowledge",
        "description": "Re-processes eligible emails (pending, drafted, skipped, failed, rejected) through the AI pipeline using the latest knowledge base. Emails that were already sent or approved are never re-drafted. Rate limited to 1 request per 30 seconds per customer.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emailIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional list of specific email IDs to re-draft. If omitted, all eligible emails are re-drafted."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Re-draft results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer",
                      "description": "Total emails processed"
                    },
                    "succeeded": {
                      "type": "integer",
                      "description": "Successfully re-drafted"
                    },
                    "failed": {
                      "type": "integer",
                      "description": "Failed to re-draft"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Error messages for failed emails"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Some requested emails are not eligible"
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "424": {
            "description": "Anthropic API key required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyRequiredError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Re-drafting all pending drafts (no emailIds): 1 per 30 seconds; re-drafting chosen emails: 10 per minute. Body: { error }"
          }
        }
      }
    },
    "/api/drafts/batch-approve": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Batch-approve and send qualifying drafts",
        "description": "Approves and sends drafted/pending emails with confidence at or above the NEXT_PUBLIC_INBOX_AUTO_APPROVE_CONFIDENCE threshold (default 0.85), newest first, at most 50 per call. Used when the user turns on auto-approve and confirms the batch. Each email is sent the same way as a single Approve (same Gmail thread, counted in the month's usage, marked \"auto-approve\" so the Inbox shows it as auto-sent). Sends one `emails_batch_sent` event when at least one was sent.",
        "responses": {
          "200": {
            "description": "Batch approve results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approved": {
                      "type": "integer",
                      "description": "Number of emails approved and sent"
                    },
                    "failed": {
                      "type": "integer",
                      "description": "Number of emails that failed to send"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total qualifying emails processed"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          }
        }
      }
    },
    "/api/drafts/mark-viewed": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Mark inbound emails as viewed",
        "description": "Mark specific inbound emails as viewed to clear the 'new' indicator dot. Customer-scoped.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emailIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "IDs of emails to mark as viewed",
                    "minItems": 1,
                    "maxItems": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Emails marked as viewed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "markedCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/drafts/bulk": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Perform bulk actions on selected emails",
        "description": "Approve, reject, or hide up to 200 selected emails in one call. Approve sends each draft the same way as a single Approve (same Gmail thread, counted in the month's usage, one send at a time per email) and skips emails already sent; failures are listed in `errors` with a plain-language reason. Reject counts in the month's usage. Sends one `emails_batch_sent` event after approvals, or `email_updated` after reject/hide.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emailIds",
                  "action"
                ],
                "properties": {
                  "emailIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Array of email IDs to process",
                    "minItems": 1,
                    "maxItems": 200
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "reject",
                      "hide"
                    ],
                    "description": "Action to perform on the selected emails"
                  },
                  "reason": {
                    "type": "string",
                    "description": "Optional reason for reject action"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bulk action results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "successCount": {
                      "type": "integer",
                      "description": "Number of emails successfully processed"
                    },
                    "failedCount": {
                      "type": "integer",
                      "description": "Number of emails that failed"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total emails processed"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Array of error messages (only present if failures occurred)"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "404": {
            "description": "No emails found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body (no IDs, more than 200 IDs, or an unknown action)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/validate-key": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Validate and store an Anthropic API key",
        "description": "Validates an Anthropic API key by making a minimal API call. On success, encrypts and stores the key. If the key changed from a previous key, all knowledge base files are re-uploaded to the new key's Anthropic Files API account (fire-and-forget). If this is the first time setting a key, all existing knowledge entries are uploaded. Keys created outside an Anthropic workspace return valid=false with code workspace_required; send workspaceId to validate and store them.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "apiKey"
                ],
                "properties": {
                  "apiKey": {
                    "type": "string",
                    "description": "The Anthropic API key to validate (starts with sk-ant-)"
                  },
                  "validateOnly": {
                    "type": "boolean",
                    "description": "If true, only validate the key without storing it"
                  },
                  "workspaceId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^wrkspc_[A-Za-z0-9_-]{4,80}$",
                    "description": "Only for keys created outside an Anthropic workspace. Sent as the anthropic-workspace-id header on every Anthropic request for this customer and stored with the key."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "description": "Whether the key is valid"
                    },
                    "error": {
                      "type": "string",
                      "description": "Error message if invalid"
                    },
                    "reindexing": {
                      "type": "boolean",
                      "description": "True if knowledge base is being re-indexed for the new key (fire-and-forget)"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "workspace_required",
                        "workspace_invalid"
                      ],
                      "description": "When valid=false: workspace_required = the key was made outside a workspace, ask for the workspace ID; workspace_invalid = the given workspace ID didn't work with the key."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Settings"
        ],
        "summary": "Delete the stored Anthropic API key",
        "description": "Removes the stored API key. All Anthropic file references are cleared immediately (entries fall back to inline text in AI prompts). Anthropic files are deleted asynchronously. AI features (email classification, drafts, auto-replies) will stop working until a new key is configured.",
        "responses": {
          "200": {
            "description": "Key deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/drafts/{id}/approve": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Approve and send a draft reply",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Draft approved and sent"
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "404": {
            "description": "Email not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No draft to send",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Already sent, or being sent. Or: Gmail needs reconnecting (code GMAIL_RECONNECT_REQUIRED): Google refused the saved access. The connection is marked reauth_required and the owner is alerted once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Gmail refused or couldn't be reached (for example, the Gmail connection was removed). Nothing was marked sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Sends the draft as a Gmail reply in the original thread and marks the email sent. The same action runs when the owner taps Approve and send on a Telegram alert. A second request while the first is still sending, or after the reply was sent, returns 409, so a reply is never sent twice."
      }
    },
    "/api/drafts/{id}/edit": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Edit a draft and optionally send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "draftText": {
                    "type": "string"
                  },
                  "draftHtml": {
                    "type": "string"
                  },
                  "send": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft updated"
          },
          "404": {
            "description": "Email not found"
          },
          "409": {
            "description": "With send=true: the reply was already sent, or a send for this email is already running. Or: Gmail needs reconnecting (code GMAIL_RECONNECT_REQUIRED): Google refused the saved access. The connection is marked reauth_required and the owner is alerted once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "With send=true: Gmail refused or isn't connected. Plain-language message pointing to Settings → Email; nothing is marked sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/drafts/{id}/reject": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Reject a draft",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft rejected"
          },
          "404": {
            "description": "Email not found"
          },
          "409": {
            "description": "The reply was already sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/drafts/{id}/hide": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Hide an email permanently",
        "description": "Permanently hides an email from the inbox. Hidden emails are excluded from all inbox queries and cannot be unhidden.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Email hidden successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Email not found"
          }
        }
      }
    },
    "/api/drafts/{id}/retry": {
      "post": {
        "tags": [
          "Inbox"
        ],
        "summary": "Retry a failed email",
        "description": "Retries drafting for one email whose draft failed (status 'failed'). Resets processingAttempts and failureReason, sets status to 'drafting' and re-runs classification and draft generation in the background with the customer's own Anthropic key. Shares the 10-per-minute limit with POST /api/drafts/redraft for chosen emails.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Email ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Email retry triggered successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in"
          },
          "402": {
            "description": "PLAN_REQUIRED: drafting needs a paid plan"
          },
          "404": {
            "description": "Email not found for this customer"
          },
          "409": {
            "description": "The email's draft did not fail, so there is nothing to retry"
          },
          "424": {
            "description": "API_KEY_REQUIRED: add an Anthropic API key in Settings → API Key"
          },
          "429": {
            "description": "Too many retries; wait a minute"
          }
        }
      }
    },
    "/api/drafts/threads/{threadId}": {
      "get": {
        "tags": [
          "Inbox"
        ],
        "summary": "Get email thread",
        "description": "Fetch all emails in a Gmail thread, ordered chronologically. Returns thread metadata including participant count, date range, and all email messages in the conversation.",
        "parameters": [
          {
            "name": "threadId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Gmail thread ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Thread details with all emails",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "threadId": {
                      "type": "string"
                    },
                    "emails": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InboundEmail"
                      }
                    },
                    "metadata": {
                      "type": "object",
                      "properties": {
                        "emailCount": {
                          "type": "integer"
                        },
                        "participantCount": {
                          "type": "integer"
                        },
                        "participants": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "List of email addresses participating in the thread"
                        },
                        "dateRange": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "first": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "last": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing thread ID"
          },
          "401": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Thread not found"
          }
        }
      }
    },
    "/api/templates": {
      "get": {
        "tags": [
          "Templates"
        ],
        "summary": "List templates",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Template list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Template"
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Templates"
        ],
        "summary": "Create a template",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "personalizeWithAi": {
                    "type": "boolean",
                    "description": "Enable AI personalization (can rewrite wording). If false, template is used as written with variable substitution only."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template created"
          }
        }
      },
      "put": {
        "tags": [
          "Templates"
        ],
        "summary": "Update a template",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "personalizeWithAi": {
                    "type": "boolean",
                    "description": "Enable AI personalization (can rewrite wording). If false, template is used as written with variable substitution only."
                  }
                },
                "required": [
                  "id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template updated"
          }
        }
      },
      "delete": {
        "tags": [
          "Templates"
        ],
        "summary": "Delete a template",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Template deleted"
          }
        }
      }
    },
    "/api/templates/analytics": {
      "get": {
        "tags": [
          "Templates"
        ],
        "summary": "Get template performance analytics",
        "description": "Returns performance metrics for all templates including open rates, reply rates, average response times, and A/B test suggestions. Templates with fewer than 10 sent emails are marked as having insufficient data.",
        "responses": {
          "200": {
            "description": "Template analytics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "analytics": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "templateId": {
                            "type": "string"
                          },
                          "templateName": {
                            "type": "string"
                          },
                          "category": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "angle": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "totalSent": {
                            "type": "integer",
                            "description": "Total emails sent using this template"
                          },
                          "totalOpened": {
                            "type": "integer",
                            "description": "Total emails opened"
                          },
                          "totalReplied": {
                            "type": "integer",
                            "description": "Total emails that received replies"
                          },
                          "openRate": {
                            "type": "number",
                            "description": "Percentage of sent emails that were opened (0-100)"
                          },
                          "replyRate": {
                            "type": "number",
                            "description": "Percentage of sent emails that received replies (0-100)"
                          },
                          "avgTimeToOpenHours": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "Average hours from send to open"
                          },
                          "avgTimeToReplyHours": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "Average hours from send to reply"
                          }
                        }
                      }
                    },
                    "bestPerformers": {
                      "type": "array",
                      "description": "Top 3 templates by reply rate (minimum 10 sent)",
                      "items": {
                        "$ref": "#/components/schemas/TemplateAnalytics"
                      }
                    },
                    "worstPerformers": {
                      "type": "array",
                      "description": "Bottom 3 templates by reply rate (minimum 10 sent)",
                      "items": {
                        "$ref": "#/components/schemas/TemplateAnalytics"
                      }
                    },
                    "suggestions": {
                      "type": "array",
                      "description": "Plain-language suggestions derived from template reply-rate comparisons",
                      "items": {
                        "type": "string"
                      }
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "totalTemplates": {
                          "type": "integer",
                          "description": "Total number of templates"
                        },
                        "templatesWithData": {
                          "type": "integer",
                          "description": "Number of templates with at least 1 sent email"
                        },
                        "avgOpenRate": {
                          "type": "number",
                          "description": "Average open rate across all templates (0-100)"
                        },
                        "avgReplyRate": {
                          "type": "number",
                          "description": "Average reply rate across all templates (0-100)"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/knowledge/jobs": {
      "get": {
        "tags": [
          "Knowledge"
        ],
        "summary": "List knowledge-related queue jobs (full + single URL crawl)",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Knowledge job list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/knowledge": {
      "get": {
        "tags": [
          "Knowledge"
        ],
        "summary": "List knowledge base items",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Knowledge items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "knowledge": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/KnowledgeItem"
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "counts": {
                      "type": "object",
                      "description": "Totals per type across all entries (not just this page): crawled website pages, manual entries, uploaded documents (pdf/txt/csv).",
                      "properties": {
                        "crawled": {
                          "type": "integer"
                        },
                        "manual": {
                          "type": "integer"
                        },
                        "documents": {
                          "type": "integer"
                        }
                      },
                      "example": {
                        "crawled": 42,
                        "manual": 3,
                        "documents": 2
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Knowledge"
        ],
        "summary": "Add knowledge (crawl or manual)",
        "description": "Add knowledge entries via website crawl or manual text. Crawl with append=true queues a single-page crawl job (knowledge.crawl.single) that deduplicates by normalized URL. Crawl without append queues a full re-crawl job (knowledge.crawl) that replaces crawled pages but preserves manual/document entries. Manual entries are inserted immediately and uploaded as .md files when an API key exists.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "action": {
                        "const": "crawl"
                      },
                      "websiteUrl": {
                        "type": "string",
                        "format": "uri"
                      },
                      "append": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "action"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "action": {
                        "const": "manual"
                      },
                      "title": {
                        "type": "string"
                      },
                      "content": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "action",
                      "title"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Knowledge added or crawl queued"
          },
          "429": {
            "description": "Rate limited"
          }
        }
      },
      "delete": {
        "tags": [
          "Knowledge"
        ],
        "summary": "Delete a knowledge item",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Item deleted"
          }
        }
      }
    },
    "/api/knowledge/upload": {
      "post": {
        "tags": [
          "Knowledge"
        ],
        "summary": "Upload a document (PDF, TXT, CSV) to the knowledge base",
        "description": "Files are stored in Cloudflare R2 for user downloads and Anthropic Files API for AI usage. PDF and TXT files get an Anthropic file_id for direct AI reading. CSV content is extracted as text. Max upload size configurable via MAX_UPLOAD_SIZE_MB env var (default 10MB).",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file to upload (PDF, TXT, or CSV)"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "File uploaded successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Knowledge entry ID"
                    },
                    "title": {
                      "type": "string",
                      "description": "Original filename"
                    },
                    "contentType": {
                      "type": "string",
                      "enum": [
                        "pdf",
                        "txt",
                        "csv"
                      ]
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "R2 download URL"
                    },
                    "anthropicFileId": {
                      "type": "string",
                      "nullable": true,
                      "description": "Anthropic Files API file_id (null for CSV)"
                    },
                    "sizeBytes": {
                      "type": "integer",
                      "description": "File size in bytes"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid file type or file too large"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 5 requests per minute per customer"
          }
        }
      }
    },
    "/api/knowledge/recrawl": {
      "post": {
        "tags": [
          "Knowledge"
        ],
        "summary": "Recrawl a knowledge entry",
        "description": "Re-crawls a single website knowledge entry to update its content. Only works for entries that have a URL (not manual entries or documents).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Knowledge entry ID to recrawl"
                  }
                },
                "required": [
                  "id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Entry recrawled successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Knowledge entry ID"
                    },
                    "title": {
                      "type": "string",
                      "description": "Updated page title"
                    },
                    "crawledAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Recrawl timestamp"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Entry has no URL to recrawl or invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Knowledge entry not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 5 requests per minute per customer"
          },
          "500": {
            "description": "Failed to crawl URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/documents": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "List uploaded documents",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Document list"
          }
        }
      },
      "post": {
        "tags": [
          "Documents"
        ],
        "summary": "Upload a document",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "knowledge",
                      "attachment",
                      "import"
                    ]
                  },
                  "description": {
                    "type": "string"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Document uploaded"
          },
          "429": {
            "description": "Rate limited"
          }
        }
      },
      "delete": {
        "tags": [
          "Documents"
        ],
        "summary": "Delete a document",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Document deleted"
          }
        }
      }
    },
    "/api/documents/{id}": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Download a document",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File binary",
            "content": {
              "application/octet-stream": {}
            }
          },
          "404": {
            "description": "Document not found"
          }
        }
      }
    },
    "/api/settings": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get all settings",
        "responses": {
          "200": {
            "description": "Customer settings, reply settings, connections, crawl stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "customer": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "settings": {
                      "type": "object",
                      "additionalProperties": true,
                      "properties": {
                        "anthropicModel": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Stored model setting: null (ReachClaw default), \"latest:sonnet\", or an exact model id."
                        }
                      }
                    },
                    "connections": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "provider": {
                            "type": "string"
                          },
                          "emailAddress": {
                            "type": "string"
                          },
                          "displayName": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "reauth_required"
                            ],
                            "description": "reauth_required: Google refused the saved access (revoked, password change, expired). Nothing syncs or sends until the owner reconnects through /api/gmail/connect."
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      },
                      "description": "Email accounts that are connected or need reconnecting. Display fields only; tokens are never returned."
                    },
                    "crawlStats": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update settings",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "supportMode": {
                    "type": "string",
                    "enum": [
                      "autopilot",
                      "copilot",
                      "manual"
                    ]
                  },
                  "salesMode": {
                    "type": "string",
                    "enum": [
                      "autopilot",
                      "copilot",
                      "manual"
                    ]
                  },
                  "generalMode": {
                    "type": "string",
                    "enum": [
                      "autopilot",
                      "copilot",
                      "manual"
                    ]
                  },
                  "outboundMode": {
                    "type": "string",
                    "enum": [
                      "autopilot",
                      "copilot",
                      "manual"
                    ]
                  },
                  "notifyChannel": {
                    "oneOf": [
                      {
                        "type": "string",
                        "description": "Comma-separated channels, e.g. \"dashboard,email\""
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "dashboard",
                            "email",
                            "telegram"
                          ]
                        }
                      }
                    ],
                    "description": "Any combination of dashboard, email and telegram (or none: \"\"). Normalised and stored as a comma-separated list in the order dashboard, email, telegram. Unknown channels return 400. Including \"telegram\" only delivers once a chat is connected (POST /api/settings/telegram/link); the Telegram chat itself can't be set here.",
                    "example": "dashboard,email"
                  },
                  "notifyEmail": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "tone": {
                    "type": "string"
                  },
                  "signature": {
                    "type": "string"
                  },
                  "customInstructions": {
                    "type": "string"
                  },
                  "inboxAutoApprove": {
                    "type": "boolean"
                  },
                  "anthropicModel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "AI model for this customer's AI calls. `null` = ReachClaw's default model, `\"latest:sonnet\"` = always the newest Sonnet available to the customer's key (resolved at call time), or an exact Claude model id from GET /api/settings/models. The Anthropic API has no default model of its own.",
                    "pattern": "^(latest:sonnet|claude-[a-z0-9.-]{1,64})$",
                    "examples": [
                      null,
                      "latest:sonnet",
                      "claude-sonnet-5-5"
                    ]
                  },
                  "notifyLiveChat": {
                    "type": "boolean",
                    "description": "\"Live chat notifications\": dashboard pop-ups when a website chat visitor sends a message (default true). Separate from the dashboard channel in notifyChannel."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings updated"
          },
          "400": {
            "description": "Validation error (e.g. malformed anthropicModel)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in"
          }
        }
      }
    },
    "/api/usage": {
      "get": {
        "tags": [
          "Usage"
        ],
        "summary": "Get current plan usage",
        "description": "Returns current usage and plan limits for the authenticated customer. Useful for displaying usage bars and upgrade prompts. Usage values come from persistent per-month usage counters (UTC month), not from counting live rows: deleting chats, synced emails or campaigns does not reduce them, and a new month starts at zero.",
        "responses": {
          "200": {
            "description": "Current usage and limits",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emails": {
                      "type": "integer",
                      "description": "Inbound emails received this month (counted from persistent monthly usage counters when the email is first stored; all statuses count; survives deletion)"
                    },
                    "outbound": {
                      "type": "integer",
                      "description": "Outbound emails sent this month (counted from persistent monthly usage counters when the send succeeds)"
                    },
                    "plan": {
                      "type": "string",
                      "enum": [
                        "none",
                        "solo",
                        "growth",
                        "scale"
                      ],
                      "description": "Current plan name"
                    },
                    "limits": {
                      "type": "object",
                      "properties": {
                        "emails": {
                          "type": "integer",
                          "description": "Inbound email limit for this plan"
                        },
                        "outbound": {
                          "type": "integer",
                          "description": "Outbound email limit for this plan (-1 = unlimited)"
                        },
                        "inboxes": {
                          "type": "integer",
                          "description": "Inbox connection limit for this plan"
                        },
                        "chatReplies": {
                          "type": "integer",
                          "description": "Live chat AI answers per month for the plan (0 = not included)"
                        }
                      }
                    },
                    "chatReplies": {
                      "type": "integer",
                      "description": "Live chat AI answers this month (counted from persistent monthly usage counters when an answer is saved as complete; failed answers do not count; deleting chats does not reduce it)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/settings/website": {
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update website URL and optionally re-crawl",
        "description": "Updates the customer's primary website URL. If recrawl=true, triggers a full site re-crawl: deletes all existing crawled knowledge entries (preserves manual entries and uploaded documents), cleans up their Anthropic files, then crawls and stores new pages with Anthropic file uploads. Crawl runs asynchronously with status tracking (crawlStatus in global DB).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "websiteUrl": {
                    "type": "string"
                  },
                  "recrawl": {
                    "type": "boolean",
                    "default": false
                  }
                },
                "required": [
                  "websiteUrl"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Website updated"
          },
          "429": {
            "description": "Rate limited"
          }
        }
      }
    },
    "/api/setup-status": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get setup completion status for onboarding checklist",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Setup status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "gmailConnected": {
                      "type": "boolean",
                      "description": "Whether Gmail account is connected"
                    },
                    "hasApiKey": {
                      "type": "boolean",
                      "description": "Whether the customer has saved an Anthropic API key that validated successfully (Settings → API Key)"
                    },
                    "websiteCrawled": {
                      "type": "boolean",
                      "description": "Whether website URL has been set"
                    },
                    "hasKnowledge": {
                      "type": "boolean",
                      "description": "Whether knowledge base has at least one entry"
                    },
                    "hasProcessedEmail": {
                      "type": "boolean",
                      "description": "Informational: whether at least one email has been synced. Not counted in completionPercentage."
                    },
                    "completionPercentage": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 100,
                      "description": "Setup completion percentage (0-100) across gmailConnected, hasApiKey, websiteCrawled and hasKnowledge"
                    },
                    "liveChatEnabled": {
                      "type": "boolean",
                      "description": "Live chat is turned on (adds the install step to completionPercentage)"
                    },
                    "liveChatInstalled": {
                      "type": "boolean",
                      "description": "The chat has loaded on an allowed website at least once"
                    }
                  },
                  "required": [
                    "gmailConnected",
                    "hasApiKey",
                    "websiteCrawled",
                    "hasKnowledge",
                    "hasProcessedEmail",
                    "completionPercentage"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/ai-metrics": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get AI learning feedback metrics",
        "description": "Returns total correction count, recent corrections (last 20), and weekly confidence trend (last 12 weeks). Used to display AI learning progress in Settings page.",
        "responses": {
          "200": {
            "description": "AI learning metrics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "totalCorrections": {
                      "type": "integer",
                      "description": "Total number of corrections made by the user"
                    },
                    "recentCorrections": {
                      "type": "array",
                      "description": "Last 20 corrections (newest first)",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "originalText": {
                            "type": "string",
                            "nullable": true,
                            "description": "First 100 characters of original AI draft"
                          },
                          "correctedText": {
                            "type": "string",
                            "nullable": true,
                            "description": "First 100 characters of corrected text"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "confidenceTrend": {
                      "type": "array",
                      "description": "Weekly average confidence scores (last 12 weeks, oldest to newest)",
                      "items": {
                        "type": "object",
                        "properties": {
                          "week": {
                            "type": "string",
                            "description": "Week identifier in YYYY-WW format",
                            "example": "2026-05"
                          },
                          "avgConfidence": {
                            "type": "number",
                            "description": "Average AI confidence score for this week (0-1, rounded to 2 decimals)",
                            "minimum": 0,
                            "maximum": 1,
                            "example": 0.85
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "totalCorrections",
                    "recentCorrections",
                    "confidenceTrend"
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 10 requests per minute per customer"
          }
        }
      }
    },
    "/api/settings/mcp-servers": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "List configured MCP servers",
        "description": "Returns all MCP servers configured by the customer, without decrypted auth tokens.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of MCP servers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "hasAuthToken": {
                            "type": "boolean"
                          },
                          "features": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "support",
                                "leads",
                                "templates",
                                "chat"
                              ],
                              "description": "Where the server may be used: support = email replies, chat = the website chat (Scale, Live chat → Setup). On PUT, send the full list: it replaces the stored one."
                            }
                          },
                          "enabled": {
                            "type": "boolean"
                          },
                          "createdAt": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Add a new MCP server",
        "description": "Add a new MCP server integration. Max 5 servers per customer. URL must use HTTPS.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "example": "Billing API"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://api.example.com/mcp"
                  },
                  "authToken": {
                    "type": "string",
                    "description": "Optional bearer token for server authentication"
                  },
                  "features": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "support",
                        "leads",
                        "templates",
                        "chat"
                      ],
                      "description": "Where the server may be used: support = email replies, chat = the website chat (Scale, Live chat → Setup). On PUT, send the full list: it replaces the stored one."
                    },
                    "minItems": 1
                  }
                },
                "required": [
                  "name",
                  "url",
                  "features"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Server added successfully"
          },
          "400": {
            "description": "Validation error or max servers reached"
          },
          "402": {
            "description": "Scale plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update an existing MCP server",
        "description": "Update server properties. Pass empty string for authToken to clear it.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "authToken": {
                    "type": "string"
                  },
                  "features": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "support",
                        "leads",
                        "templates",
                        "chat"
                      ],
                      "description": "Where the server may be used: support = email replies, chat = the website chat (Scale, Live chat → Setup). On PUT, send the full list: it replaces the stored one."
                    }
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Server updated"
          },
          "404": {
            "description": "Server not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Settings"
        ],
        "summary": "Remove an MCP server",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Server removed"
          },
          "404": {
            "description": "Server not found"
          }
        }
      }
    },
    "/api/settings/mcp-servers/test": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test MCP server connectivity",
        "description": "Tests connection to a configured MCP server using the customer's Anthropic API key. Returns discovered tool names on success.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string"
                  }
                },
                "required": [
                  "serverId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Test result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "tools": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "message": {
                      "type": "string"
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/billing/checkout": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Create Stripe checkout session",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "solo",
                      "growth",
                      "scale"
                    ]
                  },
                  "interval": {
                    "type": "string",
                    "enum": [
                      "monthly",
                      "yearly"
                    ],
                    "default": "monthly",
                    "description": "Billing interval. Defaults to monthly."
                  }
                },
                "required": [
                  "plan"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Stripe not configured"
          }
        }
      }
    },
    "/api/billing/portal": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Redirect to Stripe billing portal",
        "responses": {
          "302": {
            "description": "Redirect to Stripe"
          }
        },
        "description": "Redirects the browser to the Stripe billing portal. On failure it redirects to /settings/billing?billing_error=<plain-language reason> (or /login when not signed in)."
      },
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Get Stripe billing portal URL",
        "responses": {
          "200": {
            "description": "Portal URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Billing not configured",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Billing isn't available right now. Please try again later."
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Unauthorized"
                }
              }
            }
          },
          "404": {
            "description": "No Stripe customer (no plan, or a complimentary plan with no card on file)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "There's no billing to manage yet: your plan has no card or invoices on file. Choose a plan to start paying for it."
                }
              }
            }
          }
        },
        "description": "Returns a Stripe billing portal URL (update card, invoices, cancel). Returns to /settings/billing afterwards."
      }
    },
    "/api/billing/subscription": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Get the customer's plan and how it is paid for",
        "description": "Reads the customer's plan. When a Stripe subscription exists, its live status and price are read from Stripe to fill interval and source; if Stripe can't be reached the stored record is used.",
        "responses": {
          "200": {
            "description": "Plan and billing details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": {
                      "type": "string",
                      "enum": [
                        "none",
                        "solo",
                        "growth",
                        "scale"
                      ],
                      "description": "Active plan, or none."
                    },
                    "status": {
                      "type": "string",
                      "description": "Stored subscription status (active, past_due, canceled, none...)."
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "stripe",
                        "complimentary",
                        "none"
                      ],
                      "description": "stripe = a live Stripe subscription the customer pays for; complimentary = an active plan set up by ReachClaw with no card on file; none = no active plan."
                    },
                    "interval": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "monthly",
                        "yearly",
                        null
                      ],
                      "description": "Billing interval of the Stripe subscription; null unless source is stripe."
                    },
                    "currentPeriodEnd": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When the current paid period ends (renewal or end date); null unless source is stripe."
                    },
                    "cancelAtPeriodEnd": {
                      "type": "boolean",
                      "description": "True when the subscription is cancelled and ends at currentPeriodEnd."
                    },
                    "hasBillingAccount": {
                      "type": "boolean",
                      "description": "True when the customer has a Stripe customer record, so POST /api/billing/portal can open the Stripe billing portal."
                    }
                  },
                  "required": [
                    "plan",
                    "status",
                    "source",
                    "interval",
                    "currentPeriodEnd",
                    "cancelAtPeriodEnd",
                    "hasBillingAccount"
                  ]
                },
                "example": {
                  "plan": "growth",
                  "status": "active",
                  "source": "stripe",
                  "interval": "monthly",
                  "currentPeriodEnd": "2026-11-11T09:00:00.000Z",
                  "cancelAtPeriodEnd": false,
                  "hasBillingAccount": true
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Unauthorized"
                }
              }
            }
          },
          "404": {
            "description": "Customer not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Customer not found"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Billing"
        ],
        "summary": "Switch plan or billing interval",
        "description": "Moves the customer's live Stripe subscription to another plan and/or interval. Takes effect immediately; Stripe prorates (credits unused time and adds the difference to the next invoice). The new plan is written to the database right away and confirmed by the customer.subscription.updated webhook. Customers without a live subscription (no plan, or a complimentary plan) get 409 NO_SUBSCRIPTION and should use POST /api/billing/checkout. Rate limited to 5 requests per minute per customer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "solo",
                      "growth",
                      "scale"
                    ]
                  },
                  "interval": {
                    "type": "string",
                    "enum": [
                      "monthly",
                      "yearly"
                    ]
                  }
                },
                "required": [
                  "plan",
                  "interval"
                ]
              },
              "example": {
                "plan": "scale",
                "interval": "monthly"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plan changed; returns the updated billing details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": {
                      "type": "string",
                      "enum": [
                        "none",
                        "solo",
                        "growth",
                        "scale"
                      ],
                      "description": "Active plan, or none."
                    },
                    "status": {
                      "type": "string",
                      "description": "Stored subscription status (active, past_due, canceled, none...)."
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "stripe",
                        "complimentary",
                        "none"
                      ],
                      "description": "stripe = a live Stripe subscription the customer pays for; complimentary = an active plan set up by ReachClaw with no card on file; none = no active plan."
                    },
                    "interval": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "monthly",
                        "yearly",
                        null
                      ],
                      "description": "Billing interval of the Stripe subscription; null unless source is stripe."
                    },
                    "currentPeriodEnd": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When the current paid period ends (renewal or end date); null unless source is stripe."
                    },
                    "cancelAtPeriodEnd": {
                      "type": "boolean",
                      "description": "True when the subscription is cancelled and ends at currentPeriodEnd."
                    },
                    "hasBillingAccount": {
                      "type": "boolean",
                      "description": "True when the customer has a Stripe customer record, so POST /api/billing/portal can open the Stripe billing portal."
                    }
                  },
                  "required": [
                    "plan",
                    "status",
                    "source",
                    "interval",
                    "currentPeriodEnd",
                    "cancelAtPeriodEnd",
                    "hasBillingAccount"
                  ]
                },
                "example": {
                  "plan": "scale",
                  "status": "active",
                  "source": "stripe",
                  "interval": "monthly",
                  "currentPeriodEnd": "2026-11-11T09:00:00.000Z",
                  "cancelAtPeriodEnd": false,
                  "hasBillingAccount": true
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, or already on this plan and interval",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "You're already on this plan."
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Unauthorized"
                }
              }
            }
          },
          "409": {
            "description": "No live subscription to change; start one with checkout",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "You don't have a paid subscription to change yet. Choose a plan to start one.",
                  "code": "NO_SUBSCRIPTION"
                }
              }
            }
          },
          "429": {
            "description": "Too many plan changes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Too many requests. Please try again later."
                }
              }
            }
          },
          "500": {
            "description": "Stripe rejected the change; nothing was changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Internal server error"
                }
              }
            }
          },
          "503": {
            "description": "Billing not configured",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Billing isn't available right now. Please try again later."
                }
              }
            }
          }
        }
      }
    },
    "/api/onboard": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Complete onboarding",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "websiteUrl": {
                    "type": "string",
                    "format": "uri"
                  },
                  "replyMode": {
                    "type": "string",
                    "enum": [
                      "autopilot",
                      "copilot",
                      "manual"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Onboarding completed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded",
            "description": "Rate limit: 3 requests per minute by IP address"
          }
        }
      }
    },
    "/api/events": {
      "get": {
        "tags": [
          "Real-time"
        ],
        "summary": "Server-Sent Events stream",
        "responses": {
          "200": {
            "description": "SSE stream",
            "content": {
              "text/event-stream": {}
            }
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          }
        }
      }
    },
    "/api/webhooks/stripe": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Stripe webhook handler",
        "security": [],
        "responses": {
          "200": {
            "description": "Webhook processed"
          }
        }
      }
    },
    "/api/webhooks/gmail": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Gmail push notification webhook",
        "description": "Gmail push notification receiver. Deduplicates notifications, resolves customer by connected email, and enqueues an `inbox.sync` queue job (per-customer dedupe key `inbox-sync:{customerId}`). Supports optional Bearer token auth via GMAIL_PUSH_WEBHOOK_SECRET.",
        "security": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "object",
                    "properties": {
                      "data": {
                        "type": "string",
                        "description": "Base64-encoded JSON with emailAddress and historyId"
                      },
                      "messageId": {
                        "type": "string"
                      }
                    }
                  },
                  "subscription": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Notification acknowledged"
          },
          "401": {
            "description": "Invalid auth token (when GMAIL_PUSH_WEBHOOK_SECRET is configured)"
          }
        }
      }
    },
    "/api/cron/poll-emails": {
      "get": {
        "tags": [
          "Cron"
        ],
        "summary": "Poll all customers for new emails",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Poll results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "totalCustomers": {
                      "type": "integer"
                    },
                    "enqueued": {
                      "type": "integer"
                    },
                    "durationMs": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Cron"
        ],
        "summary": "Poll all customers for new emails",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Poll results"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/cron/retry-failed": {
      "get": {
        "tags": [
          "Cron"
        ],
        "summary": "Retry failed email processing",
        "description": "Finds emails stuck in 'failed' or 'drafting' status and reprocesses them with exponential backoff (1min, 5min, 15min). After 3 attempts, marks as permanently failed and notifies admin.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Retry results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "totalCustomers": {
                      "type": "integer"
                    },
                    "totalRetried": {
                      "type": "integer"
                    },
                    "totalRecovered": {
                      "type": "integer"
                    },
                    "totalPermanentlyFailed": {
                      "type": "integer"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "durationMs": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Cron"
        ],
        "summary": "Retry failed email processing",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Retry results"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/cron/execute-campaigns": {
      "post": {
        "tags": [
          "Cron"
        ],
        "summary": "Execute scheduled campaigns",
        "description": "Checks for campaigns where scheduledAt <= now AND status='scheduled' AND pausedAt IS NULL, then executes them by generating drafts for all pending outbound emails. Runs automatically every 5 minutes via background job.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Execution results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "executed": {
                      "type": "integer",
                      "description": "Number of campaigns executed"
                    },
                    "errors": {
                      "type": "integer",
                      "description": "Number of errors encountered"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized - invalid or missing CRON_SECRET"
          }
        }
      }
    },
    "/api/admin/queue": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Admin queue overview with pagination and runtime stats",
        "description": "Admin-only endpoint for queue observability. Returns filtered jobs, aggregate counters, and worker runtime/retention settings.",
        "parameters": [
          {
            "name": "statuses",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated statuses (pending,running,completed,failed,cancelled,dead_letter)"
          },
          {
            "name": "types",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated job types"
          },
          {
            "name": "customerId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter jobs by customer id"
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by exact queue tag"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Queue jobs, aggregate stats, and runtime config",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "byStatus": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        },
                        "byType": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        },
                        "activeCustomers": {
                          "type": "integer"
                        },
                        "completed24h": {
                          "type": "integer"
                        },
                        "failed24h": {
                          "type": "integer"
                        },
                        "oldestPendingAgeSeconds": {
                          "type": "integer"
                        }
                      }
                    },
                    "runtime": {
                      "type": "object",
                      "properties": {
                        "workerId": {
                          "type": "string"
                        },
                        "started": {
                          "type": "boolean"
                        },
                        "activeJobs": {
                          "type": "integer"
                        },
                        "maxConcurrent": {
                          "type": "integer"
                        },
                        "perCustomerMax": {
                          "type": "integer"
                        },
                        "pollMs": {
                          "type": "integer"
                        },
                        "leaseMs": {
                          "type": "integer"
                        },
                        "staleSweepMs": {
                          "type": "integer"
                        },
                        "retention": {
                          "type": "object",
                          "additionalProperties": {
                            "type": [
                              "integer",
                              "string"
                            ]
                          }
                        },
                        "lastCleanup": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "additionalProperties": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not an admin"
          }
        }
      },
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Admin queue control actions",
        "description": "Admin-only queue controls. wake triggers worker pump; cleanup runs retention deletion pass immediately.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "wake",
                      "cleanup"
                    ]
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Queue action completed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not an admin"
          }
        }
      }
    },
    "/api/admin/queue/{id}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Get queue job by id (admin)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Queue job detail"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not an admin"
          },
          "404": {
            "description": "Job not found"
          }
        }
      },
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Retry or cancel queue job (admin)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "retry",
                      "cancel"
                    ]
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Queue job updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not an admin"
          },
          "404": {
            "description": "Job not found"
          }
        }
      }
    },
    "/api/templates/generate": {
      "post": {
        "tags": [
          "Templates"
        ],
        "summary": "Generate templates with AI",
        "description": "Generates 1-5 outreach templates from product knowledge. Optional instructions guide style and messaging.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "category"
                ],
                "properties": {
                  "category": {
                    "type": "string",
                    "enum": [
                      "cold_outreach",
                      "follow_up",
                      "traffic_driver"
                    ]
                  },
                  "count": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "default": 3
                  },
                  "instructions": {
                    "type": "string",
                    "description": "Optional guidance for tone, constraints, and desired message direction"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Template"
                      }
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Paid plan required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredError"
                }
              }
            }
          },
          "424": {
            "description": "Anthropic API key required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyRequiredError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/email-connections/{id}": {
      "delete": {
        "tags": [
          "Settings"
        ],
        "summary": "Unlink an email connection",
        "description": "Disconnects a linked email account, clears stored tokens, and cancels active inbox sync jobs.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Connection unlinked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "emailAddress": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Connection not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/engagement": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "List engagement events",
        "description": "Returns paginated engagement events (open, click, reply) with summary stats, suspected-bot classification fields, and filters by type/source/search/unviewed/bot. Pass campaignId and/or leadId to scope both the event list and all summary stats to one outreach campaign.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 500,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "types",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated: open,click,reply"
          },
          {
            "name": "sources",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated trigger/source names"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search in link URL and metadata JSON"
          },
          {
            "name": "unviewed",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ]
            },
            "description": "Set to 1 to return only unviewed events"
          },
          {
            "name": "bot",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "human",
                "suspected"
              ],
              "default": "all"
            },
            "description": "Filter engagement events by bot classification"
          },
          {
            "name": "campaignId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            },
            "description": "Only events for this outreach campaign. Stats (unviewed, last24h, byType, bySource, suspected bots) are scoped to the campaign too. Unknown ids return an empty result.",
            "example": "cmp_8f2k1"
          },
          {
            "name": "leadId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            },
            "description": "Only events for this lead (used by the lead detail activity timeline). Combines with campaignId; stats are scoped the same way. Unknown ids return an empty result.",
            "example": "lead_3hX9a"
          }
        ],
        "responses": {
          "200": {
            "description": "Engagement events and stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "eventType": {
                            "type": "string",
                            "enum": [
                              "open",
                              "click",
                              "reply"
                            ]
                          },
                          "source": {
                            "type": "string"
                          },
                          "occurredAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "viewedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "linkUrl": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "metadata": {
                            "type": [
                              "object",
                              "null"
                            ]
                          },
                          "isSuspectedBot": {
                            "type": "boolean"
                          },
                          "botReason": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "botConfidence": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "classifierVersion": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "leadId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "leadName": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "leadEmail": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "company": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "outboundEmailId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "outboundSubject": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "inboundEmailId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "inboundFrom": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "inboundSubject": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "campaignId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "hasMore": {
                      "type": "boolean"
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "unviewed": {
                          "type": "integer"
                        },
                        "last24h": {
                          "type": "integer"
                        },
                        "suspectedBotTotal": {
                          "type": "integer"
                        },
                        "defaultMetricMode": {
                          "type": "string",
                          "enum": [
                            "human_only"
                          ]
                        },
                        "byType": {
                          "type": "object",
                          "properties": {
                            "open": {
                              "type": "integer"
                            },
                            "click": {
                              "type": "integer"
                            },
                            "reply": {
                              "type": "integer"
                            }
                          }
                        },
                        "byTypeHuman": {
                          "type": "object",
                          "properties": {
                            "open": {
                              "type": "integer"
                            },
                            "click": {
                              "type": "integer"
                            },
                            "reply": {
                              "type": "integer"
                            }
                          }
                        },
                        "byTypeBot": {
                          "type": "object",
                          "properties": {
                            "open": {
                              "type": "integer"
                            },
                            "click": {
                              "type": "integer"
                            },
                            "reply": {
                              "type": "integer"
                            }
                          }
                        },
                        "bySource": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "source": {
                                "type": "string"
                              },
                              "count": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "400": {
            "description": "Invalid campaignId or leadId",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ]
                },
                "example": {
                  "error": "Invalid campaignId: use 1-64 letters, numbers, '-' or '_'"
                }
              }
            }
          }
        }
      }
    },
    "/api/engagement/unviewed-count": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Get unviewed engagement count",
        "description": "Returns unviewed engagement event count for sidebar badges.",
        "responses": {
          "200": {
            "description": "Unviewed count returned"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/engagement/mark-viewed": {
      "post": {
        "tags": [
          "Analytics"
        ],
        "summary": "Mark engagement events as viewed",
        "description": "Marks all or specific engagement events as viewed and returns how many were marked.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "all": {
                    "type": "boolean",
                    "description": "If true, mark all unviewed events"
                  },
                  "eventIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Specific event IDs to mark"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Events marked viewed"
          },
          "400": {
            "description": "Invalid payload"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/api/engagement/click/{token}": {
      "get": {
        "tags": [
          "Tracking"
        ],
        "summary": "Track click and redirect",
        "description": "Public tracking endpoint. Records click engagement event from encrypted token and redirects to destination URL.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to destination URL"
          }
        }
      }
    },
    "/api/engagement/open/{token}": {
      "get": {
        "tags": [
          "Tracking"
        ],
        "summary": "Track email open pixel",
        "description": "Public tracking endpoint. Records open event from encrypted token and returns a 1x1 pixel.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "1x1 tracking pixel"
          }
        }
      }
    },
    "/api/admin/billing": {
      "get": {
        "summary": "List customer subscriptions",
        "description": "List all customers with their subscription and billing information. Supports search, filtering by plan/status, and pagination.",
        "tags": [
          "Admin - Billing"
        ],
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "plan",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "none",
                "solo",
                "growth",
                "scale"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "canceled",
                "past_due",
                "none"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search by email, name, or company"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of customers with billing info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdminBillingCustomer"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — admin only"
          }
        }
      }
    },
    "/api/admin/billing/analytics": {
      "get": {
        "summary": "Subscription analytics",
        "description": "Returns MRR, ARR, subscriber counts, churn metrics, and revenue breakdown by plan. Cached for 60 seconds.",
        "tags": [
          "Admin - Billing"
        ],
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Billing analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingAnalytics"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden — admin only"
          }
        }
      }
    },
    "/api/admin/billing/{customerId}/cancel": {
      "post": {
        "summary": "Cancel customer subscription",
        "description": "Cancel a customer's Stripe subscription. Can cancel immediately or at the end of the current billing period.",
        "tags": [
          "Admin - Billing"
        ],
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "immediate": {
                    "type": "boolean",
                    "default": false,
                    "description": "If true, cancel immediately. If false, cancel at period end."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "immediate": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No Stripe subscription found"
          },
          "404": {
            "description": "Customer not found"
          },
          "503": {
            "description": "Stripe not configured"
          }
        }
      }
    },
    "/api/admin/billing/{customerId}/change-plan": {
      "post": {
        "summary": "Change customer plan",
        "description": "Change a customer's subscription plan. If the customer has a Stripe subscription, the subscription item is updated with prorated billing. If no Stripe subscription, the DB is updated directly.",
        "tags": [
          "Admin - Billing"
        ],
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "plan"
                ],
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "solo",
                      "growth",
                      "scale"
                    ]
                  },
                  "interval": {
                    "type": "string",
                    "enum": [
                      "monthly",
                      "yearly"
                    ],
                    "default": "monthly"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plan changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "plan": {
                      "type": "string"
                    },
                    "interval": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid plan or missing Stripe price configuration"
          },
          "404": {
            "description": "Customer not found"
          }
        }
      }
    },
    "/api/admin/billing/{customerId}/comp": {
      "post": {
        "summary": "Grant complimentary access",
        "description": "Grant or revoke complimentary (free) access to a paid plan without involving Stripe. Useful for beta testers, partners, or special cases. Set plan to 'none' to revoke.",
        "tags": [
          "Admin - Billing"
        ],
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "plan"
                ],
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "solo",
                      "growth",
                      "scale",
                      "none"
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "description": "Optional reason for granting comp access"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comp access granted/revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "plan": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid plan"
          },
          "404": {
            "description": "Customer not found"
          }
        }
      }
    },
    "/api/admin/seo/keywords/bulk": {
      "post": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Bulk update keyword status/priority and optionally assign content tasks",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "keywordIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "high",
                      "medium",
                      "low"
                    ]
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "approved",
                      "draft",
                      "paused",
                      "retired"
                    ]
                  },
                  "assignTask": {
                    "type": "object",
                    "properties": {
                      "title": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "new-page",
                          "update",
                          "optimize",
                          "remove"
                        ]
                      },
                      "priority": {
                        "type": "string",
                        "enum": [
                          "urgent",
                          "high",
                          "medium",
                          "low"
                        ]
                      },
                      "assignedTo": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "dueDate": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      }
                    }
                  }
                },
                "required": [
                  "keywordIds"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bulk action result"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not an admin"
          }
        }
      }
    },
    "/api/admin/seo/ai/chat": {
      "post": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Ask the AI SEO Expert",
        "description": "Admin-only chat with the Danny Postma SEO expert. Bills SEO_EXPERT_ANTHROPIC_API_KEY; the model is the admin's own Settings → API Key choice, resolved like every other AI call (null = ReachClaw default, \"latest:sonnet\", or an exact id). Appends the exchange to the conversation and stores generated titles/meta/articles as drafts.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "conversationId": {
                    "type": "string",
                    "description": "Continue an existing conversation; omitted = new conversation"
                  },
                  "message": {
                    "type": "string",
                    "minLength": 1
                  },
                  "context": {
                    "type": "object",
                    "properties": {
                      "page": {
                        "type": "string",
                        "maxLength": 2048
                      },
                      "keyword": {
                        "type": "string",
                        "maxLength": 255
                      },
                      "existingContent": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Assistant reply",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "conversationId": {
                          "type": "string"
                        },
                        "message": {
                          "type": "object",
                          "properties": {
                            "role": {
                              "type": "string",
                              "enum": [
                                "assistant"
                              ]
                            },
                            "content": {
                              "type": "string"
                            },
                            "timestamp": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        },
                        "model": {
                          "type": "string",
                          "description": "Model id actually used after resolving the admin's setting"
                        },
                        "toolsUsed": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "data": {}
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (empty message or bad context)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "The SEO expert key has no credits"
          },
          "403": {
            "description": "Not an admin"
          },
          "404": {
            "description": "Conversation not found"
          },
          "503": {
            "description": "SEO_EXPERT_ANTHROPIC_API_KEY is not set"
          }
        }
      }
    },
    "/api/admin/seo/ai/generated-content": {
      "get": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "List AI-generated SEO outputs",
        "parameters": [
          {
            "name": "conversationId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "title",
                "meta",
                "article",
                "schema",
                "headline"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "approved",
                "published"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Generated content list"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not an admin"
          }
        }
      }
    },
    "/api/admin/seo/ai/generated-content/{id}": {
      "get": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Get AI-generated content item",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Generated content item"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "put": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Update AI-generated content status/apply metadata",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "approved",
                      "published"
                    ]
                  },
                  "appliedToUrl": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "appliedAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  },
                  "score": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/admin/seo/ai/generated-content/{id}/apply": {
      "post": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Apply generated content to marketing page file or create manual apply task",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "targetUrl": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Apply result with mode=file|task"
          },
          "400": {
            "description": "Missing/invalid target URL"
          },
          "404": {
            "description": "Generated content not found"
          }
        }
      }
    },
    "/api/admin/seo/competitors": {
      "get": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "List tracked SEO competitors",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "isActive",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Competitor list"
          }
        }
      },
      "post": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Create tracked competitor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string"
                  },
                  "displayName": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "domainRating": {
                    "type": "integer"
                  },
                  "organicKeywords": {
                    "type": "integer"
                  },
                  "organicTraffic": {
                    "type": "integer"
                  },
                  "shareOfVoice": {
                    "type": "number"
                  },
                  "isActive": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "domain"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "409": {
            "description": "Domain already exists"
          }
        }
      }
    },
    "/api/admin/seo/competitors/{id}": {
      "get": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Get competitor details and top gaps",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Competitor details"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "put": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Update competitor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string"
                  },
                  "displayName": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "domainRating": {
                    "type": "integer"
                  },
                  "organicKeywords": {
                    "type": "integer"
                  },
                  "organicTraffic": {
                    "type": "integer"
                  },
                  "shareOfVoice": {
                    "type": "number"
                  },
                  "isActive": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Delete competitor and related gaps",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/admin/seo/competitors/gaps": {
      "get": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "List competitor keyword gaps",
        "parameters": [
          {
            "name": "competitorId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "opportunity",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "high",
                "medium",
                "low"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Gap list"
          }
        }
      },
      "post": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Create competitor keyword gap",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "competitorId": {
                    "type": "string"
                  },
                  "keyword": {
                    "type": "string"
                  },
                  "ourPosition": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "competitorPosition": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "searchVolume": {
                    "type": "integer"
                  },
                  "keywordDifficulty": {
                    "type": "integer"
                  },
                  "trafficPotential": {
                    "type": "integer"
                  },
                  "opportunity": {
                    "type": "string",
                    "enum": [
                      "high",
                      "medium",
                      "low"
                    ]
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "competitorId",
                  "keyword"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          }
        }
      }
    },
    "/api/admin/seo/competitors/gaps/{id}": {
      "put": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Update keyword gap",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "competitorId": {
                    "type": "string"
                  },
                  "keyword": {
                    "type": "string"
                  },
                  "ourPosition": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "competitorPosition": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "searchVolume": {
                    "type": "integer"
                  },
                  "keywordDifficulty": {
                    "type": "integer"
                  },
                  "trafficPotential": {
                    "type": "integer"
                  },
                  "opportunity": {
                    "type": "string",
                    "enum": [
                      "high",
                      "medium",
                      "low"
                    ]
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Delete keyword gap",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/admin/seo/reports/summary": {
      "get": {
        "tags": [
          "Admin SEO"
        ],
        "summary": "Preview weekly/monthly SEO report summaries without sending notifications",
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "weekly",
                "monthly"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Summary payload for selected period"
          },
          "400": {
            "description": "Unknown period"
          }
        }
      }
    },
    "/api/settings/models": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "List Anthropic models available to the customer's API key",
        "description": "Lists the models the customer's own Anthropic API key can use, newest first, for the model dropdown in Settings → API Key. Fetched from Anthropic's Models API with the customer's key and cached server-side for 1 hour. Also returns what `null` (default) and `\"latest:sonnet\"` currently resolve to. Rate limited to 30 requests/minute per customer.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Models page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "limit",
                    "offset",
                    "defaultModel",
                    "latestSonnet"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "displayName"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "claude-sonnet-5-5"
                          },
                          "displayName": {
                            "type": "string",
                            "example": "Claude Sonnet 5.5"
                          },
                          "createdAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "defaultModel": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "claude-haiku-4-5-20251001"
                        },
                        "displayName": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "example": "Claude Haiku 4.5"
                        }
                      }
                    },
                    "latestSonnet": {
                      "oneOf": [
                        {
                          "type": "object",
                          "required": [
                            "id",
                            "displayName"
                          ],
                          "properties": {
                            "id": {
                              "type": "string",
                              "example": "claude-sonnet-5-5"
                            },
                            "displayName": {
                              "type": "string",
                              "example": "Claude Sonnet 5.5"
                            },
                            "createdAt": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "format": "date-time"
                            }
                          }
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "claude-sonnet-5-5",
                      "displayName": "Claude Sonnet 5.5",
                      "createdAt": "2026-09-01T00:00:00Z"
                    }
                  ],
                  "total": 1,
                  "limit": 100,
                  "offset": 0,
                  "defaultModel": {
                    "id": "claude-haiku-4-5-20251001",
                    "displayName": "Claude Haiku 4.5"
                  },
                  "latestSonnet": {
                    "id": "claude-sonnet-5-5",
                    "displayName": "Claude Sonnet 5.5",
                    "createdAt": "2026-09-01T00:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid limit/offset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "424": {
            "description": "No valid Anthropic API key (`code: API_KEY_REQUIRED`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Anthropic's Models API couldn't be reached",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/{widgetId}/config": {
      "get": {
        "tags": [
          "Live chat (public widget)"
        ],
        "summary": "Widget settings for the loader script",
        "description": "Called by widget.js from the customer's website (cross-origin). The browser's Origin must match the widget's allowed domains (a domain also allows its subdomains; entries with a port match exactly; ReachClaw's own origin is always allowed). Returns launcher settings and a frame token for the chat iframe, or {enabled:false} when the chat is off. Counts a widget load. Rate limit: 120 per 5 minutes per IP.",
        "security": [],
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cw_[A-Za-z0-9_-]{10,32}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Widget settings",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "const": false
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "const": true
                        },
                        "widgetId": {
                          "type": "string"
                        },
                        "mode": {
                          "type": "string",
                          "enum": [
                            "chat",
                            "contact_only"
                          ],
                          "description": "contact_only = the AI can't answer right now (no or invalid API key, monthly cap reached); the widget shows a leave-a-message form"
                        },
                        "assistantName": {
                          "type": "string"
                        },
                        "theme": {
                          "type": "object",
                          "properties": {
                            "mode": {
                              "type": "string",
                              "enum": [
                                "light",
                                "dark",
                                "auto"
                              ]
                            },
                            "preset": {
                              "type": "string"
                            },
                            "brandColor": {
                              "type": "string",
                              "pattern": "^#[0-9a-fA-F]{6}$"
                            },
                            "position": {
                              "type": "string",
                              "enum": [
                                "right",
                                "left"
                              ]
                            },
                            "launcherStyle": {
                              "type": "string",
                              "enum": [
                                "round",
                                "pill"
                              ]
                            },
                            "radius": {
                              "type": "string",
                              "enum": [
                                "soft",
                                "sharp"
                              ]
                            }
                          }
                        },
                        "iframeUrl": {
                          "type": "string",
                          "format": "uri"
                        },
                        "frameToken": {
                          "type": "string"
                        },
                        "voiceEnabled": {
                          "type": "boolean",
                          "description": "Show the microphone and voice call buttons (owner turned talking on and the plan is active). The loader then adds allow=\"microphone\" to the chat iframe."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The requesting website isn't on the allowed list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown widget",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/{widgetId}/conversations": {
      "post": {
        "tags": [
          "Live chat (public widget)"
        ],
        "summary": "Start or resume a visitor's conversation",
        "description": "Called by the chat iframe. Returns the visitor's open conversation from the last 24 hours (or a new one when forceNew is true or none exists), its messages and a conversation token. Rate limit: 20 per minute per IP.",
        "security": [],
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cw_[A-Za-z0-9_-]{10,32}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "frameToken",
                  "visitorId"
                ],
                "properties": {
                  "frameToken": {
                    "type": "string"
                  },
                  "visitorId": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{8,64}$"
                  },
                  "pageUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 2000
                  },
                  "forceNew": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Conversation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "conversationToken": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "open",
                        "escalated",
                        "resolved",
                        "closed"
                      ]
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "chat",
                        "contact_only"
                      ]
                    },
                    "assistantName": {
                      "type": "string"
                    },
                    "companyName": {
                      "type": "string"
                    },
                    "greeting": {
                      "type": "string"
                    },
                    "starters": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "maxItems": 4
                    },
                    "handoffEnabled": {
                      "type": "boolean"
                    },
                    "showBranding": {
                      "type": "boolean"
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "seq": {
                            "type": "integer"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "visitor",
                              "assistant",
                              "owner"
                            ]
                          },
                          "content": {
                            "type": "string"
                          },
                          "feedback": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "enum": [
                              1,
                              -1,
                              null
                            ]
                          },
                          "sources": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "format": "uri"
                            }
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "visitorEmail": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The visitor's email if they left one. The chat only promises an email reply when this is set."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Frame token missing, invalid or expired",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Chat is turned off (code chat_off)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown widget",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/{widgetId}/conversations/{id}": {
      "patch": {
        "tags": [
          "Live chat (public widget)"
        ],
        "summary": "Mark a conversation resolved (visitor tapped \"That answered it\")",
        "security": [
          {
            "liveChatConversation": []
          }
        ],
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cw_[A-Za-z0-9_-]{10,32}$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "const": "resolved"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or expired conversation token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/{widgetId}/conversations/{id}/messages": {
      "get": {
        "tags": [
          "Live chat (public widget)"
        ],
        "summary": "List messages (oldest first)",
        "description": "Used to restore a chat and to poll for replies from a person while the chat is handed over.",
        "security": [
          {
            "liveChatConversation": []
          }
        ],
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cw_[A-Za-z0-9_-]{10,32}$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Only messages with seq greater than this"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "seq": {
                            "type": "integer"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "visitor",
                              "assistant",
                              "owner"
                            ]
                          },
                          "content": {
                            "type": "string"
                          },
                          "feedback": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "enum": [
                              1,
                              -1,
                              null
                            ]
                          },
                          "sources": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "format": "uri"
                            }
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "open",
                        "escalated",
                        "resolved",
                        "closed"
                      ],
                      "description": "closed/resolved: the chat has ended; the widget offers a new chat."
                    },
                    "handoffEnabled": {
                      "type": "boolean",
                      "description": "Current \"Let visitors ask for a person\" setting, so open chats follow changes."
                    },
                    "visitorEmail": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The visitor's email if they left one. The chat only promises an email reply when this is set."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or expired conversation token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Live chat (public widget)"
        ],
        "summary": "Send a visitor message and stream the answer",
        "description": "Stores the visitor's message and streams the AI answer as server-sent events (text/event-stream): `meta` {visitorMessageId, assistantMessageId}, `status` {state: looking_up} (before an answer that may use the customer's MCP tools; that answer then arrives as one `delta`), `delta` {text}, `sources` {urls}, `done` {covered, handoff, firstTokenMs, waitingForPerson?}, `error` {code: ai_unavailable | timeout | ai_error | busy | conversation_closed | too_many_turns, message?}. Answers come only from the customer's knowledge and use the customer's own Anthropic key and model. Max 1,000 characters, 40 visitor messages per conversation, one answer at a time. Rate limits: 20 per 5 min per visitor, 60 per 5 min per IP, 600 per 5 min per widget. Optional `identity` comes from ReachClawChat.identify() on the customer's page: hex HMAC-SHA256 of the lowercased email with the widget's identity secret. It is checked on every message; a missing or wrong signature means the visitor is treated as not signed in (never an error).",
        "security": [
          {
            "liveChatConversation": []
          }
        ],
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cw_[A-Za-z0-9_-]{10,32}$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 1000
                  },
                  "identity": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "required": [
                      "email",
                      "signature"
                    ],
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email",
                        "maxLength": 254
                      },
                      "signature": {
                        "type": "string",
                        "pattern": "^[0-9a-fA-F]{64}$"
                      }
                    },
                    "example": {
                      "email": "ann@example.com",
                      "signature": "3f1c…64 hex chars"
                    }
                  },
                  "inputMode": {
                    "type": "string",
                    "enum": [
                      "typed",
                      "spoken"
                    ],
                    "description": "\"spoken\" when the visitor used the microphone (browser speech-to-text). Stored only as a label for the owner; never trusted for anything else.",
                    "default": "typed"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body (empty or too long)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or expired conversation token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/{widgetId}/conversations/{id}/messages/{messageId}/feedback": {
      "post": {
        "tags": [
          "Live chat (public widget)"
        ],
        "summary": "Thumbs up/down on an AI answer",
        "security": [
          {
            "liveChatConversation": []
          }
        ],
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cw_[A-Za-z0-9_-]{10,32}$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "messageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rating"
                ],
                "properties": {
                  "rating": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "enum": [
                      1,
                      -1,
                      null
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "seq": {
                      "type": "integer"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "visitor",
                        "assistant",
                        "owner"
                      ]
                    },
                    "content": {
                      "type": "string"
                    },
                    "feedback": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "enum": [
                        1,
                        -1,
                        null
                      ]
                    },
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uri"
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or expired conversation token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Message not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/{widgetId}/conversations/{id}/escalation": {
      "post": {
        "tags": [
          "Live chat (public widget)"
        ],
        "summary": "Ask for a person / leave a message",
        "description": "The visitor leaves an email (and optional note); the conversation is handed to the business owner. reason ai_unavailable is used when the AI can't answer. Rate limit: 10 per 10 minutes per IP. Allowed when \"ask for a person\" is off for the leave-a-message fallback (reason ai_unavailable) and to leave an email in a chat the owner already replied in. 409 already_escalated only when the chat is handed over and already has the visitor's email.",
        "security": [
          {
            "liveChatConversation": []
          }
        ],
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cw_[A-Za-z0-9_-]{10,32}$"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 120
                  },
                  "note": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 2000
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "visitor_request",
                      "ai_policy",
                      "ai_unavailable"
                    ],
                    "default": "visitor_request"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Escalated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "visitorEmail": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The visitor's email if they left one. The chat only promises an email reply when this is set."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or expired conversation token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Handoff is turned off for this widget (code handoff_off)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Already handed over (code already_escalated)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/{widgetId}/events": {
      "post": {
        "tags": [
          "Live chat (public widget)"
        ],
        "summary": "Count a chat open (analytics)",
        "security": [],
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cw_[A-Za-z0-9_-]{10,32}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "frameToken",
                  "type"
                ],
                "properties": {
                  "frameToken": {
                    "type": "string"
                  },
                  "type": {
                    "const": "open"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Counted"
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid frame token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown widget",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/live-chat/widget": {
      "get": {
        "tags": [
          "Live chat"
        ],
        "summary": "Get the customer's chat widget and setup status",
        "description": "Creates a turned-off widget on first call (one per customer). Default allowed domain is the customer's website.",
        "responses": {
          "200": {
            "description": "Widget",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "widget": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "allowedDomains": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "assistantName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "greeting": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "starters": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "maxItems": 4
                        },
                        "handoffEnabled": {
                          "type": "boolean"
                        },
                        "showBranding": {
                          "type": "boolean"
                        },
                        "theme": {
                          "type": "object",
                          "properties": {
                            "mode": {
                              "type": "string",
                              "enum": [
                                "light",
                                "dark",
                                "auto"
                              ]
                            },
                            "preset": {
                              "type": "string"
                            },
                            "brandColor": {
                              "type": "string",
                              "pattern": "^#[0-9a-fA-F]{6}$"
                            },
                            "position": {
                              "type": "string",
                              "enum": [
                                "right",
                                "left"
                              ]
                            },
                            "launcherStyle": {
                              "type": "string",
                              "enum": [
                                "round",
                                "pill"
                              ]
                            },
                            "radius": {
                              "type": "string",
                              "enum": [
                                "soft",
                                "sharp"
                              ]
                            }
                          }
                        },
                        "firstLoadedAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "firstLoadedOrigin": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "mcpEnabled": {
                          "type": "boolean"
                        },
                        "hasIdentitySecret": {
                          "type": "boolean"
                        },
                        "voiceEnabled": {
                          "type": "boolean",
                          "description": "Visitors can talk to the chat instead of typing (browser speech recognition; the audio never reaches ReachClaw). Off by default; turning it on needs a paid plan (402 PLAN_REQUIRED)."
                        }
                      }
                    },
                    "embedScript": {
                      "type": "string",
                      "example": "<script async src=\"https://reachclaw.com/widget.js\" data-widget=\"cw_xxxxxxxxxxxxxxxx\"></script>"
                    },
                    "testPageUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "status": {
                      "type": "object",
                      "properties": {
                        "plan": {
                          "type": "string"
                        },
                        "planActive": {
                          "type": "boolean"
                        },
                        "apiKeyStatus": {
                          "type": "string",
                          "enum": [
                            "none",
                            "valid",
                            "invalid"
                          ]
                        },
                        "knowledgeCount": {
                          "type": "integer"
                        },
                        "mode": {
                          "type": "string",
                          "enum": [
                            "chat",
                            "contact_only",
                            "off"
                          ]
                        },
                        "reason": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "disabled",
                            "plan_required",
                            "no_api_key",
                            "key_invalid",
                            "quota_reached",
                            null
                          ]
                        },
                        "canHideBranding": {
                          "type": "boolean"
                        },
                        "canUseTools": {
                          "type": "boolean",
                          "description": "Active Scale plan: the chat may use MCP tools."
                        }
                      }
                    },
                    "usage": {
                      "type": "object",
                      "properties": {
                        "chatReplies": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "-1 = unlimited, 0 = not included"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Customer not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Live chat"
        ],
        "summary": "Update chat settings",
        "description": "Turning the chat on needs a paid plan (402 PLAN_REQUIRED). It can be on before an API key exists; visitors then see a leave-a-message form. Hiding the branding needs Growth or Scale (402). The chat can be on with an empty allowedDomains list: it then only works on the ReachClaw test page. `mcpEnabled` lets the chat look things up with the customer's MCP servers that have the `chat` scope; turning it on needs an active Scale plan (402 PLAN_REQUIRED).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "allowedDomains": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "string",
                      "example": "example.com"
                    }
                  },
                  "assistantName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 60
                  },
                  "greeting": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 300
                  },
                  "starters": {
                    "type": "array",
                    "maxItems": 4,
                    "items": {
                      "type": "string",
                      "maxLength": 120
                    }
                  },
                  "handoffEnabled": {
                    "type": "boolean"
                  },
                  "showBranding": {
                    "type": "boolean"
                  },
                  "theme": {
                    "type": "object",
                    "properties": {
                      "mode": {
                        "type": "string",
                        "enum": [
                          "light",
                          "dark",
                          "auto"
                        ]
                      },
                      "preset": {
                        "type": "string"
                      },
                      "brandColor": {
                        "type": "string",
                        "pattern": "^#[0-9a-fA-F]{6}$"
                      },
                      "position": {
                        "type": "string",
                        "enum": [
                          "right",
                          "left"
                        ]
                      },
                      "launcherStyle": {
                        "type": "string",
                        "enum": [
                          "round",
                          "pill"
                        ]
                      },
                      "radius": {
                        "type": "string",
                        "enum": [
                          "soft",
                          "sharp"
                        ]
                      }
                    }
                  },
                  "mcpEnabled": {
                    "type": "boolean",
                    "description": "Look things up in your systems (MCP). Scale plan."
                  },
                  "voiceEnabled": {
                    "type": "boolean",
                    "description": "Visitors can talk to the chat instead of typing (browser speech recognition; the audio never reaches ReachClaw). Off by default; turning it on needs a paid plan (402 PLAN_REQUIRED)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated widget",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "widget": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "allowedDomains": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "assistantName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "greeting": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "starters": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "maxItems": 4
                        },
                        "handoffEnabled": {
                          "type": "boolean"
                        },
                        "showBranding": {
                          "type": "boolean"
                        },
                        "theme": {
                          "type": "object",
                          "properties": {
                            "mode": {
                              "type": "string",
                              "enum": [
                                "light",
                                "dark",
                                "auto"
                              ]
                            },
                            "preset": {
                              "type": "string"
                            },
                            "brandColor": {
                              "type": "string",
                              "pattern": "^#[0-9a-fA-F]{6}$"
                            },
                            "position": {
                              "type": "string",
                              "enum": [
                                "right",
                                "left"
                              ]
                            },
                            "launcherStyle": {
                              "type": "string",
                              "enum": [
                                "round",
                                "pill"
                              ]
                            },
                            "radius": {
                              "type": "string",
                              "enum": [
                                "soft",
                                "sharp"
                              ]
                            }
                          }
                        },
                        "firstLoadedAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "firstLoadedOrigin": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "mcpEnabled": {
                          "type": "boolean"
                        },
                        "hasIdentitySecret": {
                          "type": "boolean"
                        },
                        "voiceEnabled": {
                          "type": "boolean",
                          "description": "Visitors can talk to the chat instead of typing (browser speech recognition; the audio never reaches ReachClaw). Off by default; turning it on needs a paid plan (402 PLAN_REQUIRED)."
                        }
                      }
                    },
                    "embedScript": {
                      "type": "string",
                      "example": "<script async src=\"https://reachclaw.com/widget.js\" data-widget=\"cw_xxxxxxxxxxxxxxxx\"></script>"
                    },
                    "testPageUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "status": {
                      "type": "object",
                      "properties": {
                        "plan": {
                          "type": "string"
                        },
                        "planActive": {
                          "type": "boolean"
                        },
                        "apiKeyStatus": {
                          "type": "string",
                          "enum": [
                            "none",
                            "valid",
                            "invalid"
                          ]
                        },
                        "knowledgeCount": {
                          "type": "integer"
                        },
                        "mode": {
                          "type": "string",
                          "enum": [
                            "chat",
                            "contact_only",
                            "off"
                          ]
                        },
                        "reason": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "disabled",
                            "plan_required",
                            "no_api_key",
                            "key_invalid",
                            "quota_reached",
                            null
                          ]
                        },
                        "canHideBranding": {
                          "type": "boolean"
                        },
                        "canUseTools": {
                          "type": "boolean",
                          "description": "Active Scale plan: the chat may use MCP tools."
                        }
                      }
                    },
                    "usage": {
                      "type": "object",
                      "properties": {
                        "chatReplies": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "-1 = unlimited, 0 = not included"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, bad domains, or no domain when turning on",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Paid plan required (code PLAN_REQUIRED)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/live-chat/conversations": {
      "get": {
        "tags": [
          "Live chat"
        ],
        "summary": "List website chats",
        "description": "Newest activity first; dashboard preview chats are left out. needs_person = handed over and not replied to, or with an unread visitor message.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "needs_person",
                "open",
                "resolved"
              ],
              "default": "all"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Search last message, visitor email or page URL"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Chats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "open",
                              "escalated",
                              "resolved",
                              "closed"
                            ]
                          },
                          "source": {
                            "type": "string"
                          },
                          "visitorEmail": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "visitorName": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "visitorVerified": {
                            "type": "boolean"
                          },
                          "pageUrl": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "pageOrigin": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "escalatedAt": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "escalationReason": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "escalationNote": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "ownerRepliedAt": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "resolvedAt": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "messageCount": {
                            "type": "integer"
                          },
                          "lastMessageAt": {
                            "type": "string"
                          },
                          "lastMessagePreview": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "hasThumbsDown": {
                            "type": "boolean"
                          },
                          "hadUnanswered": {
                            "type": "boolean"
                          },
                          "unread": {
                            "type": "boolean"
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/live-chat/conversations/{id}": {
      "get": {
        "tags": [
          "Live chat"
        ],
        "summary": "One chat with its messages",
        "description": "Marks the chat as viewed (emits SSE chat_viewed).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Chat",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "open",
                            "escalated",
                            "resolved",
                            "closed"
                          ]
                        },
                        "source": {
                          "type": "string"
                        },
                        "visitorEmail": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "visitorName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "visitorVerified": {
                          "type": "boolean"
                        },
                        "pageUrl": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "pageOrigin": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "escalatedAt": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "escalationReason": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "escalationNote": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "ownerRepliedAt": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "resolvedAt": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "messageCount": {
                          "type": "integer"
                        },
                        "lastMessageAt": {
                          "type": "string"
                        },
                        "lastMessagePreview": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "hasThumbsDown": {
                          "type": "boolean"
                        },
                        "hadUnanswered": {
                          "type": "boolean"
                        },
                        "unread": {
                          "type": "boolean"
                        },
                        "createdAt": {
                          "type": "string"
                        }
                      }
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "seq": {
                            "type": "integer"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "visitor",
                              "assistant",
                              "owner"
                            ]
                          },
                          "content": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "covered": {
                            "type": [
                              "boolean",
                              "null"
                            ],
                            "description": "false = the knowledge didn't answer this question"
                          },
                          "topic": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "feedback": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "firstTokenMs": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "sources": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "tools": {
                            "type": "array",
                            "items": {}
                          },
                          "createdAt": {
                            "type": "string"
                          },
                          "inputMode": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "spoken",
                              null
                            ],
                            "description": "\"spoken\" = the visitor used the microphone; null = typed."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Chat not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Live chat"
        ],
        "summary": "Change chat status",
        "description": "open = let the AI answer again (hand back), resolved, or closed (end).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "closed",
                      "resolved"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated chat",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "open",
                        "escalated",
                        "resolved",
                        "closed"
                      ]
                    },
                    "source": {
                      "type": "string"
                    },
                    "visitorEmail": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "visitorName": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "visitorVerified": {
                      "type": "boolean"
                    },
                    "pageUrl": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "pageOrigin": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "escalatedAt": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "escalationReason": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "escalationNote": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "ownerRepliedAt": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "resolvedAt": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "messageCount": {
                      "type": "integer"
                    },
                    "lastMessageAt": {
                      "type": "string"
                    },
                    "lastMessagePreview": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "hasThumbsDown": {
                      "type": "boolean"
                    },
                    "hadUnanswered": {
                      "type": "boolean"
                    },
                    "unread": {
                      "type": "boolean"
                    },
                    "createdAt": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Chat not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Live chat"
        ],
        "summary": "Delete a chat",
        "description": "Deletes the chat and all its messages for good. A visitor who still has it open sees \"This chat has ended\" and can start a new chat. Sends the `chat_updated` SSE event so the sidebar badge updates. Rate limited to 60 a minute.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Chat not found"
          },
          "429": {
            "description": "Too many requests"
          }
        }
      }
    },
    "/api/live-chat/conversations/{id}/messages": {
      "post": {
        "tags": [
          "Live chat"
        ],
        "summary": "Reply to a visitor as a person",
        "description": "Takes the chat over (status escalated; the AI stays quiet until handed back). The visitor's open chat shows the reply within seconds. alsoEmail sends it to the visitor's email with your notification email (or login email) as reply-to. Rate limit 60/min.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "alsoEmail": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Reply stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "seq": {
                          "type": "integer"
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "visitor",
                            "assistant",
                            "owner"
                          ]
                        },
                        "content": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "covered": {
                          "type": [
                            "boolean",
                            "null"
                          ],
                          "description": "false = the knowledge didn't answer this question"
                        },
                        "topic": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "feedback": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "firstTokenMs": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "sources": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "tools": {
                          "type": "array",
                          "items": {}
                        },
                        "createdAt": {
                          "type": "string"
                        },
                        "inputMode": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "spoken",
                            null
                          ],
                          "description": "\"spoken\" = the visitor used the microphone; null = typed."
                        }
                      }
                    },
                    "emailed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or alsoEmail without a visitor email",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Chat not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/live-chat/unviewed-count": {
      "get": {
        "tags": [
          "Live chat"
        ],
        "summary": "Chats needing attention (sidebar badge)",
        "responses": {
          "200": {
            "description": "Count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Sidebar badge count, like the Inbox badge: chats with a visitor message the owner hasn't opened yet (new chats and new messages) plus chats handed to a person that haven't been replied to. Kept live by the SSE events chat_message, chat_escalated, chat_viewed and chat_updated."
      }
    },
    "/api/live-chat/widget/suggestions": {
      "post": {
        "tags": [
          "Live chat"
        ],
        "summary": "Suggest the chat's name, first message and questions",
        "description": "From the customer's knowledge. useAI=true writes them with the customer's Anthropic key and model (uses their credits; 10 per hour); useAI=false picks real questions found in the knowledge for free. Nothing is saved. empty=true when there's no knowledge yet.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "useAI"
                ],
                "properties": {
                  "useAI": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suggestions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assistantName": {
                      "type": "string"
                    },
                    "greeting": {
                      "type": "string"
                    },
                    "starters": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "maxItems": 4
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "ai",
                        "knowledge"
                      ]
                    },
                    "empty": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid body",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Paid plan required (PLAN_REQUIRED)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "424": {
            "description": "No working Anthropic key (API_KEY_REQUIRED)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/telegram": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Telegram alerts status",
        "description": "Whether Telegram alerts are available on this server and whether the owner connected a chat. The chat id is never returned. The Settings page polls this while the connect steps are open.",
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "available",
                    "connected",
                    "botUsername"
                  ],
                  "properties": {
                    "available": {
                      "type": "boolean",
                      "description": "This server has a Telegram bot. False: Telegram alerts can't be turned on."
                    },
                    "connected": {
                      "type": "boolean",
                      "description": "The owner connected a Telegram chat."
                    },
                    "botUsername": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The bot's @username without the @, or null when unavailable or Telegram can't be reached."
                    }
                  },
                  "example": {
                    "available": true,
                    "connected": false,
                    "botUsername": "ReachClawBot"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Settings"
        ],
        "summary": "Disconnect Telegram",
        "description": "Removes the connected chat and turns the Telegram channel off. Idempotent. The bot also disconnects a chat on its own when the owner blocks the bot or the chat is deleted.",
        "responses": {
          "200": {
            "description": "Disconnected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "connected": {
                      "type": "boolean",
                      "const": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/telegram/link": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Create a one-tap Telegram connect link",
        "description": "Returns `https://t.me/<bot>?start=<payload>`. The payload is signed (HMAC) for this account and expires after 15 minutes; nothing is stored until the owner opens the link and taps Start, which saves that private chat for alerts and adds \"telegram\" to the notification channels. Group chats are refused. Rate limited to 10 links per 10 minutes per account.",
        "responses": {
          "201": {
            "description": "Link created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "botUsername",
                    "expiresAt"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "botUsername": {
                      "type": "string"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  },
                  "example": {
                    "url": "https://t.me/ReachClawBot?start=KQVH6U2Plcah07aEE17Sz0t3k9qzAbCdEfGhIjKlMnOpQrSt",
                    "botUsername": "ReachClawBot",
                    "expiresAt": "2026-10-11T23:59:00.000Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many links requested",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Telegram isn't available on this server, or Telegram can't be reached",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/analytics/chat": {
      "get": {
        "tags": [
          "Analytics",
          "Live chat"
        ],
        "summary": "Website chat analytics",
        "description": "Headline numbers, chats per day (UTC days), top topics and a weekday x hour grid of chats started in the given time zone. Only chats from the customer's website count (source widget); test-page and preview chats are left out. Handed to a person = the chat was ever handed over. Resolved % = resolved / finished (resolved, closed or handed over) chats. Percentages are null when there is nothing to divide by.",
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "schema": {
              "type": "integer",
              "enum": [
                7,
                30,
                90,
                365
              ],
              "default": 30
            },
            "description": "Days to include, counting back from now"
          },
          {
            "name": "tz",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 64,
              "default": "UTC"
            },
            "description": "IANA time zone for the weekday x hour grid, e.g. America/Los_Angeles"
          }
        ],
        "responses": {
          "200": {
            "description": "Chat analytics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range": {
                      "type": "integer"
                    },
                    "timeZone": {
                      "type": "string"
                    },
                    "hasWidget": {
                      "type": "boolean",
                      "description": "False when the chat was never set up"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "conversations": {
                          "type": "integer"
                        },
                        "openNow": {
                          "type": "integer"
                        },
                        "aiAnswers": {
                          "type": "integer"
                        },
                        "resolvedPct": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "handedOver": {
                          "type": "integer"
                        },
                        "handedOverPct": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "medianFirstWordMs": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "Median time until the first words of an AI answer appeared"
                        },
                        "ratedUp": {
                          "type": "integer"
                        },
                        "ratedDown": {
                          "type": "integer"
                        },
                        "ratedUpPct": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "unanswered": {
                          "type": "integer",
                          "description": "AI answers that said the knowledge didn't cover the question"
                        },
                        "widgetLoads": {
                          "type": "integer"
                        },
                        "widgetOpens": {
                          "type": "integer"
                        }
                      }
                    },
                    "timeline": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "example": "2026-10-11"
                          },
                          "conversations": {
                            "type": "integer"
                          },
                          "handedOver": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "topics": {
                      "type": "array",
                      "maxItems": 8,
                      "items": {
                        "type": "object",
                        "properties": {
                          "topic": {
                            "type": "string"
                          },
                          "answers": {
                            "type": "integer"
                          },
                          "unanswered": {
                            "type": "integer"
                          },
                          "ratedDown": {
                            "type": "integer"
                          }
                        }
                      },
                      "description": "Most asked topics (greetings and unclear questions left out)"
                    },
                    "heatmap": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "weekday": {
                            "type": "string",
                            "enum": [
                              "Sun",
                              "Mon",
                              "Tue",
                              "Wed",
                              "Thu",
                              "Fri",
                              "Sat"
                            ]
                          },
                          "hours": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            },
                            "minItems": 24,
                            "maxItems": 24
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "range": 30,
                  "timeZone": "America/Los_Angeles",
                  "hasWidget": true,
                  "summary": {
                    "conversations": 57,
                    "openNow": 0,
                    "aiAnswers": 80,
                    "resolvedPct": 65,
                    "handedOver": 8,
                    "handedOverPct": 14,
                    "medianFirstWordMs": 1450,
                    "ratedUp": 18,
                    "ratedDown": 10,
                    "ratedUpPct": 64,
                    "unanswered": 16,
                    "widgetLoads": 1879,
                    "widgetOpens": 208
                  },
                  "timeline": [
                    {
                      "date": "2026-10-11",
                      "conversations": 3,
                      "handedOver": 1
                    }
                  ],
                  "topics": [
                    {
                      "topic": "delivery areas",
                      "answers": 15,
                      "unanswered": 0,
                      "ratedDown": 1
                    }
                  ],
                  "heatmap": [
                    {
                      "weekday": "Mon",
                      "hours": [
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0,
                        0
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "range or tz is not valid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/live-chat/questions": {
      "get": {
        "tags": [
          "Live chat"
        ],
        "summary": "Chat answers to follow up on",
        "description": "Newest first. unanswered = the AI said your knowledge didn't cover the question; rated_down = a visitor tapped thumbs-down. The same question asked in several chats is returned once (timesAsked). Answers the owner already answered or dismissed are left out. Only chats from the customer's website count.",
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "unanswered",
                "rated_down"
              ],
              "default": "unanswered"
            }
          },
          {
            "name": "range",
            "in": "query",
            "schema": {
              "type": "integer",
              "enum": [
                7,
                30,
                90,
                365
              ],
              "default": 30
            },
            "description": "Days to include, counting back from now"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Questions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "The newest AI answer to this question (use it for answer/dismiss)"
                          },
                          "conversationId": {
                            "type": "string"
                          },
                          "question": {
                            "type": "string",
                            "description": "What the visitor asked"
                          },
                          "answer": {
                            "type": "string",
                            "description": "What the AI answered"
                          },
                          "topic": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "timesAsked": {
                            "type": "integer",
                            "description": "How many open answers share this question (same text, ignoring case)"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Number of distinct questions"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "msg_123",
                      "conversationId": "conv_456",
                      "question": "Do you carry semolina flour?",
                      "answer": "I couldn't find semolina in what I know about the products.",
                      "topic": "products",
                      "timesAsked": 3,
                      "createdAt": "2026-10-07T21:13:56.000Z"
                    }
                  ],
                  "total": 1,
                  "limit": 20,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "kind or range is not valid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/live-chat/questions/{id}": {
      "delete": {
        "tags": [
          "Live chat"
        ],
        "summary": "Dismiss a question",
        "description": "Takes the question off both follow-up lists, together with every other open answer to the same question. Nothing is added to Knowledge and the chat is kept.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Answer id from GET /api/live-chat/questions"
          }
        ],
        "responses": {
          "200": {
            "description": "Dismissed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Question not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This question was already handled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/live-chat/questions/{id}/answer": {
      "post": {
        "tags": [
          "Live chat"
        ],
        "summary": "Answer a question (adds it to Knowledge)",
        "description": "Saves the owner's answer as a manual Knowledge note (used by website chat and email replies from then on; uploaded to the customer's Anthropic account like other notes) and takes the question off both follow-up lists, together with every other open answer to the same question.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Answer id from GET /api/live-chat/questions"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "content"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "content": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 5000
                  }
                }
              },
              "example": {
                "title": "Do you carry semolina flour?",
                "content": "Yes, we stock 25 kg bags of semolina flour for $36."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "knowledgeId": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "knowledgeId": "k_789"
                }
              }
            }
          },
          "400": {
            "description": "Missing or too long title or answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Question not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This question was already handled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/live-chat/widget/identity-secret": {
      "get": {
        "tags": [
          "Live chat"
        ],
        "summary": "Read the identity secret",
        "description": "The chat's identity secret, used on the customer's own server to sign signed-in visitors' emails (`hex(HMAC-SHA256(secret, lowercase(trim(email))))`). `secret` is null when none was made. Never cached.",
        "responses": {
          "200": {
            "description": "Secret",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secret": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "rcs_2b7…"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Live chat"
        ],
        "summary": "Make or replace the identity secret",
        "description": "Makes a new secret. Replacing it makes signatures made with the old one stop working straight away (visitors are then treated as not signed in).",
        "responses": {
          "201": {
            "description": "New secret",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "secret"
                  ],
                  "properties": {
                    "secret": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Live chat"
        ],
        "summary": "Remove the identity secret",
        "description": "No visitor can be signed in to the chat any more.",
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "tags": [
          "Public MCP"
        ],
        "summary": "ReachClaw's own MCP server (read-only product facts)",
        "description": "Streamable HTTP MCP endpoint (stateless JSON-RPC 2.0) that Anthropic's MCP connector calls while the chat on reachclaw.com answers a visitor. Tools: get_plans, get_plan_limits, check_feature, get_service_status, get_site_chat_stats. Everything is public or aggregate (plans, prices, caps from PLAN_LIMITS, which plan includes what, queue/integration status, the reachclaw.com chat's own counts); never another customer's rows, never visitor text, never a write. Auth: `Authorization: Bearer <MCP_PUBLIC_TOKEN>`; with no token configured the endpoint answers 503. Rate limit 240 requests per minute per IP. Accepts one JSON-RPC message or a batch (max 20, body max 64 KB). Notifications only → 202 with no body. No server-initiated SSE stream (GET → 405) and no session ids (DELETE → 405).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "jsonrpc",
                      "method"
                    ],
                    "properties": {
                      "jsonrpc": {
                        "type": "string",
                        "enum": [
                          "2.0"
                        ]
                      },
                      "id": {
                        "oneOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "integer"
                          }
                        ],
                        "description": "Omit for notifications (no response)."
                      },
                      "method": {
                        "type": "string",
                        "enum": [
                          "initialize",
                          "ping",
                          "tools/list",
                          "tools/call",
                          "notifications/initialized"
                        ]
                      },
                      "params": {
                        "type": "object",
                        "description": "tools/call: { name, arguments }. initialize: { protocolVersion, capabilities, clientInfo }."
                      }
                    },
                    "example": {
                      "jsonrpc": "2.0",
                      "id": 1,
                      "method": "tools/call",
                      "params": {
                        "name": "get_plan_limits",
                        "arguments": {
                          "plan": "growth"
                        }
                      }
                    }
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "jsonrpc",
                        "method"
                      ],
                      "properties": {
                        "jsonrpc": {
                          "type": "string",
                          "enum": [
                            "2.0"
                          ]
                        },
                        "id": {
                          "oneOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "integer"
                            }
                          ],
                          "description": "Omit for notifications (no response)."
                        },
                        "method": {
                          "type": "string",
                          "enum": [
                            "initialize",
                            "ping",
                            "tools/list",
                            "tools/call",
                            "notifications/initialized"
                          ]
                        },
                        "params": {
                          "type": "object",
                          "description": "tools/call: { name, arguments }. initialize: { protocolVersion, capabilities, clientInfo }."
                        }
                      },
                      "example": {
                        "jsonrpc": "2.0",
                        "id": 1,
                        "method": "tools/call",
                        "params": {
                          "name": "get_plan_limits",
                          "arguments": {
                            "plan": "growth"
                          }
                        }
                      }
                    },
                    "maxItems": 20
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response (or array for a batch)",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "jsonrpc": {
                          "type": "string",
                          "enum": [
                            "2.0"
                          ]
                        },
                        "id": {
                          "oneOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "integer"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "result": {
                          "type": "object",
                          "description": "tools/list → { tools: [{ name, description, inputSchema }] }; tools/call → { content: [{ type: 'text', text }], isError }"
                        },
                        "error": {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "integer"
                            },
                            "message": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "jsonrpc": {
                            "type": "string",
                            "enum": [
                              "2.0"
                            ]
                          },
                          "id": {
                            "oneOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "integer"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "result": {
                            "type": "object",
                            "description": "tools/list → { tools: [{ name, description, inputSchema }] }; tools/call → { content: [{ type: 'text', text }], isError }"
                          },
                          "error": {
                            "type": "object",
                            "properties": {
                              "code": {
                                "type": "integer"
                              },
                              "message": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "result": {
                    "content": [
                      {
                        "type": "text",
                        "text": "{\n  \"plan\": \"growth\",\n  \"connectedInboxes\": 3,\n  \"aiHandledEmailsPerMonth\": 2000,\n  \"outboundEmailsPerMonth\": 500,\n  \"websiteChatAnswersPerMonth\": 2000,\n  \"aiWrittenTemplates\": \"unlimited\"\n}"
                      }
                    ],
                    "isError": false
                  }
                }
              }
            }
          },
          "202": {
            "description": "Notification(s) only, nothing to return"
          },
          "400": {
            "description": "Parse error or invalid JSON-RPC envelope",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "type": "string",
                      "enum": [
                        "2.0"
                      ]
                    },
                    "id": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "integer"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "result": {
                      "type": "object",
                      "description": "tools/list → { tools: [{ name, description, inputSchema }] }; tools/call → { content: [{ type: 'text', text }], isError }"
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or wrong bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body over 64 KB",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "type": "string",
                      "enum": [
                        "2.0"
                      ]
                    },
                    "id": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "integer"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "result": {
                      "type": "object",
                      "description": "tools/list → { tools: [{ name, description, inputSchema }] }; tools/call → { content: [{ type: 'text', text }], isError }"
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "MCP_PUBLIC_TOKEN not configured on this server",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Public MCP"
        ],
        "summary": "Not supported (no server-initiated stream)",
        "responses": {
          "405": {
            "description": "Use POST"
          }
        }
      },
      "delete": {
        "tags": [
          "Public MCP"
        ],
        "summary": "Not supported (stateless, no sessions)",
        "responses": {
          "405": {
            "description": "Use POST"
          }
        }
      }
    }
  }
}
