{
  "openapi": "3.1.0",
  "info": {
    "title": "ShelfCycle Developer API",
    "version": "v1",
    "summary": "Resolve ShelfCycle records and write notes and contacts back to them.",
    "description": "A scoped API for chemical distributors built on ShelfCycle. The core workflow is resolve, confirm, write: search returns typed candidate records, a human confirms the right one, then a write creates a note or contact with an idempotency key and returns a link back to it in ShelfCycle. This API does not touch orders, inventory, pricing, or accounting. Every write is attributed to a real user, checked against that user's live permissions, and recorded in the audit trail.\n\nThis specification is a placeholder. It will be replaced by a specification generated from the server schemas, which is the source of truth."
  },
  "servers": [
    { "url": "https://app.shelfcycle.com/api/v1", "description": "Production" }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Introspection", "description": "Confirm a key, its org, scopes, and acting user." },
    { "name": "Search", "description": "Resolve records into typed candidate cards before a write." },
    { "name": "Notes", "description": "Create, read, and update notes written through this API." },
    { "name": "Contacts", "description": "Create, read, and update customer and supplier contacts." }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "tags": ["Introspection"],
        "summary": "Introspect the current key",
        "description": "Returns the org, the key, its scopes, and the acting user. Any valid key may call this. No scope is required.",
        "responses": {
          "200": {
            "description": "The current key context.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MeResponse" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/search": {
      "get": {
        "operationId": "search",
        "tags": ["Search"],
        "summary": "Search records",
        "description": "Resolves customers, suppliers, contacts, products, orders, and locations into typed candidate cards. Requires the search:read scope. Ranking is not contractual; it is a candidate list a human or agent confirms before acting.",
        "x-required-scope": "search:read",
        "parameters": [
          { "name": "q", "in": "query", "required": true, "schema": { "type": "string", "minLength": 2, "maxLength": 120 }, "description": "The search term." },
          { "name": "types", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Optional comma list of record types to search: customer, supplier, contact, product, order, location." },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 20, "default": 10 } }
        ],
        "responses": {
          "200": {
            "description": "A ranked list of candidate cards.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResult" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/notes": {
      "post": {
        "operationId": "createNote",
        "tags": ["Notes"],
        "summary": "Create a note",
        "description": "Creates a note about a primary subject (customer, supplier, or product) with optional linked records. Requires the notes:write scope and an Idempotency-Key header. The acting user must have permission to view every referenced record.",
        "x-required-scope": "notes:write",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateNoteRequest" } } }
        },
        "responses": {
          "201": { "description": "The created note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NoteResponse" } } } },
          "200": { "description": "An idempotent replay of a previously created note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NoteResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/notes/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/IdPath" }],
      "get": {
        "operationId": "getNote",
        "tags": ["Notes"],
        "summary": "Read a note",
        "description": "Returns a note created through this API. Requires the notes:write scope. A note that was not created through this API returns 403.",
        "x-required-scope": "notes:write",
        "responses": {
          "200": { "description": "The note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NoteResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "operationId": "updateNote",
        "tags": ["Notes"],
        "summary": "Update a note",
        "description": "Updates a note created through this API. Requires the notes:write scope and an If-Match header set to the note's updatedAt value. A stale value returns 409.",
        "x-required-scope": "notes:write",
        "parameters": [{ "$ref": "#/components/parameters/IfMatch" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateNoteRequest" } } }
        },
        "responses": {
          "200": { "description": "The updated note.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NoteResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/contacts": {
      "post": {
        "operationId": "createContact",
        "tags": ["Contacts"],
        "summary": "Create a contact",
        "description": "Creates a customer or supplier contact under an existing parent. Requires the contacts:create scope and an Idempotency-Key header. A name or email that matches an existing contact under the same parent returns 409.",
        "x-required-scope": "contacts:create",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateContactRequest" } } }
        },
        "responses": {
          "201": { "description": "The created contact.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactResponse" } } } },
          "200": { "description": "An idempotent replay of a previously created contact.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/contacts/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/IdPath" }],
      "get": {
        "operationId": "getContact",
        "tags": ["Contacts"],
        "summary": "Read a contact",
        "description": "Returns a customer or supplier contact by id. Requires the contacts:update scope.",
        "x-required-scope": "contacts:update",
        "responses": {
          "200": { "description": "The contact.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      },
      "patch": {
        "operationId": "updateContact",
        "tags": ["Contacts"],
        "summary": "Update a contact",
        "description": "Updates safe fields on a contact. Requires the contacts:update scope and an If-Match header set to the contact's updatedAt value. Optional fields may be set to null to clear them.",
        "x-required-scope": "contacts:update",
        "parameters": [{ "$ref": "#/components/parameters/IfMatch" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateContactRequest" } } }
        },
        "responses": {
          "200": { "description": "The updated contact.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContactResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A ShelfCycle API key in the form sk_ followed by a random secret. Send it in the Authorization header."
      }
    },
    "parameters": {
      "IdPath": { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The record id." },
      "IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string" }, "description": "A stable key derived from the source event, so retries return the original record rather than creating a duplicate." },
      "IfMatch": { "name": "If-Match", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The record's current updatedAt value, used for optimistic concurrency. A stale value returns 409." }
    },
    "responses": {
      "Unauthorized": { "description": "The key is missing, malformed, unknown, revoked, expired, or its acting user is no longer active.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "The key lacks the required scope, or the acting user lacks the required live permission.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "The record was not found, is archived, or belongs to another org.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Conflict": { "description": "An idempotency key was reused with a different body, a record changed since it was read, or a matching contact already exists.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "ValidationError": { "description": "The request failed validation. The param field names the offending input.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "The per-key, per-endpoint rate limit was exceeded. Retry after the delay in the Retry-After header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["type", "code", "message", "requestId"],
            "properties": {
              "type": { "type": "string", "enum": ["auth_error", "method_error", "permission_error", "validation_error", "not_found", "conflict", "rate_limit_error", "server_error"] },
              "code": { "type": "string", "examples": ["missing_scope", "stale_record", "idempotency_key_reused", "validation_failed", "duplicate_contact", "not_found"] },
              "message": { "type": "string" },
              "param": { "type": "string", "description": "Present when a specific field is at fault." },
              "requestId": { "type": "string" }
            }
          }
        },
        "examples": [{ "error": { "type": "conflict", "code": "stale_record", "message": "Record changed since it was last read.", "param": "If-Match", "requestId": "req_8f0a1c" } }]
      },
      "RecordRef": {
        "type": "object",
        "required": ["type", "id"],
        "properties": {
          "type": { "type": "string", "enum": ["customer", "supplier", "product", "contact", "location"] },
          "id": { "type": "string", "format": "uuid" }
        }
      },
      "NoteSubject": {
        "type": "object",
        "required": ["type", "id"],
        "properties": {
          "type": { "type": "string", "enum": ["customer", "supplier", "product"] },
          "id": { "type": "string", "format": "uuid" }
        }
      },
      "MeResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "apiVersion": { "type": "string", "const": "v1" },
              "org": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } },
              "key": {
                "type": "object",
                "properties": {
                  "id": { "type": "string" },
                  "name": { "type": "string" },
                  "purpose": { "type": ["string", "null"] },
                  "preview": { "type": "string", "examples": ["sk_...3b00"] },
                  "scopes": { "type": "array", "items": { "type": "string" } },
                  "expiresAt": { "type": ["string", "null"], "format": "date-time" }
                }
              },
              "actor": { "type": "object", "properties": { "type": { "type": "string", "const": "user" }, "id": { "type": "string" }, "displayName": { "type": "string" } } },
              "provenance": { "type": "object", "properties": { "apiKeyId": { "type": "string" }, "purpose": { "type": ["string", "null"] } } }
            }
          }
        }
      },
      "SearchCard": {
        "type": "object",
        "required": ["type", "id", "displayName", "subtitle", "url", "archived"],
        "properties": {
          "type": { "type": "string", "enum": ["customer", "supplier", "contact", "product", "order", "location"] },
          "id": { "type": "string" },
          "displayName": { "type": "string" },
          "subtitle": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "archived": { "type": "boolean" },
          "parent": { "type": "object", "properties": { "type": { "type": "string", "enum": ["customer", "supplier"] }, "id": { "type": "string" }, "displayName": { "type": "string" } } }
        }
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/SearchCard" } },
          "meta": {
            "type": "object",
            "properties": {
              "requestId": { "type": "string" },
              "limit": { "type": "integer" },
              "types": { "type": "array", "items": { "type": "string" } },
              "rankingIsContractual": { "type": "boolean", "const": false },
              "truncated": { "type": "boolean" },
              "durationMs": { "type": "integer" }
            }
          }
        }
      },
      "CreateNoteRequest": {
        "type": "object",
        "required": ["primarySubject", "noteType", "title", "body", "happenedAt"],
        "properties": {
          "primarySubject": { "$ref": "#/components/schemas/NoteSubject" },
          "linkedRecords": { "type": "array", "items": { "$ref": "#/components/schemas/RecordRef" }, "maxItems": 50, "default": [] },
          "noteType": { "type": "string", "enum": ["NOTE", "EMAIL", "PHONE_MEETING", "VIRTUAL_MEETING", "PHYSICAL_MEETING", "SAMPLE_REQUEST", "OPPORTUNITY", "QUOTE"] },
          "title": { "type": "string", "minLength": 1, "maxLength": 240 },
          "body": { "type": "string", "minLength": 1, "maxLength": 20000 },
          "happenedAt": { "type": "string", "format": "date-time", "description": "An ISO 8601 instant with an offset." }
        }
      },
      "UpdateNoteRequest": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "linkedRecords": { "type": "array", "items": { "$ref": "#/components/schemas/RecordRef" }, "maxItems": 50 },
          "noteType": { "type": "string", "enum": ["NOTE", "EMAIL", "PHONE_MEETING", "VIRTUAL_MEETING", "PHYSICAL_MEETING", "SAMPLE_REQUEST", "OPPORTUNITY", "QUOTE"] },
          "title": { "type": "string", "minLength": 1, "maxLength": 240 },
          "body": { "type": "string", "minLength": 1, "maxLength": 20000 },
          "happenedAt": { "type": "string", "format": "date-time" }
        }
      },
      "NoteResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "type": { "type": "string", "const": "note" },
              "url": { "type": "string", "format": "uri" },
              "createdAt": { "type": "string", "format": "date-time" },
              "updatedAt": { "type": "string", "format": "date-time" },
              "createdBy": { "type": "object", "properties": { "type": { "type": "string", "const": "user" }, "id": { "type": "string" }, "displayName": { "type": "string" } } },
              "createdVia": { "type": ["object", "null"], "properties": { "type": { "type": "string", "const": "public_api" }, "actorUserId": { "type": "string" } } },
              "requestedVia": { "type": "object", "properties": { "type": { "type": "string", "const": "api_key" }, "id": { "type": "string" }, "purpose": { "type": ["string", "null"] } } },
              "noteType": { "type": "string" },
              "title": { "type": "string" },
              "body": { "type": "string" },
              "happenedAt": { "type": "string", "format": "date-time" },
              "primarySubject": { "$ref": "#/components/schemas/NoteSubject" },
              "linkedRecords": { "type": "array", "items": { "$ref": "#/components/schemas/RecordRef" } },
              "idempotencyStatus": { "type": "string", "enum": ["created", "replayed", "updated", "fetched"] }
            }
          }
        }
      },
      "CreateContactRequest": {
        "type": "object",
        "required": ["parent", "name"],
        "properties": {
          "parent": { "type": "object", "required": ["type", "id"], "properties": { "type": { "type": "string", "enum": ["customer", "supplier"] }, "id": { "type": "string", "format": "uuid" } } },
          "name": { "type": "string", "minLength": 1, "maxLength": 160 },
          "title": { "type": ["string", "null"], "maxLength": 160 },
          "email": { "type": ["string", "null"], "format": "email", "maxLength": 240 },
          "phone": { "type": ["string", "null"], "maxLength": 80 }
        }
      },
      "UpdateContactRequest": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 160 },
          "title": { "type": ["string", "null"], "maxLength": 160 },
          "email": { "type": ["string", "null"], "format": "email", "maxLength": 240 },
          "phone": { "type": ["string", "null"], "maxLength": 80 }
        }
      },
      "ContactResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "type": { "type": "string", "const": "contact" },
              "id": { "type": "string" },
              "contactKind": { "type": "string", "enum": ["customer", "supplier"] },
              "displayName": { "type": "string" },
              "subtitle": { "type": "string" },
              "url": { "type": "string", "format": "uri" },
              "parent": { "type": "object", "properties": { "type": { "type": "string", "enum": ["customer", "supplier"] }, "id": { "type": "string" }, "displayName": { "type": "string" } } },
              "createdAt": { "type": "string", "format": "date-time" },
              "updatedAt": { "type": "string", "format": "date-time" },
              "name": { "type": "string" },
              "title": { "type": ["string", "null"] },
              "email": { "type": ["string", "null"] },
              "phone": { "type": ["string", "null"] },
              "createdVia": { "type": ["object", "null"], "properties": { "type": { "type": "string", "const": "public_api" }, "actorUserId": { "type": "string" } } },
              "requestedVia": { "type": "object", "properties": { "type": { "type": "string", "const": "api_key" }, "id": { "type": "string" }, "purpose": { "type": ["string", "null"] } } },
              "idempotencyStatus": { "type": "string", "enum": ["created", "replayed", "updated", "fetched"] }
            }
          }
        }
      }
    }
  }
}
