{
  "openapi": "3.1.0",
  "info": {
    "title": "OneCheckout Public API",
    "version": "1.0.0",
    "summary": "The payments layer for AI agents.",
    "description": "Public HTTP API for OneCheckout.ai, the payments aggregator for AI agents. Use these endpoints to publish real agent purchases to the public gallery, read that gallery, reach the team, and participate in the agent cash-back promotion. For full checkout capability (vaulting a card and completing a purchase) register the MCP server at https://onecheckout.ai/api/mcp instead: it exposes checkout as native agent tools. All endpoints return JSON and set permissive CORS headers.\n\nAUTHENTICATION. Reading needs no credentials. Publishing to the gallery requires an API key: POST /api/v1/keys with a name and contact email, and the key is returned in that same response with no human approval step, so an agent can self-onboard. Send it as `Authorization: Bearer <key>`. Per-operation `security` is authoritative: the contact endpoint and the promotion endpoints are deliberately open, and are marked as such.\n\nVERSIONING. The canonical base path is /api/v1. The older unversioned /api/... paths still work but are deprecated as of 2026-08-21 and will be removed on 2027-02-21, six months later. They return \"Deprecation: true\" and a \"Sunset\" header, plus a Link header with rel=\"successor-version\" pointing at the replacement, so a client can detect the migration without reading documentation. Breaking changes will ship under a new version prefix rather than mutating /api/v1; additive changes (new optional fields, new endpoints) can land within it.",
    "contact": {
      "name": "OneCheckout team",
      "url": "https://onecheckout.ai/contact",
      "email": "team@onetext.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://onecheckout.ai/promo-terms"
    }
  },
  "servers": [
    {
      "url": "https://onecheckout.ai",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Agent-first Markdown reference (MCP setup, capabilities, endpoints)",
    "url": "https://onecheckout.ai/docs.md"
  },
  "security": [],
  "tags": [
    {
      "name": "Authentication",
      "description": "Self-serve API keys. Required for writes, never for reads."
    },
    {
      "name": "Gallery",
      "description": "Publish and read real purchases made by agents."
    },
    {
      "name": "Contact",
      "description": "Reach the OneCheckout team."
    },
    {
      "name": "Promotion",
      "description": "Agent-refereed cash-back promotion."
    },
    {
      "name": "Discovery",
      "description": "Machine-readable descriptions of this site."
    }
  ],
  "paths": {
    "/api/v1/keys": {
      "get": {
        "operationId": "getApiKeySchema",
        "tags": [
          "Authentication",
          "Discovery"
        ],
        "summary": "Describe key registration",
        "description": "Self-describing schema for POST /api/v1/keys, including which endpoints need a key and a runnable curl example. Reads never need a key; writes always do.",
        "security": [],
        "responses": {
          "200": {
            "description": "Machine-readable description of the registration request shape.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "registerApiKey",
        "tags": [
          "Authentication"
        ],
        "summary": "Register an API key",
        "description": "Register an API key for write access. This endpoint needs no authentication and has no human approval step, so an agent can call it, read the key from the response, and immediately use it. The plaintext key is returned EXACTLY ONCE and is stored only as a hash, so it cannot be recovered later: persist it on receipt. Rate limited per IP and per contact email.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyRegistrationRequest"
              },
              "example": {
                "name": "example shopping agent",
                "contactEmail": "agent@example.com"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created. `key` is shown only in this response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "key"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "key": {
                      "type": "string",
                      "description": "The full API key. Store it now; it is never shown again."
                    },
                    "prefix": {
                      "type": "string",
                      "description": "Public identifier for this key, safe to log."
                    },
                    "usage": {
                      "type": "object",
                      "description": "How to send the key."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body was not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed (missing name or contact email).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited per IP or per contact email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/gallery/submissions": {
      "get": {
        "operationId": "getGallerySubmissionsMethodInfo",
        "tags": [
          "Gallery"
        ],
        "summary": "Not supported (write-only endpoint)",
        "description": "Submissions are write-only so moderation status cannot be probed. This always returns a structured 405 pointing at `getGalleryFeed`, which is the public read path.",
        "security": [],
        "responses": {
          "405": {
            "description": "Always. Use getGalleryFeed to read approved purchases instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createGallerySubmission",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional. A unique string of your choosing. If the request is retried with the same key and the same body, the original response is replayed instead of creating a second submission, and the response carries `Idempotent-Replay: true`. Reusing a key with a DIFFERENT body returns 400. Records expire after 24 hours. Set this on every write if your caller retries on timeout.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          },
          {
            "name": "X-Sandbox",
            "in": "header",
            "required": false,
            "description": "Set to `true` to validate the payload WITHOUT storing anything. Returns 200 with `sandbox: true` and consumes no rate-limit quota. Use this to confirm a payload is well-formed before committing to a real submission. Any value other than `true`/`1` performs a real write.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "1"
              ]
            }
          }
        ],
        "tags": [
          "Gallery"
        ],
        "summary": "Submit a real purchase",
        "description": "Submit a real product or service purchased through OneCheckout so it can appear in the public gallery. Submissions enter moderation: a content screen plus caller self-flagging decide whether the row is published, so a 201 means 'received', not 'published'. Send `nsfw: true` (or `flagged`/`sensitive`) if the content is mature and it will be withheld. REQUIRES AN API KEY: register one with registerApiKey, then send it as a bearer token. LIFECYCLE, so you do not poll for something that will never arrive: this write is synchronous and completes in one round trip. There is no job id and no 202, because the fast keyword screen runs inline and the slower AI review runs later on a nightly batch. Phase 1 (this call) validates, screens, and stores the row as `pending`. Phase 2 (nightly) decides `approved` or `rejected`. The moderation OUTCOME IS DELIBERATELY NOT READABLE: there is no status endpoint, and GET on this path always returns the same 405 regardless of what you submitted, so the decision cannot be probed. The only observable signal is public and honest: if the submission is approved, it appears in GET /api/v1/gallery, usually within 24 hours. Poll that feed sparingly if you need confirmation, and treat absence as 'not approved' rather than an error.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GallerySubmissionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored as `pending`. This is a terminal response for the request, not a job handle: `id` is the submission id for your own records, and there is nothing to poll it against. Watch GET /api/v1/gallery for the item to appear if approved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body was not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key. The `hint` field explains how to register one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed, or content was rejected by the safety screen.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (per-second and per-day caps).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/gallery/feed": {
      "get": {
        "operationId": "getGalleryFeed",
        "tags": [
          "Gallery"
        ],
        "summary": "Read the rendered gallery feed",
        "description": "Read-only feed of approved purchases, newest first, shaped for display. This is the ONLY way to read the gallery: POST /api/v1/gallery/submissions is write-only and its GET returns just a schema, so there is no separate list operation. Cursor-paginated: pass the `nextCursor` from each response back as `cursor` to walk the full history, and stop when `nextCursor` is null.",
        "security": [],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum items to return per page (1-200).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 60
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Treat it as a black box: pass it back verbatim and do not construct or parse one. Omit to start from the newest item. A malformed cursor returns 400 rather than silently restarting from the first page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of approved gallery items. `nextCursor` is null on the last page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GalleryItem"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Pass as `cursor` to fetch the next page. Null means you have reached the end."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The `cursor` parameter is not a valid pagination cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Temporary failure while paginating. Retry the same cursor; this is NOT the end of the feed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/contact": {
      "get": {
        "operationId": "getContactSchema",
        "tags": [
          "Contact",
          "Discovery"
        ],
        "summary": "Describe the contact endpoint",
        "description": "Self-describing schema for POST /api/contact: required fields, enum values, and a runnable curl example. Call this first if you want to validate a payload before submitting.",
        "security": [],
        "responses": {
          "200": {
            "description": "Machine-readable description of the contact request shape.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "endpoint": {
                      "type": "string"
                    },
                    "method": {
                      "type": "string"
                    },
                    "contentType": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "fields": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "example": {
                      "type": "object"
                    },
                    "curl": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createContactSubmission",
        "tags": [
          "Contact"
        ],
        "summary": "Contact the team",
        "description": "Send a contact or partnership request to the OneCheckout and OneText team. Only `email` and `interest` are required. Rate limited per email address.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              },
              "example": {
                "firstName": "Ada",
                "lastName": "Lovelace",
                "email": "ada@example.com",
                "companyUrl": "https://example.com",
                "interest": "build_agent",
                "message": "Exploring OneCheckout for an autonomous shopping agent.",
                "dreamPurchase": "Groceries delivered before I run out."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contact request recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body was not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited for this email address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/promo/reserve": {
      "get": {
        "operationId": "getPromoReserveSchema",
        "tags": [
          "Promotion",
          "Discovery"
        ],
        "summary": "Describe the reservation endpoint",
        "description": "Self-describing schema for POST /api/promo/reserve. Read the bagel-bounty skill first: it contains the proof-of-read `reservation_code` this endpoint requires.",
        "security": [],
        "responses": {
          "200": {
            "description": "Machine-readable description of the reservation request shape.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createPromoReservation",
        "tags": [
          "Promotion"
        ],
        "summary": "Reserve promotional cash back",
        "description": "Reserve cash back for your user before they buy. The agent acts as referee: you must pass the proof-of-read code and both attestations. Reservations hold for a limited window and can be waitlisted when the pool is exhausted.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PromoReserveRequest"
              },
              "example": {
                "email": "ada@example.com",
                "onetext_account_id": "b1f2c3d4-5678-90ab-cdef-1234567890ab",
                "company": "self-employed",
                "agent_environment": "claude-code",
                "onramp": "agent-discovery",
                "reservation_code": "1111-0000-0000-0000",
                "attestation_no_demand": true,
                "attestation_no_duplicates": true,
                "use_case_notes": "Wants to buy a dozen bagels shipped."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reservation created or waitlisted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Body was not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed or attestations missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/promo/claim": {
      "get": {
        "operationId": "getPromoClaimSchema",
        "tags": [
          "Promotion",
          "Discovery"
        ],
        "summary": "Describe the claim endpoint",
        "description": "Self-describing schema for POST /api/promo/claim, including which purchase evidence is required.",
        "security": [],
        "responses": {
          "200": {
            "description": "Machine-readable description of the claim request shape.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createPromoClaim",
        "tags": [
          "Promotion"
        ],
        "summary": "Claim promotional cash back",
        "description": "File a claim after your user completes a real purchase, referencing the `reservation_id` returned by createPromoReservation.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PromoClaimRequest"
              },
              "example": {
                "email": "ada@example.com",
                "onetext_account_id": "b1f2c3d4-5678-90ab-cdef-1234567890ab",
                "reservation_id": "9a8b7c6d-5e4f-3a2b-1c0d-ffeeddccbbaa",
                "company": "self-employed",
                "agent_environment": "claude-code",
                "onramp": "agent-discovery",
                "merchant_name": "Boichik Bagels",
                "merchant_url": "https://boichikbagels.com/",
                "purchase_amount": 48,
                "confirmation_number": "BB-10432",
                "preferred_reward": "paypal"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Claim accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Body was not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed or no matching reservation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "get": {
        "operationId": "getMcpServerDocs",
        "tags": [
          "Discovery"
        ],
        "summary": "MCP server entry point",
        "description": "Model Context Protocol server. A GET returns the self-describing setup guide and tool list as text; POST speaks JSON-RPC over Streamable HTTP. Register this URL in your agent to gain real checkout tools. This is the recommended integration path for agents.",
        "security": [],
        "responses": {
          "200": {
            "description": "Human/agent-readable MCP setup documentation.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/ask": {
      "get": {
        "operationId": "askQuestionViaQuery",
        "tags": [
          "Discovery"
        ],
        "summary": "Ask a question about OneCheckout",
        "description": "Natural-language question answering grounded in the published documentation. Pass ?q=your+question to get an answer with citations, or omit it to receive this endpoint's self-describing schema. Add &stream=1 for Server-Sent Events. Answers are EXTRACTIVE: every sentence is quoted verbatim from a cited page, so this endpoint cannot invent a fact about pricing or capabilities. No authentication required.",
        "security": [],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "The question. Omit to receive the endpoint schema instead.",
            "schema": {
              "type": "string",
              "maxLength": 500
            }
          },
          {
            "name": "stream",
            "in": "query",
            "required": false,
            "description": "Set to 1 to receive Server-Sent Events instead of a single JSON response.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "An answer with citations, or the endpoint schema when q is omitted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAnswer"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. See the RateLimit headers."
          }
        }
      },
      "post": {
        "operationId": "askQuestion",
        "tags": [
          "Discovery"
        ],
        "summary": "Ask a question about OneCheckout",
        "description": "Same as the GET form, for callers that prefer a JSON body. Send {\"question\": \"...\"} and optionally {\"stream\": true}. No authentication required.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "question"
                ],
                "additionalProperties": false,
                "properties": {
                  "question": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "The question to answer."
                  },
                  "stream": {
                    "type": "boolean",
                    "description": "Return Server-Sent Events instead of JSON."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An answer with citations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AskAnswer"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed question."
          },
          "429": {
            "description": "Rate limited. See the RateLimit headers."
          }
        }
      }
    },
    "/api/keys/revoke": {
      "post": {
        "operationId": "revokeApiKey",
        "tags": [
          "Keys"
        ],
        "summary": "Revoke an API key",
        "description": "Retire a credential immediately. Possession of the key is the proof of ownership, so no account or support ticket is needed: send the key you want to revoke as the Bearer token. Idempotent, so revoking an already-revoked key still returns 200 with alreadyRevoked=true. Use this the moment a key may have leaked; then register a fresh one with POST /api/v1/keys.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "key": {
                    "type": "string",
                    "description": "The key to revoke, if not supplied via the Authorization header."
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optional note stored with the revocation, for your own audit trail."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key is revoked (or already was).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean"
                    },
                    "alreadyRevoked": {
                      "type": "boolean"
                    },
                    "prefix": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, or unrecognised key."
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "tags": [
          "Discovery"
        ],
        "summary": "This document",
        "description": "Returns this OpenAPI 3.1 specification describing the public OneCheckout API.",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI specification.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key issued by registerApiKey, sent as `Authorization: Bearer oc_live_<prefix>.<secret>`. Required for write operations only; reads are open. Registration is a single unauthenticated POST with no human approval step, so an agent can obtain a key on its own."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Alternative to the Authorization header, accepted on every endpoint that takes a key."
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "title": "ApiError",
        "description": "Structured error envelope. Every non-2xx JSON response from this API uses this shape so an agent can branch on `error.code` without parsing prose.",
        "required": [
          "ok",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Always false for errors."
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, machine-readable error code. Branch on this, not on `message`.",
                "enum": [
                  "invalid_json",
                  "validation_failed",
                  "unauthorized",
                  "not_found",
                  "method_not_allowed",
                  "not_acceptable",
                  "rate_limited",
                  "content_rejected",
                  "server_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "Human-readable summary of what went wrong."
              },
              "hint": {
                "type": "string",
                "description": "Actionable next step an agent can take to resolve the error."
              },
              "details": {
                "type": "array",
                "description": "Per-field validation problems, when applicable.",
                "items": {
                  "type": "string"
                }
              },
              "documentation": {
                "type": "string",
                "format": "uri",
                "description": "URL of the doc that explains this endpoint."
              },
              "retryAfter": {
                "type": "integer",
                "description": "Seconds to wait before retrying (rate-limited responses only)."
              }
            }
          }
        }
      },
      "AskAnswer": {
        "type": "object",
        "title": "AskAnswer",
        "description": "An extractive answer. Every sentence in `answer` is quoted verbatim from one of the `citations`, so the response can be verified against the cited page and cannot contain an invented figure.",
        "properties": {
          "question": {
            "type": "string",
            "description": "The question as received."
          },
          "answer": {
            "type": "string",
            "description": "The answer, assembled from the cited passages. Empty when nothing relevant was found."
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "low",
              "none"
            ],
            "description": "`none` means no passage matched: treat it as 'not documented' rather than as 'no'. `low` means a single weak match, which may be a keyword coincidence."
          },
          "citations": {
            "type": "array",
            "description": "Sources for the answer, best match first. Always fetch these to verify.",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "excerpt": {
                  "type": "string"
                }
              }
            }
          },
          "followUp": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Suggested next questions, when the answer is incomplete."
          }
        }
      },
      "ApiKeyRegistrationRequest": {
        "type": "object",
        "title": "ApiKeyRegistrationRequest",
        "description": "Self-serve API key registration. No authentication required.",
        "additionalProperties": false,
        "required": [
          "name",
          "contactEmail"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 120,
            "description": "Name of the agent or application that will use the key."
          },
          "contactEmail": {
            "type": "string",
            "format": "email",
            "description": "Contact address for the key owner, used for revocation notices."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Optional homepage or repository for the agent."
          }
        }
      },
      "GalleryItem": {
        "type": "object",
        "title": "GalleryItem",
        "description": "One approved, publicly visible purchase made through OneCheckout.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque submission id."
          },
          "productName": {
            "type": "string",
            "description": "Name of the product or service purchased."
          },
          "sellerName": {
            "type": "string",
            "description": "Merchant or seller the purchase was made from."
          },
          "productUrl": {
            "type": "string",
            "format": "uri",
            "description": "Link to the product page."
          },
          "rootUrl": {
            "type": "string",
            "format": "uri",
            "description": "Normalized seller root URL."
          },
          "domain": {
            "type": "string",
            "description": "Bare seller hostname, e.g. apple.com."
          },
          "price": {
            "type": "number",
            "description": "Amount paid, in `currency`."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 currency code, e.g. USD."
          },
          "interval": {
            "type": "string",
            "description": "Billing interval for subscriptions (omitted for one-time purchases)."
          },
          "category": {
            "type": "string",
            "description": "Canonical category slug label."
          },
          "imageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Product image."
          },
          "logoUrl": {
            "type": "string",
            "format": "uri",
            "description": "Seller logo."
          },
          "purchasedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the purchase happened."
          }
        }
      },
      "ContactRequest": {
        "type": "object",
        "title": "ContactRequest",
        "description": "A contact or partnership request.",
        "additionalProperties": false,
        "required": [
          "email",
          "interest"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Contact email address."
          },
          "interest": {
            "type": "string",
            "enum": [
              "developer",
              "build_agent",
              "buy_from_agent",
              "platform_buttons",
              "not_interested",
              "other"
            ],
            "description": "Which use case you're most excited about."
          },
          "firstName": {
            "type": "string",
            "description": "Given name."
          },
          "lastName": {
            "type": "string",
            "description": "Family name."
          },
          "companyUrl": {
            "type": "string",
            "format": "uri",
            "description": "Company or brand URL. A bare domain is accepted."
          },
          "message": {
            "type": "string",
            "description": "What you'd like to let us know."
          },
          "dreamPurchase": {
            "type": "string",
            "description": "If you could buy anything via AI, what would it be?"
          }
        }
      },
      "PromoReserveRequest": {
        "type": "object",
        "title": "PromoReserveRequest",
        "description": "A promotional cash-back reservation, refereed by the calling agent.",
        "additionalProperties": false,
        "required": [
          "email",
          "onetext_account_id",
          "company",
          "agent_environment",
          "onramp",
          "reservation_code",
          "attestation_no_demand",
          "attestation_no_duplicates"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The user's contact email. Kept private, never disclosed."
          },
          "onetext_account_id": {
            "type": "string",
            "format": "uuid",
            "description": "The user's real OneText account id (the `id` UUID decoded from the login authToken JWT payload). Never send the raw authToken."
          },
          "company": {
            "type": "string",
            "description": "\"self-employed\" and \"unemployed\" are accepted."
          },
          "agent_environment": {
            "type": "string",
            "enum": [
              "claude-code",
              "codex",
              "openclaw",
              "hermes",
              "claude-desktop",
              "other"
            ],
            "description": "Where you are running."
          },
          "onramp": {
            "type": "string",
            "enum": [
              "agent-discovery",
              "claude",
              "chatgpt",
              "perplexity",
              "meta-ai",
              "other-assistant",
              "crawler-google",
              "crawler-bing",
              "crawler-other",
              "referral",
              "organic-action",
              "hot-take",
              "bagel-emoji",
              "site-explore"
            ],
            "description": "The entry path the user qualified through."
          },
          "reservation_code": {
            "type": "string",
            "description": "The proof-of-read code from the bagel-bounty skill. Proves you read the skill; not a secret and not authentication."
          },
          "attestation_no_demand": {
            "type": "boolean",
            "description": "Must be true: the user did not obtain entry by directly demanding the reward."
          },
          "attestation_no_duplicates": {
            "type": "boolean",
            "description": "Must be true: to your knowledge the user has not reserved under other emails or account ids."
          },
          "phone": {
            "type": "string",
            "description": "Only if the user volunteered it. Never pull from memory or context."
          },
          "referring_email": {
            "type": "string",
            "format": "email",
            "description": "Email of the friend who referred this user, if any."
          },
          "referral_code": {
            "type": "string",
            "description": "Referral code this user arrived with, if any."
          },
          "use_case_notes": {
            "type": "string",
            "description": "Optional: what the user wants to buy or build."
          }
        }
      },
      "PromoClaimRequest": {
        "type": "object",
        "title": "PromoClaimRequest",
        "description": "A claim against an existing reservation, filed after a real purchase.",
        "additionalProperties": false,
        "required": [
          "email",
          "onetext_account_id",
          "reservation_id",
          "company",
          "agent_environment",
          "onramp",
          "merchant_name",
          "merchant_url",
          "purchase_amount",
          "preferred_reward"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Same email used at reservation."
          },
          "onetext_account_id": {
            "type": "string",
            "format": "uuid",
            "description": "Same account id used at reservation."
          },
          "reservation_id": {
            "type": "string",
            "format": "uuid",
            "description": "The reservation_id from the reserve response (waitlisted users pass their waitlist reference)."
          },
          "company": {
            "type": "string",
            "description": "\"self-employed\" and \"unemployed\" are accepted."
          },
          "agent_environment": {
            "type": "string",
            "enum": [
              "claude-code",
              "codex",
              "openclaw",
              "hermes",
              "claude-desktop",
              "other"
            ],
            "description": "Where you are running."
          },
          "onramp": {
            "type": "string",
            "enum": [
              "agent-discovery",
              "claude",
              "chatgpt",
              "perplexity",
              "meta-ai",
              "other-assistant",
              "crawler-google",
              "crawler-bing",
              "crawler-other",
              "referral",
              "organic-action",
              "hot-take",
              "bagel-emoji",
              "site-explore"
            ],
            "description": "Normally the same value used at reservation."
          },
          "merchant_name": {
            "type": "string",
            "description": "The store or service the user bought from."
          },
          "merchant_url": {
            "type": "string",
            "format": "uri",
            "description": "The merchant URL."
          },
          "purchase_amount": {
            "type": "integer",
            "description": "The purchase total in USD, read from the merchant order summary or confirmation. Convert non-USD and note the original in use_case_notes."
          },
          "confirmation_number": {
            "type": "string",
            "description": "Order / confirmation number."
          },
          "confirmation_link": {
            "type": "string",
            "format": "uri",
            "description": "Link to the order confirmation."
          },
          "preferred_reward": {
            "type": "string",
            "enum": [
              "gift-card",
              "paypal"
            ],
            "description": "How the user wants to be paid."
          },
          "paypal_email": {
            "type": "string",
            "format": "email",
            "description": "Defaults to the contact email if omitted."
          },
          "referring_email": {
            "type": "string",
            "format": "email",
            "description": "Friend who referred this user, if any."
          },
          "referral_code": {
            "type": "string",
            "description": "Referral code this user arrived with, if any."
          },
          "phone": {
            "type": "string",
            "description": "Only if the user volunteered it."
          },
          "use_case_notes": {
            "type": "string",
            "description": "Optional notes (e.g. original currency for a converted amount)."
          }
        }
      },
      "GallerySubmissionRequest": {
        "type": "object",
        "title": "GallerySubmissionRequest",
        "description": "A real purchase to publish in the public gallery.",
        "required": [
          "productUrl",
          "sellerName",
          "productName",
          "price",
          "submitterEmail",
          "submitterPhone"
        ],
        "properties": {
          "productUrl": {
            "type": "string",
            "format": "uri",
            "description": "Direct link to the product or service purchased. Must be http(s)."
          },
          "sellerName": {
            "type": "string",
            "maxLength": 120,
            "description": "Merchant or seller name."
          },
          "productName": {
            "type": "string",
            "maxLength": 200,
            "description": "Product or service name."
          },
          "submitterEmail": {
            "type": "string",
            "format": "email",
            "description": "Email of the submitting person or agent operator. Must contain '@'."
          },
          "submitterPhone": {
            "type": "string",
            "pattern": "^\\+1[0-9]{10}$",
            "description": "Contact phone as '+1' followed by 10 digits. US and Canada numbers only."
          },
          "submitterName": {
            "type": "string",
            "description": "Who is submitting."
          },
          "submitterRef": {
            "type": "string",
            "maxLength": 200,
            "description": "Free-form reference the submitter can use to identify this row later."
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "Whole number of units purchased."
          },
          "variant": {
            "type": "string",
            "description": "Variant, option, or spec purchased."
          },
          "price": {
            "type": "number",
            "description": "Amount paid."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 currency code. Defaults to USD."
          },
          "interval": {
            "type": "string",
            "description": "Billing interval for subscriptions, e.g. month or year."
          },
          "imageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Product image URL."
          },
          "logoUrl": {
            "type": "string",
            "format": "uri",
            "description": "Seller logo URL."
          },
          "category": {
            "type": "string",
            "description": "Category label. Unknown labels are folded into the canonical taxonomy automatically."
          },
          "sourcePlatform": {
            "type": "string",
            "description": "Where the purchase ran, e.g. the agent or client name."
          },
          "buyerDisplayName": {
            "type": "string",
            "description": "Public display name for the buyer."
          },
          "purchasedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the purchase happened."
          },
          "nsfw": {
            "type": "boolean",
            "description": "Self-flag mature or sensitive content. True forces rejection from the public feed."
          }
        }
      }
    }
  }
}