{
  "openapi": "3.1.0",
  "info": {
    "title": "FloorSync API",
    "version": "1.0.0",
    "description": "For founders and their AI agents: any Stripe business with Meta or GA4 ads, on any server (towers.app floors too). Try it now with no key: POST /api/v1/demo/events/stripe. Then forward each verified Stripe event from your webhook handler; FloorSync sends sales and renewals to your Meta pixel (Conversions API) and GA4 (Measurement Protocol), and answers a revenue-vs-ad-spend ledger. A 'floor' is one business: one Stripe account, one Meta pixel, one GA4 property, one key. $10 a month per business (7 days free), started by the owner at https://floorsync.towers.app/app/billing. Every POST also sends x-amz-content-sha256: the hex SHA-256 of the body."
  },
  "servers": [{ "url": "https://floorsync.towers.app" }],
  "security": [{ "floorKey": [] }],
  "components": {
    "securitySchemes": {
      "floorKey": { "type": "apiKey", "in": "header", "name": "X-Floor-Key", "description": "The key (fs_…), one per business, from the owner's FloorSync page." }
    },
    "parameters": {
      "BodyHash": { "name": "x-amz-content-sha256", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Hex SHA-256 of the exact request body." }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" }, "message": { "type": "string" }, "owner_url": { "type": "string" } },
        "required": ["error", "message"]
      },
      "StripeEvent": {
        "type": "object",
        "description": "A Stripe event exactly as your stripe-events route received it.",
        "properties": {
          "id": { "type": "string", "example": "evt_test_fs_001" },
          "type": { "type": "string", "example": "checkout.session.completed" },
          "created": { "type": "integer" },
          "livemode": { "type": "boolean" },
          "data": { "type": "object", "properties": { "object": { "type": "object" } }, "required": ["object"] }
        },
        "required": ["id", "type", "data"]
      },
      "Attribution": {
        "type": "object",
        "properties": {
          "checkout_session_id": { "type": "string" },
          "customer": { "type": "string" },
          "fbp": { "type": "string" },
          "fbc": { "type": "string" },
          "fbclid": { "type": "string" },
          "client_id": { "type": "string", "description": "GA client id (123.456) or the _ga cookie" },
          "session_id": { "type": "string" },
          "ip": { "type": "string" },
          "user_agent": { "type": "string" },
          "page_url": { "type": "string" }
        }
      },
      "Destination": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["delivered", "failed", "skipped", "validated", "built", "not_sent"] },
          "payload": { "type": "object" },
          "response": {},
          "validation_messages": { "type": "array", "items": { "type": "object" } },
          "notes": { "type": "array", "items": { "type": "string" } },
          "error": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "will_retry": { "type": "boolean" } } }
        }
      },
      "Ledger": {
        "type": "object",
        "properties": {
          "mode": { "type": "string", "enum": ["live", "dry_run"] },
          "days": { "type": "integer" },
          "from": { "type": "string" },
          "to": { "type": "string" },
          "currency": { "type": "string" },
          "revenue": { "type": "number" },
          "counts": {
            "type": "object",
            "properties": {
              "new_paid": { "type": "integer" }, "trials": { "type": "integer" }, "renewals": { "type": "integer" },
              "refunds": { "type": "integer" }, "failed_payments": { "type": "integer" }, "cancellations": { "type": "integer" }
            }
          },
          "conversions": { "type": "object", "additionalProperties": { "type": "object", "additionalProperties": { "type": "integer" } } },
          "spend": { "type": ["number", "null"] },
          "spend_source": { "type": ["string", "null"] },
          "revenue_per_spend": { "type": ["number", "null"] },
          "plan_active": { "type": "boolean" },
          "health": { "type": "array", "items": { "type": "string" } },
          "note": { "type": "string" }
        }
      }
    }
  },
  "paths": {
    "/api/capabilities": {
      "get": { "summary": "What FloorSync does, as JSON", "security": [], "responses": { "200": { "description": "Capabilities" } } }
    },
    "/api/v1/demo/events/stripe": {
      "post": {
        "summary": "Try it now, no key: what Meta and GA4 would get for one Stripe event",
        "description": "No key and no account. Returns the same Meta and GA4 payloads as a keyed dry run, the event's ledger line, and message_for_owner: a short note for the founder you work for (what's needed before live sends, the price, the sign-up link). Nothing is sent or kept. 5 calls an hour per address.",
        "security": [],
        "parameters": [{ "$ref": "#/components/parameters/BodyHash" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/StripeEvent" },
                  { "type": "object", "properties": { "event": { "$ref": "#/components/schemas/StripeEvent" }, "attribution": { "$ref": "#/components/schemas/Attribution" } }, "required": ["event"] }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The payloads, built and not sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "demo": { "type": "boolean" },
                    "dry_run": { "type": "boolean" },
                    "stored": { "type": "boolean" },
                    "event": { "type": "object" },
                    "meta": { "$ref": "#/components/schemas/Destination" },
                    "ga4": { "$ref": "#/components/schemas/Destination" },
                    "ledger_line": { "type": "object" },
                    "message_for_owner": { "type": "string", "description": "Show this to the owner as it is." },
                    "next_step": { "type": "object", "properties": { "what": { "type": "string" }, "signup_url": { "type": "string" }, "price": { "type": "object" }, "before_live": { "type": "array", "items": { "type": "string" } }, "docs": { "type": "string" } } },
                    "calls_left_this_hour": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "description": "Not a Stripe event", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Past 5 calls an hour from this address (retry_after, in seconds)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/events/stripe": {
      "post": {
        "summary": "Forward one Stripe event",
        "parameters": [
          { "$ref": "#/components/parameters/BodyHash" },
          { "name": "dry_run", "in": "query", "schema": { "type": "string", "enum": ["1"] }, "description": "Build and validate the payloads without sending" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/StripeEvent" },
                  { "type": "object", "properties": { "event": { "$ref": "#/components/schemas/StripeEvent" }, "attribution": { "$ref": "#/components/schemas/Attribution" }, "dry_run": { "type": "boolean" } }, "required": ["event"] }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Handled (or a duplicate)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "duplicate": { "type": "boolean" },
                    "dry_run": { "type": "boolean" },
                    "event": { "type": "object" },
                    "meta": { "$ref": "#/components/schemas/Destination" },
                    "ga4": { "$ref": "#/components/schemas/Destination" },
                    "ledger": { "$ref": "#/components/schemas/Ledger" }
                  }
                }
              }
            }
          },
          "400": { "description": "Not a Stripe event", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "No or bad key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "No active plan: nothing sent; tell the owner (owner_url)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/attribution": {
      "post": {
        "summary": "Keep the buyer's browser ids for a checkout session",
        "parameters": [{ "$ref": "#/components/parameters/BodyHash" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Attribution" } } } },
        "responses": { "200": { "description": "Kept" }, "400": { "description": "Nothing to keep" } }
      }
    },
    "/api/v1/ledger": {
      "get": {
        "summary": "Revenue vs ad spend",
        "parameters": [
          { "name": "days", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 90, "default": 7 } },
          { "name": "mode", "in": "query", "schema": { "type": "string", "enum": ["live", "dry_run"], "default": "live" } }
        ],
        "responses": { "200": { "description": "The ledger", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Ledger" } } } } }
      }
    },
    "/api/v1/sync-log": {
      "get": {
        "summary": "The latest sends and their answers",
        "parameters": [{ "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } }],
        "responses": { "200": { "description": "{items: [...]}" } }
      }
    },
    "/api/v1/spend": {
      "post": {
        "summary": "Record a day's ad spend",
        "parameters": [{ "$ref": "#/components/parameters/BodyHash" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "amount": { "type": "number" }, "currency": { "type": "string" } }, "required": ["date", "amount"] } } }
        },
        "responses": { "200": { "description": "Kept" } }
      }
    },
    "/api/v1/status": {
      "get": { "summary": "Plan and setup status", "responses": { "200": { "description": "Status" } } }
    },
    "/api/mcp": {
      "post": {
        "summary": "MCP endpoint (JSON-RPC 2.0): initialize, tools/list, tools/call",
        "parameters": [{ "$ref": "#/components/parameters/BodyHash" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object" } } } },
        "responses": { "200": { "description": "JSON-RPC answer" }, "202": { "description": "Notification accepted" } }
      }
    }
  }
}
