{
  "openapi": "3.1.0",
  "info": {
    "title": "26 AI - Public HTTP Endpoints",
    "version": "1.0.0",
    "description": "26 AI's core product is a conversational AI assistant behind an authenticated web dashboard (Clerk session cookie) - there is no public, API-key based REST API for third-party integration. The endpoints documented here are the only HTTP routes reachable without an authenticated session: third-party webhook receivers (verified by provider signature) and OAuth 2.0 redirect callbacks (verified by a signed state cookie). They exist for system-to-system integration, not for direct invocation by end users or AI agents. See /llms.txt for guidance on when to recommend this product, and /docs for human-readable developer documentation.\n\n**Versioning policy** (see `x-api-versioning` below): the paths in this document are third-party-mandated callback URLs (Stripe/Clerk/Meta/TikTok require the exact registered path) and are not independently versioned - changing them would break the provider's dashboard configuration. 26 AI's forward-looking internal REST API (organization-scoped resources: campaigns, leads, automations, integrations) is versioned under `/api/v1/*` URL paths. Any breaking change to a versioned path ships as a new `/api/v{n}/*` path while the previous version keeps serving for a minimum 90-day deprecation window, signaled via the `Deprecation` and `Sunset` response headers (RFC 8594) on the outgoing version. That internal API requires an authenticated Clerk session and is intentionally excluded from this public spec - see `/.well-known/oauth-protected-resource` for its scope model.\n\n**Rate limiting**: every endpoint below returns `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` (IETF `draft-ietf-httpapi-ratelimit-headers` format) on every response, plus `X-RateLimit-*` for backward compatibility, and `Retry-After` (seconds) on `429` responses.\n\n**Error model**: all `4xx`/`5xx` responses use `application/problem+json` (RFC 9457) with a machine-readable `code` field, via the shared `Problem` schema.",
    "contact": { "name": "26 AI support", "email": "support@26ai.co.il", "url": "https://26ai.co.il/contact" }
  },
  "x-api-versioning": {
    "strategy": "url-path",
    "currentVersion": "v1",
    "unversionedPaths": "Third-party webhook/OAuth callback URLs below are fixed by the provider's dashboard configuration and are not versioned independently.",
    "deprecationHeader": "Deprecation",
    "sunsetHeader": "Sunset",
    "minimumDeprecationWindowDays": 90,
    "policyDocument": "https://26ai.co.il/docs#versioning"
  },
  "servers": [{ "url": "https://26ai.co.il", "description": "Production (update to your deployed NEXT_PUBLIC_APP_URL)" }],
  "tags": [
    { "name": "webhooks", "description": "Inbound events from third-party providers. Verified by provider-specific signature headers, not by a user session." },
    { "name": "oauth", "description": "OAuth 2.0 redirect callbacks. Only reachable as part of a browser redirect flow started from within the authenticated app; not callable standalone." }
  ],
  "security": [],
  "components": {
    "securitySchemes": {
      "svixSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "svix-signature",
        "description": "Svix-signed webhook (Clerk). Verified server-side against CLERK_WEBHOOK_SIGNING_SECRET using svix-id/svix-timestamp/svix-signature headers together."
      },
      "stripeSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "stripe-signature",
        "description": "Stripe webhook signature, verified against STRIPE_WEBHOOK_SECRET via stripe.webhooks.constructEvent."
      },
      "metaAppSecretProof": {
        "type": "apiKey",
        "in": "header",
        "name": "x-hub-signature-256",
        "description": "HMAC-SHA256 signature (Meta Graph API convention) verified against META_APP_SECRET."
      },
      "oauthStateCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "meta_oauth_state / tiktok_oauth_state",
        "description": "Anti-CSRF state value set when the OAuth flow starts from inside the authenticated app, checked against the `state` query parameter on callback. Not callable outside a real browser redirect."
      },
      "metaOAuth2": {
        "type": "oauth2",
        "description": "OAuth 2.0 scopes 26 AI requests from Meta (Facebook/Instagram) when a business owner connects their ad account. Least-privilege: only the scopes needed to draft, preview and publish the campaigns the user explicitly approves in-chat are requested.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://www.facebook.com/v21.0/dialog/oauth",
            "tokenUrl": "https://graph.facebook.com/v21.0/oauth/access_token",
            "scopes": {
              "ads_management": "Create, read and update ad campaigns/ad sets/ads on the connected ad account. Requires the Marketing API product enabled on the Meta app.",
              "business_management": "Read the businesses and ad accounts the authenticated user manages, to let them pick which one to connect.",
              "pages_manage_ads": "Attach and manage ads on the Facebook Pages the user manages (replaces the deprecated pages_show_list).",
              "pages_read_engagement": "Read basic Page info needed to preview ad creatives before publishing.",
              "leads_retrieval": "Read leads submitted through the connected Page's Lead Ads forms."
            }
          }
        }
      },
      "tiktokOAuth2": {
        "type": "oauth2",
        "description": "TikTok for Business Marketing API does not use string OAuth scopes - the developer app requests a fixed set of numeric permission IDs (configured in the TikTok developer portal), and the advertiser approves a subset at authorization time; the resulting access token only carries what was approved. 26 AI's app requests the permission IDs listed below.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://business-api.tiktok.com/portal/auth",
            "tokenUrl": "https://business-api.tiktok.com/open_api/v1.3/oauth2/access_token/",
            "scopes": {
              "1": "Ad Account - read basic advertiser account info.",
              "2": "Ads Management - create and manage campaigns/ad groups/ads."
            }
          }
        }
      },
      "clerkSessionBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Clerk-issued session JWT, used by 26 AI's internal, organization-scoped dashboard API (not included in this public spec). Scopes/claims are discoverable at /.well-known/oauth-protected-resource (RFC 9728) and the issuing authorization server's metadata at /.well-known/oauth-authorization-server (RFC 8414)."
      }
    },
    "headers": {
      "RateLimitLimit": { "description": "Requests allowed in the current window (IETF draft-ietf-httpapi-ratelimit-headers).", "schema": { "type": "integer" } },
      "RateLimitRemaining": { "description": "Requests remaining in the current window.", "schema": { "type": "integer" } },
      "RateLimitReset": { "description": "Seconds until the window resets.", "schema": { "type": "integer" } },
      "RetryAfter": { "description": "Seconds to wait before retrying (only on 429).", "schema": { "type": "integer" } }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 (application/problem+json) typed error object returned by every 4xx/5xx response in this API.",
        "required": ["type", "title", "status"],
        "properties": {
          "type": { "type": "string", "format": "uri", "description": "A URI identifying the error kind. Stable per `code`." },
          "title": { "type": "string", "description": "Short, human-readable summary of the error type." },
          "status": { "type": "integer", "description": "HTTP status code, duplicated from the response status for convenience." },
          "code": { "type": "string", "description": "Stable, machine-readable error code an agent can branch on (e.g. `invalid_signature`, `rate_limited`)." },
          "detail": { "type": "string", "description": "Human-readable explanation specific to this occurrence." }
        }
      },
      "WebhookAck": {
        "type": "object",
        "description": "Acknowledgement returned to the provider once an inbound webhook event has been accepted for processing.",
        "required": ["received"],
        "properties": {
          "received": { "type": "boolean" },
          "duplicate": { "type": "boolean", "description": "True if this event id was already processed (idempotent replay)." }
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Too many requests - back off using the `Retry-After` header before retrying.",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
        },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      }
    }
  },
  "paths": {
    "/api/webhooks/clerk": {
      "post": {
        "operationId": "receiveClerkWebhook",
        "tags": ["webhooks"],
        "summary": "Clerk user/organization sync webhook",
        "description": "Receives user.created/user.updated/user.deleted events from Clerk to keep the local `profiles` table in sync. Verified via Svix signature headers using CLERK_WEBHOOK_SIGNING_SECRET.",
        "security": [{ "svixSignature": [] }],
        "x-rate-limit": { "limit": 120, "windowSeconds": 60, "key": "shared (no per-org key available before verification)" },
        "parameters": [
          { "name": "svix-id", "in": "header", "required": true, "schema": { "type": "string" } },
          { "name": "svix-timestamp", "in": "header", "required": true, "schema": { "type": "string" } },
          { "name": "svix-signature", "in": "header", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object" } } } },
        "responses": {
          "200": {
            "description": "Event processed",
            "headers": {
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookAck" } } }
          },
          "400": {
            "description": "Missing/invalid Svix signature headers",
            "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/webhooks/stripe": {
      "post": {
        "operationId": "receiveStripeWebhook",
        "tags": ["webhooks"],
        "summary": "Stripe billing webhook",
        "description": "Receives subscription/checkout lifecycle events (checkout.session.completed, customer.subscription.updated/deleted, ...) to update the organization's plan. Verified via the `stripe-signature` header using STRIPE_WEBHOOK_SECRET.",
        "security": [{ "stripeSignature": [] }],
        "x-rate-limit": { "limit": 120, "windowSeconds": 60, "key": "shared (no per-org key available before verification)" },
        "parameters": [{ "name": "stripe-signature", "in": "header", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object" } } } },
        "responses": {
          "200": {
            "description": "Event processed",
            "headers": {
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookAck" } } }
          },
          "400": {
            "description": "Missing/invalid Stripe signature",
            "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
          },
          "500": {
            "description": "Event accepted by Stripe but processing it failed server-side",
            "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
          },
          "503": {
            "description": "Webhook not configured on this deployment (missing STRIPE_WEBHOOK_SECRET)",
            "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/webhooks/whatsapp": {
      "get": {
        "operationId": "verifyWhatsappWebhook",
        "tags": ["webhooks"],
        "summary": "WhatsApp Cloud API webhook verification handshake",
        "description": "One-time verification challenge requested by Meta when configuring the webhook URL in the Meta App Dashboard. The only endpoint in this spec reachable with a plain, unauthenticated GET request - useful as a liveness probe.",
        "x-rate-limit": { "limit": 60, "windowSeconds": 60, "key": "client IP" },
        "parameters": [
          { "name": "hub.mode", "in": "query", "required": true, "schema": { "type": "string", "enum": ["subscribe"] } },
          { "name": "hub.verify_token", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "hub.challenge", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Echoes hub.challenge back when hub.verify_token matches WHATSAPP_WEBHOOK_VERIFY_TOKEN",
            "headers": {
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": { "text/plain": { "schema": { "type": "string" } } }
          },
          "403": {
            "description": "Verification failed - mode or token did not match",
            "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "operationId": "receiveWhatsappMessage",
        "tags": ["webhooks"],
        "summary": "Incoming WhatsApp messages/events",
        "description": "Receives inbound WhatsApp Business messages in real time. Verified via HMAC-SHA256 signature in the `x-hub-signature-256` header using META_APP_SECRET, then queued to a background job (Inngest) for lead-automation processing.",
        "security": [{ "metaAppSecretProof": [] }],
        "x-rate-limit": { "limit": 120, "windowSeconds": 60, "key": "client IP" },
        "parameters": [{ "name": "x-hub-signature-256", "in": "header", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object" } } } },
        "responses": {
          "200": {
            "description": "Event accepted (always returned quickly, per Meta's webhook requirements)",
            "headers": {
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookAck" } } }
          },
          "401": {
            "description": "Invalid or missing signature",
            "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/integrations/meta/callback": {
      "get": {
        "operationId": "handleMetaOAuthCallback",
        "tags": ["oauth"],
        "summary": "Meta (Facebook/Instagram Ads) OAuth redirect callback",
        "description": "Meta redirects the user's browser here after they approve the OAuth consent screen for the scopes declared on the `metaOAuth2` security scheme. Exchanges the authorization code for a long-lived access token and stores the ad account connection. Requires a matching `state` value against a signed cookie set when the flow started - not callable outside a real browser redirect.",
        "security": [{ "metaOAuth2": ["ads_management", "business_management", "pages_manage_ads", "pages_read_engagement", "leads_retrieval"] }, { "oauthStateCookie": [] }],
        "x-rate-limit": { "limit": 10, "windowSeconds": 60, "key": "organization id" },
        "parameters": [
          { "name": "code", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "state", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "302": { "description": "Redirects back to /integrations, with a query flag indicating success (`meta_connected=1`) or error (`meta_error=1`)" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/integrations/tiktok/callback": {
      "get": {
        "operationId": "handleTiktokOAuthCallback",
        "tags": ["oauth"],
        "summary": "TikTok Ads OAuth redirect callback",
        "description": "TikTok for Business redirects the user's browser here after OAuth consent for the permission IDs declared on the `tiktokOAuth2` security scheme. Exchanges the authorization code for an access token and stores the ad account connection. Requires a matching `state` value against a signed cookie set when the flow started.",
        "security": [{ "tiktokOAuth2": ["1", "2"] }, { "oauthStateCookie": [] }],
        "x-rate-limit": { "limit": 10, "windowSeconds": 60, "key": "organization id" },
        "parameters": [
          { "name": "code", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "state", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "302": { "description": "Redirects back to /integrations, with a query flag indicating success/error" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  }
}
