{
  "openapi": "3.1.0",
  "info": {
    "title": "PartPin API",
    "version": "1.0.0",
    "summary": "Part-number resolution and cross-reference for your own B2B distributor catalog, for your own scripts and AI agents.",
    "description": "Every endpoint authenticates with a per-workspace API key (`Authorization: Bearer pk_...`, or `?key=` on GET endpoints). The key acts AS the workspace: it sees only that workspace's catalog, aliases and query log. Rate limit: 300 calls per key per hour. The same jobs are available as MCP tools at https://partpin.net/mcp and from the `partpin` CLI (`npx partpin --help`). Setup guide: https://partpin.net/docs/agents.",
    "termsOfService": "https://partpin.net/terms",
    "contact": { "name": "PartPin support", "email": "hello@heronnetworks.net" }
  },
  "servers": [{ "url": "https://partpin.net" }],
  "security": [{ "apiKey": [] }],
  "tags": [
    { "name": "account" },
    { "name": "resolve", "description": "Resolve and browse YOUR OWN uploaded catalog." },
    { "name": "catalog", "description": "Add or replace catalog items." },
    { "name": "autopsy", "description": "Score real customer queries against the catalog." },
    { "name": "write-back", "description": "Confirm a query resolves to a catalog item; list recent queries." }
  ],
  "paths": {
    "/api/v1/me": {
      "get": {
        "tags": ["account"], "operationId": "getMe", "summary": "Who this key belongs to, scopes, recent activity",
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/v1/resolve": {
      "get": {
        "tags": ["resolve"], "operationId": "resolvePart", "summary": "Resolve one query against the workspace's own catalog",
        "description": "Tiered resolver: exact/alias matches are ASSERTED; fuzzy/hybrid matches and interchange cross-references are ranked CANDIDATES, never asserted. Every call is logged to the query log.",
        "parameters": [{ "name": "q", "in": "query", "required": true, "schema": { "type": "string" }, "example": "6203DDU" }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResolveResult" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "description": "Billing refused (only when PARTPIN_BILLING_ENABLED is on)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "No catalog uploaded yet", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "tags": ["resolve"], "operationId": "searchCatalog", "summary": "Raw candidate browse of the workspace's own catalog",
        "description": "Exact key matches plus nearby trigram neighbors. No tiered scoring, no assertion doctrine, nothing logged.",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } }
        ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "required": ["query", "results"], "properties": { "query": { "type": "string" }, "results": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogItem" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "description": "No catalog uploaded yet" }, "429": { "$ref": "#/components/responses/RateLimited" } }
      }
    },
    "/api/v1/parts": {
      "post": {
        "tags": ["catalog"], "operationId": "registerPart", "summary": "Register (add or update) one catalog item",
        "description": "Additive/idempotent: does not touch the rest of the catalog (unlike POST /api/catalog, which replaces the whole catalog). Requires a write-scope key.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RegisterPartRequest" } } } },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "item"], "properties": { "ok": { "type": "boolean" }, "item": { "$ref": "#/components/schemas/CatalogItem" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Scope" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/autopsy": {
      "post": {
        "tags": ["autopsy"], "operationId": "runAutopsy", "summary": "Score real customer queries against the workspace's own catalog",
        "description": "Each query is classified resolved (asserted match), candidate (unasserted match only) or dead (nothing). Up to 5000 queries per call. Read-only. This is the authenticated, your-own-catalog version of the free, no-auth public demo at POST /api/autopsy.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["queries"], "properties": { "queries": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 5000 } } } } } },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AutopsyReport" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "description": "No catalog uploaded yet" }, "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/confirm": {
      "post": {
        "tags": ["write-back"], "operationId": "confirmMatch", "summary": "Confirm a query resolves to a catalog item (write-back)",
        "description": "Persists the query as a workspace alias so the SAME query asserts next time. Accepts either a dashboard session cookie OR a write-scope API key.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["query", "itemId"], "properties": { "query": { "type": "string" }, "itemId": { "type": "string" }, "source": { "type": "string", "enum": ["confirm", "interchange"] } } } } } },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "learned": { "type": "boolean" }, "alias": { "type": "string" }, "itemId": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "description": "Not authenticated (no session, no key)" }, "404": { "description": "itemId not found" }
        }
      }
    },
    "/api/v1/queries": {
      "get": {
        "tags": ["write-back"], "operationId": "listQueries", "summary": "Recent resolve calls, or only unresolved ones",
        "parameters": [
          { "name": "misses", "in": "query", "schema": { "type": "string", "enum": ["1", "true"] }, "description": "Only zero-result / candidate-only (unasserted) queries" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } }
        ],
        "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "queries": { "type": "array", "items": { "$ref": "#/components/schemas/QueryLogRow" } } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": { "type": "http", "scheme": "bearer", "bearerFormat": "pk_<48 hex>", "description": "Create or rotate a key from the dashboard (API KEY card). Scopes: read, write." }
    },
    "responses": {
      "Unauthorized": { "description": "Missing, unknown or revoked key. Carries `WWW-Authenticate: Bearer realm=\"partpin\"`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "BadRequest": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Scope": { "description": "The key lacks the write scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "300 calls per key per hour exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" }, "docs": { "type": "string" }, "createKeyAt": { "type": "string" } } },
      "Me": {
        "type": "object", "required": ["orgId", "key"],
        "properties": {
          "orgId": { "type": "string" }, "billingAllowed": { "type": "boolean" },
          "key": { "type": "object", "properties": { "id": { "type": "string" }, "scopes": { "type": "array", "items": { "type": "string", "enum": ["read", "write"] } }, "prefix": { "type": ["string", "null"] }, "lastUsedAt": { "type": ["integer", "null"] } } },
          "recentQueries": { "type": "array", "items": { "type": "object" } },
          "recentAgentCalls": { "type": "array", "items": { "type": "object" } }
        }
      },
      "CatalogItem": { "type": "object", "required": ["id", "sku"], "properties": { "id": { "type": "string" }, "sku": { "type": "string" }, "mpn": { "type": "string" }, "title": { "type": "string" }, "aliases": { "type": "array", "items": { "type": "string" } } } },
      "RegisterPartRequest": { "allOf": [{ "$ref": "#/components/schemas/CatalogItem" }] },
      "Match": {
        "type": "object", "required": ["item", "tier", "confidence", "asserted", "explanation"],
        "properties": { "item": { "$ref": "#/components/schemas/CatalogItem" }, "tier": { "type": "string", "enum": ["exact", "alias", "fuzzy", "hybrid", "llm", "interchange"] }, "confidence": { "type": "number" }, "asserted": { "type": "boolean" }, "explanation": { "type": "string" } }
      },
      "ResolveResult": { "type": "object", "required": ["query", "matches"], "properties": { "query": { "type": "string" }, "matches": { "type": "array", "items": { "$ref": "#/components/schemas/Match" } } } },
      "AutopsyReport": {
        "type": "object", "required": ["total", "resolved", "candidates", "dead", "recoveredPct", "results"],
        "properties": { "total": { "type": "integer" }, "resolved": { "type": "integer" }, "candidates": { "type": "integer" }, "dead": { "type": "integer" }, "recoveredPct": { "type": "integer" }, "results": { "type": "array", "items": { "type": "object", "properties": { "query": { "type": "string" }, "status": { "type": "string", "enum": ["resolved", "candidate", "dead"] }, "top": { "anyOf": [{ "$ref": "#/components/schemas/Match" }, { "type": "null" }] } } } } }
      },
      "QueryLogRow": { "type": "object", "properties": { "query": { "type": "string" }, "normalized": { "type": "string" }, "topTier": { "type": ["string", "null"] }, "asserted": { "type": "boolean" }, "hit": { "type": "boolean" }, "at": { "type": "integer" } } }
    }
  }
}
