{"openapi":"3.1.0","info":{"title":"Cramdeck Integrations API","version":"1.6.0","description":"Programmatic API for third-party integrations (ChatGPT custom GPTs, Claude Desktop / local MCP, scripts, and the ChatGPT plugin via OAuth) to push and read flashcard decks and record review sessions for an authenticated user. Authenticate with a Personal Access Token (PAT) or an OAuth access token. PATs are issued from the user's Integrations settings page."},"servers":[{"url":"https://localhost:3000/api/v1/integrations"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Decks","description":"Manage flashcard decks."},{"name":"Reviews","description":"Private review sessions for ChatGPT."},{"name":"Auth","description":"Identity / connection-test endpoints."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"PAT or OAuth access token","description":"Personal Access Token (`cdk_pat_<prefix>_<secret>`) for CLI / Custom GPT / local MCP, or an OAuth 2.1 access token from the ChatGPT plugin account-linking flow. Pass as `Authorization: Bearer <token>`."},"oauth2":{"type":"oauth2","description":"OAuth 2.1 authorization code + PKCE used by the ChatGPT plugin. Discovery: `/.well-known/oauth-protected-resource`.","flows":{"authorizationCode":{"authorizationUrl":"https://login.microsoftonline.com/common/oauth2/v2.0/authorize","tokenUrl":"https://login.microsoftonline.com/common/oauth2/v2.0/token","scopes":{"decks:read":"List and read owned decks","decks:write":"Create and update owned decks","reviews:read":"Read review session state","reviews:write":"Start sessions and record answers"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","example":"DECK_NOT_FOUND"},"message":{"type":"string"},"details":{}}}}},"Visibility":{"type":"string","enum":["public","unlisted","private"]},"ContentType":{"type":"string","enum":["text","markdown","html","latex"]},"SideMedia":{"type":["object","null"],"properties":{"imageUrl":{"type":["string","null"],"format":"uri","maxLength":2048,"pattern":"^(https?://|/)","description":"Must be an http(s) absolute URL or a root-relative path. javascript:/data:/vbscript: URLs are rejected."},"imageAlt":{"type":["string","null"],"maxLength":500}}},"CardType":{"type":["string","null"],"enum":["basic","formula","definition","name-formula","code-behavior","comparison","procedure","explanation","pitfall",null],"description":"Semantic card type. Drives layout family, accent colour, and the editor's per-type affordances. See /docs/integrations#card-types. The retired values `image-question` and `image-explanation` are still accepted on input for backward compatibility and normalized to `basic` and `definition` respectively; they are never returned in responses."},"CardDirection":{"type":["string","null"],"enum":["name->formula","formula->name",null]},"CardDifficulty":{"type":["string","null"],"enum":["easy","medium","hard",null]},"CardExtras":{"type":"object","additionalProperties":false,"properties":{"info":{"type":["string","null"],"maxLength":1000},"frontType":{"$ref":"#/components/schemas/ContentType"},"backType":{"$ref":"#/components/schemas/ContentType"},"contentType":{"$ref":"#/components/schemas/ContentType"},"cardType":{"$ref":"#/components/schemas/CardType"},"subject":{"type":["string","null"],"maxLength":60},"topic":{"type":["string","null"],"maxLength":80},"direction":{"$ref":"#/components/schemas/CardDirection"},"prompt":{"type":["string","null"],"maxLength":200},"difficulty":{"$ref":"#/components/schemas/CardDifficulty"},"tags":{"type":["array","null"],"items":{"type":"string","maxLength":30},"maxItems":10},"template":{"type":["string","null"],"enum":["default","math","code",null],"deprecated":true,"description":"Legacy template axis. New decks SHOULD use `cardType` instead. The server normalises legacy values (default→basic, math→formula, code→code-behavior)."},"templateData":{"type":["object","null"]}}},"CardInput":{"type":"object","required":[],"properties":{"front":{"type":["string","null"],"maxLength":4000},"back":{"type":["string","null"],"maxLength":4000},"oneSided":{"type":"boolean","default":false},"frontMedia":{"$ref":"#/components/schemas/SideMedia"},"backMedia":{"$ref":"#/components/schemas/SideMedia"},"extras":{"$ref":"#/components/schemas/CardExtras"}},"description":"At least one of `front` or `back` MUST be a non-empty string."},"CardDto":{"type":"object","required":["id","order","front","back","oneSided"],"properties":{"id":{"type":"string"},"order":{"type":"integer"},"front":{"type":["string","null"]},"back":{"type":["string","null"]},"oneSided":{"type":"boolean"},"frontMedia":{"$ref":"#/components/schemas/SideMedia"},"backMedia":{"$ref":"#/components/schemas/SideMedia"},"extras":{"$ref":"#/components/schemas/CardExtras"}}},"DeckMetaInput":{"type":"object","required":["title"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":5000},"lang":{"type":"string","default":"en","example":"en"},"visibility":{"$ref":"#/components/schemas/Visibility","default":"private"},"tags":{"type":"array","items":{"type":"string","maxLength":40},"maxItems":20,"default":[]},"thumbnail":{"type":["string","null"],"format":"uri","maxLength":2048}}},"ImportDeckJson":{"type":"object","required":["deck","cards"],"properties":{"deck":{"$ref":"#/components/schemas/DeckMetaInput"},"cards":{"type":"array","minItems":1,"maxItems":250,"items":{"$ref":"#/components/schemas/CardInput"}}}},"ImportResult":{"type":"object","required":["deckId","cardCount","mode","previewUrl","editUrl","visibility"],"properties":{"deckId":{"type":"string"},"cardCount":{"type":"integer"},"mode":{"type":"string","enum":["create","update"]},"title":{"type":"string"},"visibility":{"$ref":"#/components/schemas/Visibility"},"version":{"type":["string","null"],"format":"date-time","description":"Optimistic-concurrency token (deck updatedAt). Required for future replace operations."},"previewUrl":{"type":"string","format":"uri","description":"Absolute URL to the learning / preview view."},"editUrl":{"type":"string","format":"uri","description":"Absolute URL to the owner editor."},"idempotencyKey":{"type":"string","description":"Echo of the Idempotency-Key when one was supplied."},"warnings":{"type":"array","items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"cardIndex":{"type":"integer"}}}}}},"DeckSummary":{"type":"object","required":["id","title","visibility","cardCount","version"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"visibility":{"$ref":"#/components/schemas/Visibility"},"lang":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"cardCount":{"type":"integer"},"thumbnail":{"type":["string","null"],"format":"uri"},"version":{"type":["string","null"],"format":"date-time","description":"Optimistic-concurrency token (= updatedAt)."},"previewUrl":{"type":"string","format":"uri"},"editUrl":{"type":"string","format":"uri"},"updatedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":["string","null"],"format":"date-time"}}},"DeckDetail":{"allOf":[{"$ref":"#/components/schemas/DeckSummary"},{"type":"object","required":["cards"],"properties":{"cards":{"type":"array","items":{"$ref":"#/components/schemas/CardDto"}},"nextCardsToken":{"type":"string"}}}]},"DeckList":{"type":"object","required":["items","nextToken"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/DeckSummary"}},"nextToken":{"type":["string","null"]}}},"DeckPatch":{"type":"object","minProperties":1,"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":5000},"lang":{"type":"string"},"visibility":{"$ref":"#/components/schemas/Visibility"},"tags":{"type":"array","items":{"type":"string","maxLength":40},"maxItems":20},"thumbnail":{"type":["string","null"],"format":"uri"}}},"AppendCards":{"type":"object","required":["cards"],"properties":{"cards":{"type":"array","minItems":1,"maxItems":250,"items":{"$ref":"#/components/schemas/CardInput"}}}},"AppendCardsResult":{"type":"object","required":["deckId","addedCount","cardCount","version","cards"],"properties":{"deckId":{"type":"string"},"addedCount":{"type":"integer"},"cardCount":{"type":"integer"},"version":{"type":["string","null"],"format":"date-time"},"cards":{"type":"array","items":{"$ref":"#/components/schemas/CardDto"}},"idempotencyKey":{"type":"string"}}},"CardPatch":{"type":"object","minProperties":1,"properties":{"front":{"type":["string","null"],"maxLength":4000},"back":{"type":["string","null"],"maxLength":4000},"oneSided":{"type":"boolean"},"frontMedia":{"$ref":"#/components/schemas/SideMedia"},"backMedia":{"$ref":"#/components/schemas/SideMedia"},"extras":{"$ref":"#/components/schemas/CardExtras"}}},"WhoAmI":{"type":"object","required":["userId","scopes","source","token"],"properties":{"userId":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["decks:read","decks:write","reviews:read","reviews:write","decks:delete","public-decks:read"]}},"source":{"type":"string","enum":["pat"]},"token":{"type":"object","required":["id","name","prefix"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"prefix":{"type":"string"}}}}}},"responses":{"Unauthorized":{"description":"Missing, invalid, revoked, or expired token. Body uses the standard error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Token is valid but lacks the required scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequest":{"description":"Malformed payload or query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"60 req/min and 2000 req/day per token. Includes `Retry-After` header.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the bucket resets."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"Idempotency conflict (same key, different body) or operation in progress.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/whoami":{"get":{"tags":["Auth"],"summary":"Validate token and return principal info","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoAmI"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/decks":{"get":{"tags":["Decks"],"summary":"List decks owned by the authenticated user","security":[{"bearerAuth":["decks:read"]}],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"in":"query","name":"token","schema":{"type":"string"},"description":"Opaque continuation token from a previous call."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeckList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/decks/{deckId}":{"parameters":[{"in":"path","name":"deckId","required":true,"schema":{"type":"string"}}],"get":{"tags":["Decks"],"summary":"Read a single deck with its complete card set (up to 250 cards)","security":[{"bearerAuth":["decks:read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeckDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["Decks"],"summary":"Update deck metadata (no card edits)","security":[{"bearerAuth":["decks:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeckPatch"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeckDetail"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/decks/{deckId}/cards":{"parameters":[{"in":"path","name":"deckId","required":true,"schema":{"type":"string"}}],"post":{"tags":["Decks"],"summary":"Append cards to an owned deck (safe; does not replace existing cards)","description":"Adds new cards at the end of the deck. Unlike `import?mode=update`, this never deletes existing cards. Total cards after append must stay within the 250-card deck limit.","security":[{"bearerAuth":["decks:write"]}],"parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","minLength":8,"maxLength":128},"description":"Recommended. Retries with the same key and body return the original result; a different body yields 409 IDEMPOTENCY_CONFLICT. Required for ChatGPT / MCP clients."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppendCards"}}}},"responses":{"200":{"description":"Cards appended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppendCardsResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/decks/{deckId}/cards/{cardId}":{"parameters":[{"in":"path","name":"deckId","required":true,"schema":{"type":"string"}},{"in":"path","name":"cardId","required":true,"schema":{"type":"string"}}],"patch":{"tags":["Decks"],"summary":"Update a single card on an owned deck","security":[{"bearerAuth":["decks:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardPatch"}}}},"responses":{"200":{"description":"Updated card","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardDto"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/decks/import":{"post":{"tags":["Decks"],"summary":"Import a deck (markdown or structured JSON)","security":[{"bearerAuth":["decks:write"]}],"parameters":[{"in":"query","name":"mode","schema":{"type":"string","enum":["create","update"],"default":"create"}},{"in":"query","name":"targetDeckId","schema":{"type":"string"},"description":"Required when `mode=update`. Replaces the entire card list."},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","minLength":8,"maxLength":128},"description":"Optional for create. Retries with the same key and body return the original result; a different body yields 409 IDEMPOTENCY_CONFLICT. Recommended for ChatGPT / MCP clients."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportDeckJson"}},"text/markdown":{"schema":{"type":"string"},"example":"---\nversion: 1\nlang: en\nvisibility: private\n---\n\n# My deck\n\n---\n\n## Front\nWhat is 2+2?\n\n## Back\n4\n"}}},"responses":{"200":{"description":"Updated existing deck","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResult"}}}},"201":{"description":"Created new deck","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"415":{"description":"Use Content-Type: application/json or text/markdown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/decks/capacity":{"get":{"tags":["Decks"],"summary":"Check whether another user-owned deck may be created","description":"Returns neutral capacity fields only. Never includes plan names or upgrade information.","security":[{"bearerAuth":["decks:read"]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["can_create","current_decks","deck_limit"],"properties":{"can_create":{"type":"boolean"},"current_decks":{"type":"integer"},"deck_limit":{"type":["integer","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/reviews":{"post":{"tags":["Reviews"],"summary":"Start or resume a private review session","security":[{"bearerAuth":["reviews:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["deckId"],"properties":{"deckId":{"type":"string"},"resume":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Session started or resumed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/reviews/{sessionId}":{"parameters":[{"in":"path","name":"sessionId","required":true,"schema":{"type":"string"}}],"get":{"tags":["Reviews"],"summary":"Read a review session","security":[{"bearerAuth":["reviews:read"]}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/reviews/{sessionId}/answers":{"parameters":[{"in":"path","name":"sessionId","required":true,"schema":{"type":"string"}}],"post":{"tags":["Reviews"],"summary":"Record one review answer (idempotent)","security":[{"bearerAuth":["reviews:write"]}],"responses":{"200":{"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict"}}}},"/reviews/{sessionId}/complete":{"parameters":[{"in":"path","name":"sessionId","required":true,"schema":{"type":"string"}}],"post":{"tags":["Reviews"],"summary":"Complete a review session and return authoritative stats","security":[{"bearerAuth":["reviews:write"]}],"responses":{"200":{"description":"OK"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}