{
  "openapi": "3.1.0",
  "info": {
    "title": "Shipshape Data API",
    "version": "1.1.0",
    "summary": "Public read API for Shipshape Data, a London AI consultancy",
    "description": "Keyless, read-only API for Shipshape Data (shipshapedata.com): services, industries, case studies with real outcomes, a cursor-paginated searchable library of 169 practical guides on data and AI, and the 16-question AI readiness assessment with server-side scoring. No authentication except the bulk export (free self-serve OAuth, see https://shipshapedata.com/auth.md). Open CORS, no write endpoints. Fair use is 300 requests/minute/IP, advertised via draft RateLimit headers (429 + Retry-After beyond it). All endpoints are stateless, so every request is idempotent; send an Idempotency-Key header and it is echoed back with Idempotency-Status. Versioning and deprecation policy: the version lives in the URL (/v1); breaking changes only ever ship as a new versioned path, deprecated versions keep working for at least 6 months and respond with Deprecation and Sunset headers for their final period (nothing is deprecated today; policy at https://shipshapedata.com/developers/). MCP servers offering the same capabilities: https://shipshapedata.com/mcp (product) and https://shipshapedata.com/mcp/docs (docs); NLWeb endpoint at https://shipshapedata.com/ask; A2A agent card at https://shipshapedata.com/.well-known/agent-card.json.",
    "contact": { "name": "Shipshape Data", "email": "hello@shipshapedata.com", "url": "https://shipshapedata.com/developers/" },
    "termsOfService": "https://shipshapedata.com/terms-of-use/"
  },
  "servers": [
    { "url": "https://shipshapedata.com/api", "description": "Production" },
    { "url": "https://shipshapedata.com/api/sandbox", "description": "Sandbox: identical endpoints and data; every call is stateless and side-effect free, responses carry X-Sandbox: true" }
  ],
  "paths": {
    "/v1": {
      "get": {
        "operationId": "getApiIndex",
        "summary": "List every endpoint",
        "description": "Self-describing index of the API: endpoints, MCP servers, and documentation links.",
        "responses": {
          "200": { "description": "Endpoint index", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiIndex" } } } }
        }
      }
    },
    "/v1/services": {
      "get": {
        "operationId": "listServices",
        "summary": "List all services",
        "description": "The 13 services: AI strategy, data consultancy, AI development, data migration, and nine specialisms including MCP connections and RAG.",
        "responses": {
          "200": { "description": "All services", "content": { "application/json": { "schema": { "type": "object", "required": ["services"], "properties": { "services": { "type": "array", "items": { "$ref": "#/components/schemas/Page" } } } } } } }
        }
      }
    },
    "/v1/services/{slug}": {
      "get": {
        "operationId": "getService",
        "summary": "Get one service",
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9-]+$" }, "description": "Service slug, e.g. ai-strategy", "example": "ai-strategy" }],
        "responses": {
          "200": { "description": "The service", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Page" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/v1/industries": {
      "get": {
        "operationId": "listIndustries",
        "summary": "List industry pages",
        "responses": {
          "200": { "description": "All industries", "content": { "application/json": { "schema": { "type": "object", "required": ["industries"], "properties": { "industries": { "type": "array", "items": { "$ref": "#/components/schemas/Page" } } } } } } }
        }
      }
    },
    "/v1/case-studies": {
      "get": {
        "operationId": "listCaseStudies",
        "summary": "List case studies",
        "description": "Three published case studies with real, verifiable outcomes (Smarter Services, 1NCE, Slimstock).",
        "responses": {
          "200": { "description": "All case studies", "content": { "application/json": { "schema": { "type": "object", "required": ["caseStudies"], "properties": { "caseStudies": { "type": "array", "items": { "$ref": "#/components/schemas/Page" } } } } } } }
        }
      }
    },
    "/v1/case-studies/{slug}": {
      "get": {
        "operationId": "getCaseStudy",
        "summary": "Get one case study",
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^[a-z0-9-]+$" }, "description": "Case study slug", "example": "1nce" }],
        "responses": {
          "200": { "description": "The case study", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Page" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/v1/resources": {
      "get": {
        "operationId": "searchResources",
        "summary": "Search the resources library",
        "description": "Full-text search across 169 practical guides on data and AI, with opaque cursor pagination: follow nextCursor from each page until it is null. Each result lists the questions the guide answers; every page also serves a markdown twin at its URL plus index.md.",
        "parameters": [
          { "name": "q", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Search terms", "example": "data lineage" },
          { "name": "category", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter to one category (see /v1/resources/categories)", "example": "Data & architecture" },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 }, "description": "Page size" },
          { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Opaque continuation token from the previous page's nextCursor; omit for the first page" }
        ],
        "responses": {
          "200": { "description": "One page of matching guides", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceSearchPage" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/v1/resources/categories": {
      "get": {
        "operationId": "listResourceCategories",
        "summary": "List resource categories",
        "responses": {
          "200": { "description": "The seven categories with article counts", "content": { "application/json": { "schema": { "type": "object", "required": ["categories"], "properties": { "categories": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } } } } } } }
        }
      }
    },
    "/v1/ai-readiness/questions": {
      "get": {
        "operationId": "getAiReadinessQuestions",
        "summary": "Get the AI readiness assessment",
        "description": "16 questions across four areas (strategy, people, data, change); 15 are scored, one is unscored sizing context. Ask a user each question, then POST the answers to /v1/ai-readiness/score.",
        "responses": {
          "200": { "description": "Sections, questions, options, and band thresholds", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Assessment" } } } }
        }
      }
    },
    "/v1/ai-readiness/score": {
      "post": {
        "operationId": "scoreAiReadiness",
        "summary": "Score AI readiness answers",
        "description": "Stateless scoring with the same maths as shipshapedata.com/ai-readiness: mean of the 15 scored answers mapped to five maturity bands. Returns a 0-100 score, band guidance, per-area averages, and advice for the weakest area. Nothing is stored, so repeat calls with the same body always return the same result.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["answers"],
                "properties": {
                  "answers": {
                    "type": "array",
                    "items": { "type": "integer", "minimum": 0, "maximum": 5 },
                    "description": "Either 16 option indices (0-4, all questions in order) or 15 point values (1-5, scored questions only, in order)"
                  }
                }
              },
              "example": { "answers": [3, 2, 1, 3, 2, 3, 2, 3, 3, 2, 2, 3, 3, 3, 4, 3] }
            }
          }
        },
        "responses": {
          "200": { "description": "The scored result", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReadinessResult" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/v1/batch": {
      "post": {
        "operationId": "batchRequests",
        "summary": "Run up to 20 GET requests in one call",
        "description": "Bulk read: send a list of GET paths (with query strings) and receive each result in order. Useful for fetching services, case studies, and categories in a single round trip.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["requests"],
                "properties": {
                  "requests": {
                    "type": "array",
                    "maxItems": 20,
                    "items": { "type": "object", "required": ["path"], "properties": { "path": { "type": "string", "description": "A GET path, e.g. /v1/services or /v1/resources?q=lineage" } } }
                  }
                }
              },
              "example": { "requests": [{ "path": "/v1/services" }, { "path": "/v1/case-studies" }, { "path": "/v1/resources?q=governance&limit=3" }] }
            }
          }
        },
        "responses": {
          "200": { "description": "Per-request results in order", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BatchResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/v1/export": {
      "get": {
        "operationId": "getBulkExport",
        "summary": "The whole dataset in one call",
        "description": "Complete export: services, industries, case studies, all 169 guides, and the assessment, as one JSON document. The only authenticated endpoint: takes a free OAuth 2.0 bearer token (client credentials; self-serve registration at /oauth/register, tokens at /oauth/token, walkthrough at https://shipshapedata.com/auth.md). Without a token it returns 401 with a WWW-Authenticate header pointing at the RFC 9728 resource metadata.",
        "security": [{ "shipshapeOAuth": ["export"] }],
        "responses": {
          "200": { "description": "The complete dataset", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BulkExport" } } } },
          "401": { "description": "Missing or invalid bearer token; WWW-Authenticate carries the resource metadata URL", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/contact": {
      "get": {
        "operationId": "getContactInfo",
        "summary": "How to contact the team",
        "description": "Contact details for reaching Shipshape Data on a user's behalf. The API deliberately has no write endpoints; enquiries are by email or the website contact form.",
        "responses": {
          "200": { "description": "Contact details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Contact" } } } }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,255}$" },
        "description": "Optional client-chosen key. Every endpoint is stateless and side-effect free, so identical requests always produce identical results; the key is echoed back with an Idempotency-Status header so clients can confirm safe replay."
      }
    },
    "securitySchemes": {
      "shipshapeOAuth": {
        "type": "oauth2",
        "description": "Free, self-serve client credentials: register at /oauth/register (open, anonymous), then exchange for a bearer token. Only the bulk export needs it.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://shipshapedata.com/oauth/token",
            "scopes": { "export": "Read the complete dataset in one call" }
          }
        }
      }
    },
    "schemas": {
      "ApiIndex": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "version": { "type": "string" },
          "description": { "type": "string" },
          "openapi": { "type": "string", "format": "uri" },
          "documentation": { "type": "string", "format": "uri" },
          "agentGuide": { "type": "string", "format": "uri" },
          "mcp": { "type": "object", "properties": { "product": { "type": "string", "format": "uri" }, "docs": { "type": "string", "format": "uri" }, "serverCard": { "type": "string", "format": "uri" } } },
          "endpoints": { "type": "object", "additionalProperties": { "type": "string" } },
          "auth": { "type": "string" },
          "authDocs": { "type": "string", "format": "uri" }
        }
      },
      "Page": {
        "type": "object",
        "required": ["name", "slug", "url", "description"],
        "properties": {
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "url": { "type": "string", "format": "uri", "description": "Canonical HTML page; a markdown twin is at this URL plus index.md" },
          "description": { "type": "string" }
        }
      },
      "Resource": {
        "type": "object",
        "required": ["title", "slug", "url", "category", "description"],
        "properties": {
          "title": { "type": "string" },
          "slug": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "category": { "type": "string" },
          "description": { "type": "string" },
          "answers": { "type": "array", "items": { "type": "string" }, "description": "The questions this guide answers" },
          "relevance": { "type": "integer", "description": "Present on keyword searches: how many query terms matched" }
        }
      },
      "ResourceSearchPage": {
        "type": "object",
        "required": ["query", "count", "results"],
        "properties": {
          "query": { "type": "string" },
          "count": { "type": "integer", "description": "Results on this page" },
          "nextCursor": { "type": ["string", "null"], "description": "Opaque token for the next page; null when this is the last page" },
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/Resource" } }
        }
      },
      "Category": {
        "type": "object",
        "required": ["name", "articles"],
        "properties": { "name": { "type": "string" }, "articles": { "type": "integer" } }
      },
      "Assessment": {
        "type": "object",
        "required": ["title", "sections", "bands"],
        "properties": {
          "title": { "type": "string" },
          "instructions": { "type": "string" },
          "sections": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["title", "key", "questions"],
              "properties": {
                "title": { "type": "string" },
                "key": { "type": "string", "enum": ["strategy", "people", "data", "change"] },
                "desc": { "type": "string" },
                "questions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": ["q", "opts"],
                    "properties": {
                      "q": { "type": "string" },
                      "scored": { "type": "boolean", "description": "false = unscored sizing context" },
                      "ctx": { "type": "string" },
                      "opts": { "type": "array", "items": { "type": "string" }, "description": "Five options; the answer is the option index 0-4" }
                    }
                  }
                }
              }
            }
          },
          "bands": { "type": "array", "items": { "type": "object", "required": ["min", "name"], "properties": { "min": { "type": "number" }, "name": { "type": "string" } } } }
        }
      },
      "ReadinessResult": {
        "type": "object",
        "required": ["score", "band", "pillars", "weakestPillar"],
        "properties": {
          "score": { "type": "integer", "minimum": 0, "maximum": 100 },
          "scale": { "type": "string" },
          "average": { "type": "number", "description": "Mean of scored answers on the 1-5 scale" },
          "band": { "type": "object", "required": ["name", "guidance"], "properties": { "name": { "type": "string" }, "guidance": { "type": "array", "items": { "type": "string" } } } },
          "pillars": {
            "type": "object",
            "description": "Keyed strategy/people/data/change",
            "additionalProperties": { "type": "object", "required": ["title", "average"], "properties": { "title": { "type": "string" }, "average": { "type": "number" }, "advice": { "type": ["string", "null"] } } }
          },
          "weakestPillar": { "type": "string", "enum": ["strategy", "people", "data", "change"] },
          "scoredQuestions": { "type": "integer" },
          "interactiveVersion": { "type": "string", "format": "uri" },
          "nextStep": { "type": "string" }
        }
      },
      "BatchResponse": {
        "type": "object",
        "required": ["responses"],
        "properties": {
          "responses": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["path", "status", "body"],
              "properties": {
                "path": { "type": "string" },
                "status": { "type": "integer" },
                "body": { "type": "object", "description": "The same shape the GET endpoint returns, or an Error object on 404" }
              }
            }
          }
        }
      },
      "BulkExport": {
        "type": "object",
        "required": ["exportedAt", "services", "industries", "caseStudies", "resources", "assessment"],
        "properties": {
          "exportedAt": { "type": "string", "format": "date-time" },
          "client": { "type": "string", "description": "The client_id the token was issued to" },
          "services": { "type": "array", "items": { "$ref": "#/components/schemas/Page" } },
          "industries": { "type": "array", "items": { "$ref": "#/components/schemas/Page" } },
          "caseStudies": { "type": "array", "items": { "$ref": "#/components/schemas/Page" } },
          "resources": { "type": "array", "items": { "$ref": "#/components/schemas/Resource" } },
          "assessment": { "$ref": "#/components/schemas/Assessment" },
          "contact": { "$ref": "#/components/schemas/Contact" }
        }
      },
      "Contact": {
        "type": "object",
        "required": ["company", "email", "website"],
        "properties": {
          "company": { "type": "string" },
          "location": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "website": { "type": "string", "format": "uri" },
          "how": { "type": "string" },
          "note": { "type": "string" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "docs": { "type": "string", "format": "uri" },
              "openapi": { "type": "string", "format": "uri" }
            }
          }
        }
      }
    },
    "responses": {
      "NotFound": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "BadRequest": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  }
}
