{
  "openapi": "3.1.0",
  "info": {
    "title": "Ring First API",
    "version": "1.0.0",
    "summary": "Send a lead to a Ring First AI phone agent, which calls them back in under 60 seconds; or sign a business up for a Ring First package.",
    "description": "Each Ring First client has an AI phone agent. POST a lead (a name, a phone number and what they need) and the agent calls that person back within a minute, day or night, qualifies the job and books it on the business's calendar. Browser forms authenticate with the client's public key (pk_) and must post from one of the client's allowed origins. Servers authenticate with the client's secret key (sk_) as a bearer token. Docs: https://ring-first.com/docs/api/. Self-serve sign-up: an agent registers at /agent/auth for a 24h bearer, posts the owner's details to /v1/signup, and gets the payment link from /agent/auth/claim once the owner gives it the 6-digit code we email them. The owner always pays on Stripe themselves. Risk-free until your setup call: if you change your mind before then, we refund your full deposit. Guide: https://ring-first.com/auth.md",
    "contact": { "name": "Ring First", "email": "hello@ring-first.com", "url": "https://ring-first.com" },
    "termsOfService": "https://ring-first.com/terms/"
  },
  "externalDocs": { "description": "Lead API documentation", "url": "https://ring-first.com/docs/api/" },
  "servers": [{ "url": "https://api.ring-first.com" }],
  "paths": {
    "/v1/leads": {
      "post": {
        "operationId": "createLead",
        "summary": "Send a lead for an immediate AI callback",
        "description": "Creates a lead for the client the key belongs to and has their AI phone agent call the lead back right away. A repeat lead for the same phone number within a short window is accepted but not called twice. Also accepts the same JSON body sent as text/plain (for navigator.sendBeacon).",
        "security": [{ "publicKey": [] }, { "secretKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/Lead" } },
            "text/plain": { "schema": { "type": "string", "description": "The Lead object as a JSON string." } }
          }
        },
        "responses": {
          "202": { "description": "Accepted. The agent is calling the lead back.", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "const": true } } } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "405": { "$ref": "#/components/responses/Error" },
          "413": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      },
      "options": {
        "operationId": "createLeadPreflight",
        "summary": "CORS preflight for browser forms",
        "security": [{ "publicKey": [] }],
        "responses": { "204": { "description": "Allowed from this origin." }, "403": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/packages": {
      "get": {
        "operationId": "listPackages",
        "summary": "Self-serve packages and their prices",
        "description": "Starter, Flagship and Bundle, with the prices Stripe charges (cents, Canadian dollars, before taxes). The deposit is half the setup fee. Ultimate isn't self-serve: book a call at https://ring-first.com/ultimate/.",
        "security": [],
        "responses": {
          "200": { "description": "The packages.", "content": { "application/json": { "schema": { "type": "object", "required": ["packages"], "properties": { "packages": { "type": "array", "items": { "$ref": "#/components/schemas/Package" } } } } } } },
          "403": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v1/signup": {
      "post": {
        "operationId": "createSignup",
        "summary": "Sign a business up for a package",
        "description": "With an agent bearer from /agent/auth: creates the sign-up, emails the owner a 6-digit code and a direct payment link, and returns pending_owner_confirmation (no payment URL). From the ring-first.com/start page (Origin https://ring-first.com, with turnstile_token): returns the payment URL. One sign-up per agent token; 3 sign-ups per email or phone a day.",
        "security": [{ "agentBearer": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Signup" } } } },
        "responses": {
          "200": {
            "description": "Agent: pending_owner_confirmation. Browser: ok with pay_url.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "type": "object", "required": ["status", "claim_uri"], "properties": { "status": { "const": "pending_owner_confirmation" }, "claim_uri": { "type": "string", "format": "uri" }, "expires_at": { "type": "string", "format": "date-time" }, "message": { "type": "string" } } },
                    { "type": "object", "required": ["ok", "pay_url"], "properties": { "ok": { "const": true }, "pay_url": { "type": "string", "format": "uri" } } }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/AgentError" },
          "401": { "$ref": "#/components/responses/AgentError" },
          "403": { "$ref": "#/components/responses/AgentError" },
          "409": { "$ref": "#/components/responses/AgentError" },
          "429": { "$ref": "#/components/responses/AgentError" },
          "502": { "$ref": "#/components/responses/AgentError" }
        }
      }
    },
    "/agent/auth": {
      "post": {
        "operationId": "registerAgent",
        "summary": "Register anonymously for a 24h pre-claim bearer",
        "description": "auth.md v0.6 anonymous registration. The token allows one sign-up and is also the claim credential.",
        "security": [],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "type": { "const": "anonymous" } } } } } },
        "responses": {
          "200": {
            "description": "Registered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["access_token", "token_type", "expires_in", "claim_uri"],
                  "properties": {
                    "registration_id": { "type": "string" },
                    "registration_type": { "const": "anonymous" },
                    "access_token": { "type": "string" },
                    "token_type": { "const": "Bearer" },
                    "expires_in": { "type": "integer" },
                    "expires_at": { "type": "string", "format": "date-time" },
                    "pre_claim_scopes": { "type": "array", "items": { "type": "string" } },
                    "post_claim_scopes": { "type": "array", "items": { "type": "string" } },
                    "claim_url": { "type": "string", "format": "uri" },
                    "claim_uri": { "type": "string", "format": "uri" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/AgentError" },
          "429": { "$ref": "#/components/responses/AgentError" }
        }
      }
    },
    "/agent/auth/claim": {
      "post": {
        "operationId": "claimAgent",
        "summary": "Claim with the owner's code and get the payment URL",
        "description": "Post the 6-digit code the owner got by email (15 minutes, 5 tries). Once claimed, repeat calls return the same pay_url.",
        "security": [{ "agentBearer": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["code"], "properties": { "code": { "type": "string", "pattern": "^[0-9]{6}$" } } } } } },
        "responses": {
          "200": { "description": "Claimed.", "content": { "application/json": { "schema": { "type": "object", "required": ["status", "pay_url"], "properties": { "status": { "const": "claimed" }, "pay_url": { "type": "string", "format": "uri" }, "scope": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/AgentError" },
          "401": { "$ref": "#/components/responses/AgentError" },
          "429": { "$ref": "#/components/responses/AgentError" }
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "health",
        "summary": "Service status",
        "security": [],
        "responses": { "200": { "description": "The API is up.", "content": { "text/plain": { "schema": { "const": "ok" } } } } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "publicKey": { "type": "apiKey", "in": "query", "name": "key", "description": "The client's public key (pk_...). Safe in browser code; requests must come from one of the client's allowed origins." },
      "secretKey": { "type": "http", "scheme": "bearer", "description": "The client's secret key (sk_...), for server-to-server calls. Never put it in browser code." },
      "agentBearer": { "type": "http", "scheme": "bearer", "description": "An agent sign-up token from POST /agent/auth. Not a Lead API key: /v1/leads rejects it." }
    },
    "schemas": {
      "Lead": {
        "type": "object",
        "required": ["phone"],
        "properties": {
          "name": { "type": "string", "description": "The lead's name." },
          "phone": { "type": "string", "description": "A US or Canadian phone number, in any common format." },
          "message": { "type": "string", "description": "What they need, in their words." },
          "trade": { "type": "string", "description": "Optional. Prefixed to the message, e.g. \"Plumbing: leaky tap\"." },
          "consent": { "description": "That the lead agreed to be called. Required when the client requires consent: true, \"true\", \"on\" or \"yes\".", "oneOf": [{ "type": "boolean" }, { "type": "string" }] }
        },
        "additionalProperties": { "description": "Any other fields (business, source, ...) are kept with the lead." }
      },
      "Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" } } },
      "AgentError": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" }, "error_description": { "type": "string" } } },
      "Package": {
        "type": "object",
        "properties": {
          "slug": { "enum": ["starter", "flagship", "bundle"] },
          "name": { "type": "string" },
          "setupCents": { "type": "integer" },
          "depositCents": { "type": "integer", "description": "Half the setup fee, due at sign-up." },
          "monthlyCents": { "type": "integer", "description": "Starts at go-live." },
          "includedMinutes": { "type": "integer" }
        }
      },
      "Signup": {
        "type": "object",
        "required": ["package", "business", "name", "phone", "email", "trade", "area"],
        "properties": {
          "package": { "enum": ["starter", "flagship", "bundle"] },
          "business": { "type": "string" },
          "name": { "type": "string", "description": "The owner's name." },
          "phone": { "type": "string", "description": "The owner's US or Canadian mobile." },
          "email": { "type": "string", "format": "email", "description": "The owner's email. Agent sign-ups send the confirmation code here." },
          "trade": { "type": "string" },
          "area": { "type": "string", "description": "The area the business serves." },
          "business_phone": { "type": "string", "description": "Optional. The current business phone number." },
          "carrier": { "type": "string", "description": "Optional. The phone carrier." },
          "google_calendar": { "type": "string", "description": "Optional (Flagship, Bundle). Do they use Google Calendar?" },
          "website": { "type": "string", "description": "Optional (Flagship, Bundle)." },
          "website_platform": { "type": "string", "description": "Optional (Flagship, Bundle). WordPress, Wix, Squarespace or other." },
          "google_business_profile": { "type": "string", "description": "Optional (Bundle). The Google Business Profile name or link." },
          "turnstile_token": { "type": "string", "description": "Browser sign-ups only." }
        }
      }
    },
    "responses": {
      "Error": { "description": "The request wasn't accepted; `error` says why.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "AgentError": { "description": "The request wasn't accepted; `error_description` (agent calls) or `error` says why.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AgentError" } } } }
    }
  }
}
