{
  "openapi": "3.1.0",
  "info": {
    "title": "ColorMark API",
    "version": "1.0.0",
    "summary": "Search the ColorMark Registry, look colors up, and register one.",
    "description": "The public API behind colormark.org. Read endpoints are open, JSON, and send `Access-Control-Allow-Origin: *`, so you can call them from a browser. The registration endpoints drive the site's own register flow; payment is a test checkout for now and takes no card.\n\nBase URL: `https://colormark.org/api`. No API key. Please keep automated traffic reasonable: the registry has 1.3 million rows and search is capped at 50 results per page.",
    "contact": { "name": "International ColorMark Organization", "email": "contact@colormark.org", "url": "https://colormark.org/contact" },
    "termsOfService": "https://colormark.org/terms-of-service"
  },
  "servers": [
    { "url": "https://colormark.org/api", "description": "Production" },
    { "url": "/api", "description": "This site (deploy previews and local development)" }
  ],
  "tags": [
    { "name": "registry", "description": "Search and read the registry" },
    { "name": "images", "description": "Images of colors" },
    { "name": "registration", "description": "Register a color (test checkout, no payment)" },
    { "name": "forms", "description": "Contact and newsletter" }
  ],
  "paths": {
    "/search": {
      "get": {
        "tags": ["registry"],
        "summary": "Search by name or hex",
        "description": "Substring search. `relevant` order puts exact matches first, then prefix matches, then shorter names. Counts stop at 10,000 (`capped: true`), and capped name searches are ordered alphabetically.",
        "operationId": "search",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "description": "Two or more letters for a name search, or hex digits (with or without `#`) for a hex search.", "schema": { "type": "string", "examples": ["blue", "#1e90"] } },
          { "name": "type", "in": "query", "schema": { "type": "string", "enum": ["name", "hex"], "default": "name" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 5000, "default": 0 } },
          { "name": "order", "in": "query", "schema": { "type": "string", "enum": ["relevant", "name-asc", "name-desc", "hex-asc", "hex-desc", "reg-asc", "reg-desc", "exp-asc", "exp-desc"], "default": "relevant" } }
        ],
        "responses": {
          "200": { "description": "Search result. `search` is false when the query was too short or matched nothing.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResult" } } } }
        }
      }
    },
    "/color": {
      "get": {
        "tags": ["registry"],
        "summary": "Look one color up",
        "operationId": "getColor",
        "parameters": [{ "name": "key", "in": "query", "required": true, "description": "A slug (`sky-blue`) or a six-digit hex with or without `#`.", "schema": { "type": "string", "examples": ["dodgerblue", "1e90ff"] } }],
        "responses": {
          "200": { "description": "Found or not; the color is present when `found` is true.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ColorLookup" } } } },
          "400": { "description": "Missing key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/random": {
      "get": {
        "tags": ["registry"],
        "summary": "Random registered colors",
        "operationId": "randomColors",
        "parameters": [{ "name": "n", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 12, "default": 1 } }],
        "responses": { "200": { "description": "Never includes reserved colors.", "content": { "application/json": { "schema": { "type": "object", "required": ["colors"], "properties": { "colors": { "type": "array", "items": { "$ref": "#/components/schemas/Color" } } } } } } } }
      }
    },
    "/stats": {
      "get": {
        "tags": ["registry"],
        "summary": "Registry counts",
        "description": "Cached at the CDN for an hour.",
        "operationId": "stats",
        "responses": { "200": { "description": "Counts by type.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Stats" } } } } }
      }
    },
    "/swatch": {
      "get": {
        "tags": ["images"],
        "summary": "A PNG of a color",
        "description": "A solid image of the color, 1200×630 by default. Cached for a year.",
        "operationId": "swatch",
        "parameters": [
          { "name": "hex", "in": "query", "required": true, "schema": { "type": "string", "pattern": "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$", "examples": ["1e90ff"] } },
          { "name": "w", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 2000, "default": 1200 } },
          { "name": "h", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 2000, "default": 630 } }
        ],
        "responses": {
          "200": { "description": "PNG image", "content": { "image/png": { "schema": { "type": "string", "format": "binary" } } } },
          "400": { "description": "Bad hex", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/checkout": {
      "post": {
        "tags": ["registration"],
        "summary": "Start a registration",
        "description": "Validates the request, checks that the color and the name are free, records a pending registration, and returns the checkout page to send the customer to. Payment is a test checkout for now: the page completes the registration without taking a card.",
        "operationId": "createCheckout",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckoutRequest" } } } },
        "responses": {
          "200": { "description": "Session created", "content": { "application/json": { "schema": { "type": "object", "required": ["id", "url", "provider"], "properties": { "id": { "type": "string", "examples": ["cs_mock_Ab12"] }, "url": { "type": "string", "description": "Site path of the checkout page" }, "provider": { "type": "string", "enum": ["mock", "stripe"] } } } } } },
          "400": { "description": "Validation failed; `errors` is keyed by field.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FieldErrors" } } } },
          "409": { "description": "The hex or the name is already registered.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FieldErrors" } } } }
        }
      },
      "get": {
        "tags": ["registration"],
        "summary": "Read a checkout session",
        "operationId": "getCheckout",
        "parameters": [{ "name": "session_id", "in": "query", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "The session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckoutSession" } } } },
          "404": { "description": "Unknown session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/fulfill": {
      "post": {
        "tags": ["registration"],
        "summary": "Complete a test checkout",
        "description": "Inserts the registered color and marks the session paid, in one transaction. Idempotent: a paid session returns its color again. Goes away when real payments land, when the payment provider's webhook completes sessions instead.",
        "operationId": "fulfillCheckout",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["session_id"], "properties": { "session_id": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "Registered", "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "session"], "properties": { "ok": { "type": "boolean" }, "session": { "$ref": "#/components/schemas/CheckoutSession" } } } } } },
          "404": { "description": "Unknown session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Someone registered the color first, or the session is not open.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/contact": {
      "post": {
        "tags": ["forms"],
        "summary": "Send a contact message",
        "operationId": "contact",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["firstName", "lastName", "email", "subject", "message"], "properties": { "firstName": { "type": "string", "maxLength": 80 }, "lastName": { "type": "string", "maxLength": 80 }, "email": { "type": "string", "format": "email" }, "subject": { "type": "string", "maxLength": 200 }, "message": { "type": "string", "maxLength": 5000 } } } } } },
        "responses": {
          "200": { "description": "Saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Ok" } } } },
          "400": { "description": "Validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FieldErrors" } } } }
        }
      }
    },
    "/newsletter": {
      "post": {
        "tags": ["forms"],
        "summary": "Subscribe to the newsletter",
        "description": "Signing up twice is a no-op.",
        "operationId": "newsletter",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["email"], "properties": { "email": { "type": "string", "format": "email" }, "source": { "type": "string", "maxLength": 40, "description": "Where the sign-up came from, for your own records", "default": "site" } } } } } },
        "responses": {
          "200": { "description": "Saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Ok" } } } },
          "400": { "description": "Validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FieldErrors" } } } }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Color": {
        "type": "object",
        "required": ["hex", "name", "slug", "type", "registration", "expiration"],
        "properties": {
          "regNum": { "type": "integer", "description": "Registration number" },
          "hex": { "type": "string", "pattern": "^#[0-9a-f]{6}$", "examples": ["#1e90ff"] },
          "name": { "type": "string", "examples": ["Dodger Blue"] },
          "slug": { "type": "string", "description": "Path of the color page: https://colormark.org/color/{slug}", "examples": ["dodgerblue"] },
          "type": { "type": "string", "enum": ["reserved", "registered_public", "registered_custom", "registered_generated"] },
          "registration": { "type": ["string", "null"], "format": "date", "description": "null for reserved colors" },
          "expiration": { "type": ["string", "null"], "format": "date", "description": "null means lifetime (or reserved)" }
        }
      },
      "SearchResult": {
        "type": "object",
        "required": ["search", "type", "query", "limit", "offset", "order", "count", "capped", "timer", "results"],
        "properties": {
          "search": { "type": "boolean", "description": "True when there is at least one result" },
          "type": { "type": "string", "enum": ["name", "hex"] },
          "query": { "type": "string" },
          "limit": { "type": "integer" },
          "offset": { "type": "integer" },
          "order": { "type": "string" },
          "count": { "type": "integer", "description": "Total matches, at most 10,000" },
          "capped": { "type": "boolean", "description": "True when more than 10,000 rows matched" },
          "timer": { "type": "number", "description": "Server time in milliseconds" },
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/Color" } }
        }
      },
      "ColorLookup": {
        "type": "object",
        "required": ["found"],
        "properties": { "found": { "type": "boolean" }, "key": { "type": "string" }, "color": { "$ref": "#/components/schemas/Color" } }
      },
      "Stats": {
        "type": "object",
        "required": ["total", "reserved", "registered", "premium", "available"],
        "properties": {
          "total": { "type": "integer", "examples": [16777216] },
          "reserved": { "type": "integer" },
          "registered": { "type": "integer" },
          "premium": { "type": "integer" },
          "available": { "type": "integer" },
          "byType": { "type": "object", "additionalProperties": { "type": "integer" } }
        }
      },
      "CheckoutRequest": {
        "type": "object",
        "required": ["hex", "name", "plan", "firstName", "lastName", "email"],
        "properties": {
          "hex": { "type": "string", "examples": ["#2b4f81"] },
          "name": { "type": "string", "description": "Letters and spaces, at least 6 letters, up to 7 words, not a reserved word", "examples": ["Blue Hour"] },
          "plan": { "type": "string", "enum": ["1", "2", "5", "lifetime"], "description": "Years, or lifetime" },
          "firstName": { "type": "string" },
          "lastName": { "type": "string" },
          "email": { "type": "string", "format": "email" }
        }
      },
      "CheckoutSession": {
        "type": "object",
        "required": ["id", "status", "provider", "hex", "name", "slug", "plan", "amountCents", "currency", "firstName", "email", "color"],
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "paid", "cancelled"] },
          "provider": { "type": "string" },
          "hex": { "type": "string" },
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "plan": { "type": "object", "properties": { "id": { "type": "string" }, "label": { "type": "string" }, "years": { "type": "integer" }, "priceCents": { "type": "integer" } } },
          "amountCents": { "type": "integer" },
          "currency": { "type": "string", "examples": ["usd"] },
          "firstName": { "type": "string" },
          "email": { "type": "string" },
          "color": { "oneOf": [{ "$ref": "#/components/schemas/Color" }, { "type": "null" }], "description": "The registered color once the session is paid" }
        }
      },
      "Ok": { "type": "object", "required": ["ok"], "properties": { "ok": { "type": "boolean" } } },
      "Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" }, "message": { "type": "string" } } },
      "FieldErrors": {
        "type": "object",
        "required": ["error", "errors"],
        "properties": { "error": { "type": "string", "examples": ["invalid", "taken"] }, "errors": { "type": "object", "additionalProperties": { "type": "string" }, "description": "A message per offending field" } }
      }
    }
  },
  "externalDocs": { "description": "ColorMark", "url": "https://colormark.org/" }
}
