{
  "openapi": "3.1.0",
  "info": {
    "title": "NDX API",
    "summary": "Public and collector APIs for the NDX trading card collection product.",
    "description": "NDX (getndx.com) identifies trading cards, tracks collection value, and publishes a release calendar. This specification covers the public waitlist and health endpoints plus the authenticated collector API at api.getndx.com. Catalog-wide scraping is not offered. See https://getndx.com/developers.",
    "version": "1.0.0",
    "contact": {
      "name": "Marbir Digital, LLC",
      "email": "legal@getndx.com",
      "url": "https://getndx.com/contact"
    },
    "license": {
      "name": "NDX Terms of Use",
      "url": "https://getndx.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://getndx.com",
      "description": "Marketing site and waitlist"
    },
    {
      "url": "https://api.getndx.com",
      "description": "NDX consumer API"
    }
  ],
  "tags": [
    { "name": "Public", "description": "No collector session required." },
    { "name": "Auth", "description": "Mint and refresh NDX session tokens." },
    { "name": "Collector", "description": "Signed-in collector resources." }
  ],
  "paths": {
    "/api/waitlist": {
      "post": {
        "operationId": "joinWaitlist",
        "tags": ["Public"],
        "summary": "Join the NDX launch waitlist",
        "description": "Stores an email for NDX launch updates. Idempotent: a repeated email still returns ok. A filled website field is treated as a bot honeypot and is not stored.",
        "servers": [{ "url": "https://getndx.com" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WaitlistRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signup accepted or already present.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WaitlistResponse" }
              }
            }
          },
          "400": {
            "description": "Invalid email or body.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "getLiveness",
        "tags": ["Public"],
        "summary": "API liveness",
        "description": "Returns ok when the consumer API process is up. Does not check Postgres.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "responses": {
          "200": {
            "description": "Process is alive.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthStatus" }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getReadiness",
        "tags": ["Public"],
        "summary": "API readiness",
        "description": "Pings Postgres. Use this to decide whether the consumer API can serve traffic.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "responses": {
          "200": {
            "description": "Database is reachable.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthStatus" }
              }
            }
          },
          "503": {
            "description": "Database is unreachable.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthStatus" }
              }
            }
          }
        }
      }
    },
    "/v1/public/u/{userId}/views": {
      "get": {
        "operationId": "listPublicViews",
        "tags": ["Public"],
        "summary": "List a collector's public library views",
        "description": "Returns published library views for the given NDX user UUID. No authentication.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "Collector user UUID.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Published views.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/PublicViewSummary" }
                }
              }
            }
          },
          "400": {
            "description": "Invalid user id.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/public/u/{userId}/views/{slug}/query": {
      "get": {
        "operationId": "queryPublicView",
        "tags": ["Public"],
        "summary": "Query one public library view",
        "description": "Returns the cards in a published view. Unauthenticated. Paginate with limit and cursor.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "Collector user UUID.",
            "schema": { "type": "string", "format": "uuid" }
          },
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Public share slug for the view.",
            "schema": { "type": "string", "minLength": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–100. Defaults to 50.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor from a previous response.",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "View contents.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublicViewQuery" }
              }
            }
          },
          "404": {
            "description": "View not found or not public.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/auth/apple": {
      "post": {
        "operationId": "signInWithApple",
        "tags": ["Auth"],
        "summary": "Sign in with Apple",
        "description": "Exchanges an Apple identity credential for NDX session tokens. Used by the iOS app. There is no password grant.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/AppleAuthRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session minted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthSession" }
              }
            }
          },
          "401": {
            "description": "Provider token rejected.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/auth/google": {
      "post": {
        "operationId": "signInWithGoogle",
        "tags": ["Auth"],
        "summary": "Sign in with Google",
        "description": "Exchanges a Google identity credential for NDX session tokens. Used by iOS and the consumer web app.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/GoogleAuthRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session minted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthSession" }
              }
            }
          },
          "401": {
            "description": "Provider token rejected.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/auth/refresh": {
      "post": {
        "operationId": "refreshSession",
        "tags": ["Auth"],
        "summary": "Refresh an NDX session",
        "description": "Rotates a refresh token for a new access token.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RefreshRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New tokens.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AuthSession" }
              }
            }
          },
          "401": {
            "description": "Refresh token rejected.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "operationId": "getCurrentUser",
        "tags": ["Collector"],
        "summary": "Current collector profile",
        "description": "Returns the signed-in collector's profile, username, and avatar flags.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Profile.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CollectorProfile" }
              }
            }
          },
          "401": {
            "description": "Missing or expired session.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/me/portfolio": {
      "get": {
        "operationId": "getPortfolio",
        "tags": ["Collector"],
        "summary": "Collection market value",
        "description": "Returns the signed-in collector's portfolio totals derived from live comps.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Portfolio snapshot.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Portfolio" }
              }
            }
          },
          "401": {
            "description": "Missing or expired session.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/release-calendar/": {
      "get": {
        "operationId": "listReleaseCalendar",
        "tags": ["Collector"],
        "summary": "Upcoming trading card releases",
        "description": "Returns upcoming and recent set releases. Requires a signed-in collector. Filter query parameters are optional and provider-defined.",
        "servers": [{ "url": "https://api.getndx.com" }],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum releases to return.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 200 }
          }
        ],
        "responses": {
          "200": {
            "description": "Release list.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReleaseCalendarResponse" }
              }
            }
          },
          "401": {
            "description": "Missing or expired session.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "NDX session access token from /v1/auth/apple, /v1/auth/google, or /v1/auth/refresh."
      }
    },
    "schemas": {
      "WaitlistRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": { "type": "string", "format": "email", "description": "Collector email." },
          "website": { "type": "string", "description": "Honeypot. Leave empty." }
        }
      },
      "WaitlistResponse": {
        "type": "object",
        "required": ["ok"],
        "properties": {
          "ok": { "type": "boolean" },
          "error": { "type": "string" }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string" },
          "code": { "type": "string" }
        }
      },
      "HealthStatus": {
        "type": "object",
        "required": ["status"],
        "properties": {
          "status": { "type": "string", "enum": ["ok", "error"] }
        }
      },
      "PublicViewSummary": {
        "type": "object",
        "required": ["name", "shareSlug", "shareUrl", "kind", "updatedAt"],
        "properties": {
          "name": { "type": "string" },
          "description": { "type": ["string", "null"] },
          "shareSlug": { "type": "string" },
          "shareUrl": { "type": "string", "format": "uri" },
          "kind": { "type": "string", "enum": ["query", "binder"] },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "PublicViewQuery": {
        "type": "object",
        "required": ["view", "owner", "copies"],
        "properties": {
          "view": { "$ref": "#/components/schemas/PublicViewSummary" },
          "owner": { "$ref": "#/components/schemas/PublicViewOwner" },
          "copies": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PublicCopy" }
          }
        }
      },
      "PublicViewOwner": {
        "type": "object",
        "required": ["id", "username"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "username": { "type": "string" },
          "displayName": { "type": ["string", "null"] }
        }
      },
      "PublicCopy": {
        "type": "object",
        "required": ["collectionCardId", "cardId", "quantity"],
        "properties": {
          "collectionCardId": { "type": "string", "format": "uuid" },
          "cardId": { "type": "string", "format": "uuid" },
          "parallelId": { "type": ["string", "null"], "format": "uuid" },
          "gradeId": { "type": ["string", "null"], "format": "uuid" },
          "quantity": { "type": "integer", "minimum": 1 }
        }
      },
      "AppleAuthRequest": {
        "type": "object",
        "required": ["identityToken"],
        "properties": {
          "identityToken": { "type": "string", "description": "Apple identity token." },
          "authorizationCode": { "type": "string" }
        }
      },
      "GoogleAuthRequest": {
        "type": "object",
        "required": ["idToken"],
        "properties": {
          "idToken": { "type": "string", "description": "Google ID token." }
        }
      },
      "RefreshRequest": {
        "type": "object",
        "required": ["refreshToken"],
        "properties": {
          "refreshToken": { "type": "string" }
        }
      },
      "AuthSession": {
        "type": "object",
        "required": ["accessToken"],
        "properties": {
          "accessToken": { "type": "string" },
          "refreshToken": { "type": "string" },
          "expiresIn": { "type": "integer", "description": "Access token lifetime in seconds." }
        }
      },
      "CollectorProfile": {
        "type": "object",
        "required": ["id"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "username": { "type": "string" },
          "displayName": { "type": ["string", "null"] },
          "hasAvatar": { "type": "boolean" }
        }
      },
      "Portfolio": {
        "type": "object",
        "properties": {
          "marketValueCents": { "type": "integer" },
          "gainPct": { "type": ["number", "null"] }
        }
      },
      "ReleaseCalendarResponse": {
        "type": "object",
        "properties": {
          "releases": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ReleaseEntry" }
          }
        }
      },
      "ReleaseEntry": {
        "type": "object",
        "required": ["id", "name"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "releaseDate": { "type": ["string", "null"], "format": "date" }
        }
      }
    }
  }
}
