openapi: 3.1.0 info: title: Crumbs API description: REST API for Crumbs by Bretzel — a self-hostable, offline-first notes app. version: 0.1.0 contact: name: Bretzel url: https://bretzel.app servers: - url: http://localhost:3000 description: Local development security: - sessionCookie: [] paths: /api/auth/setup: post: summary: First-time password setup description: Creates the single user account. Only works once — returns 400 if already set up. tags: [Auth] security: [] requestBody: required: true content: application/json: schema: type: object required: [password] properties: password: type: string minLength: 8 responses: '201': description: Account created, session cookie set content: application/json: schema: $ref: '#/components/schemas/Success' '400': description: Setup already completed or invalid password /api/auth/login: post: summary: Log in tags: [Auth] security: [] requestBody: required: true content: application/json: schema: type: object required: [password] properties: password: type: string responses: '200': description: Logged in, session cookie set content: application/json: schema: $ref: '#/components/schemas/Success' '401': description: Invalid password /api/auth/logout: post: summary: Log out tags: [Auth] responses: '200': description: Session cleared content: application/json: schema: $ref: '#/components/schemas/Success' /api/notes: get: summary: List notes tags: [Notes] parameters: - name: filter in: query schema: type: string enum: [all, archived, trashed] default: all responses: '200': description: Array of notes content: application/json: schema: type: array items: $ref: '#/components/schemas/Note' post: summary: Create a note tags: [Notes] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NoteInput' responses: '201': description: Created note content: application/json: schema: $ref: '#/components/schemas/Note' /api/notes/{id}: parameters: - name: id in: path required: true schema: type: string format: uuid get: summary: Get a note tags: [Notes] responses: '200': description: Note with tags content: application/json: schema: $ref: '#/components/schemas/Note' '404': description: Note not found patch: summary: Update a note description: Partial update — only include changed fields. tags: [Notes] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NoteInput' responses: '200': description: Updated note content: application/json: schema: $ref: '#/components/schemas/Note' '404': description: Note not found delete: summary: Permanently delete a note tags: [Notes] responses: '200': description: Deleted content: application/json: schema: $ref: '#/components/schemas/Success' '404': description: Note not found /api/notes/{id}/collaborators: parameters: - name: id in: path required: true schema: type: string format: uuid get: summary: List collaborators description: Returns all collaborators for a note. Requires access (owner or collaborator). tags: [Collaboration] responses: '200': description: Collaborator list content: application/json: schema: type: array items: $ref: '#/components/schemas/Collaborator' post: summary: Add a collaborator description: Owner-only. Adds a user as collaborator on the note. tags: [Collaboration] requestBody: required: true content: application/json: schema: type: object required: [userId] properties: userId: type: integer responses: '201': description: Collaborator added, returns updated list content: application/json: schema: type: array items: $ref: '#/components/schemas/Collaborator' '400': description: Invalid request (missing userId, owner as collaborator) '409': description: User is already a collaborator delete: summary: Remove a collaborator description: Owner can remove anyone. Collaborator can only remove self (leave). Use `userId=self` to leave. tags: [Collaboration] parameters: - name: userId in: query required: true schema: oneOf: - type: integer - type: string enum: [self] responses: '200': description: Collaborator removed content: application/json: schema: $ref: '#/components/schemas/Success' /api/notes/{id}/share: parameters: - name: id in: path required: true schema: type: string format: uuid post: summary: Create a public share link description: Owner-only. Creates a public share URL for the note. Idempotent — returns existing token if already shared. tags: [Sharing] responses: '201': description: Share link created content: application/json: schema: type: object properties: token: type: string url: type: string format: uri '404': description: Note not found or not owner delete: summary: Revoke a public share link description: Owner-only. Removes the public share link for the note. tags: [Sharing] responses: '204': description: Share link revoked '404': description: Note not found or not owner /api/shared/{token}: parameters: - name: token in: path required: true schema: type: string get: summary: Get a shared note (public) description: No authentication required. Returns the note content for a valid share token. Rate limited to 60 requests per minute per IP. tags: [Sharing] security: [] responses: '200': description: Shared note data content: application/json: schema: $ref: '#/components/schemas/SharedNoteData' '404': description: Invalid token or note is trashed '429': description: Rate limit exceeded /api/shared/{token}/attachment/{attachmentId}: parameters: - name: token in: path required: true schema: type: string - name: attachmentId in: path required: true schema: type: string get: summary: Get an attachment from a shared note (public) description: No authentication required. Serves image binary for an attachment belonging to the shared note. Rate limited. tags: [Sharing] security: [] parameters: - name: thumb in: query schema: type: string enum: ['1'] description: Set to '1' to get the thumbnail version responses: '200': description: Image binary content: image/*: schema: type: string format: binary '404': description: Invalid token, note trashed, or attachment not found '429': description: Rate limit exceeded /api/users/search: get: summary: Search users description: Search registered users by email or display name. Excludes the current user. Any authenticated user can access. tags: [Users] parameters: - name: q in: query required: true schema: type: string description: Search query (matches email or displayName) responses: '200': description: Matching users (max 10) content: application/json: schema: type: array items: type: object properties: id: type: integer displayName: type: string email: type: string /api/notes/{id}/attachments: parameters: - name: id in: path required: true schema: type: string format: uuid get: summary: List or download attachments description: Without `attachmentId`, lists all attachments. With `attachmentId`, returns the file. tags: [Attachments] parameters: - name: attachmentId in: query schema: type: string - name: thumb in: query description: Set to "1" to return the thumbnail instead of the full image (requires attachmentId) schema: type: string enum: ['1'] responses: '200': description: Attachment list (without attachmentId) or file content (with attachmentId) content: application/json: schema: type: array items: $ref: '#/components/schemas/Attachment' post: summary: Upload an image attachment tags: [Attachments] requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary description: Image file (max 10MB) thumbnail: type: string format: binary description: Pre-generated thumbnail (WebP, ~200px) responses: '201': description: Attachment metadata content: application/json: schema: $ref: '#/components/schemas/Attachment' '400': description: Invalid file delete: summary: Delete an attachment tags: [Attachments] parameters: - name: attachmentId in: query required: true schema: type: string responses: '200': description: Deleted content: application/json: schema: $ref: '#/components/schemas/Success' /api/search: get: summary: Full-text search description: Searches across note titles, content, and tags. Excludes trashed notes. tags: [Search] parameters: - name: q in: query required: true schema: type: string responses: '200': description: Matching notes content: application/json: schema: type: array items: $ref: '#/components/schemas/Note' /api/tags: get: summary: List all tags tags: [Tags] responses: '200': description: Array of tags content: application/json: schema: type: array items: $ref: '#/components/schemas/Tag' /api/sync: get: summary: Pull changes since timestamp tags: [Sync] parameters: - name: since in: query required: true schema: type: integer description: Unix timestamp in milliseconds responses: '200': description: Notes updated since the given timestamp content: application/json: schema: type: array items: $ref: '#/components/schemas/Note' post: summary: Push local changes tags: [Sync] requestBody: required: true content: application/json: schema: type: object required: [changes] properties: changes: type: array items: $ref: '#/components/schemas/SyncChange' responses: '200': description: Changes applied content: application/json: schema: $ref: '#/components/schemas/Success' '400': description: Invalid changes payload /api/settings/api-keys: get: summary: List API keys tags: [Settings] responses: '200': description: List of API keys (without hashes) content: application/json: schema: type: array items: $ref: '#/components/schemas/ApiKey' post: summary: Create API key tags: [Settings] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string responses: '201': description: API key created (key shown only once) content: application/json: schema: type: object properties: id: type: string key: type: string name: type: string keyPrefix: type: string /api/settings/api-keys/{id}: delete: summary: Revoke API key tags: [Settings] parameters: - name: id in: path required: true schema: type: string responses: '200': description: Key revoked content: application/json: schema: $ref: '#/components/schemas/Success' '404': description: Key not found /api/mcp: post: summary: MCP Streamable HTTP endpoint description: Model Context Protocol endpoint. Accepts JSON-RPC 2.0 messages. Authenticate via Bearer token (API key). tags: [MCP] security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object description: JSON-RPC 2.0 message responses: '200': description: JSON-RPC response (may be SSE stream) '400': description: Invalid session '401': description: Invalid or missing API key components: securitySchemes: sessionCookie: type: apiKey in: cookie name: session bearerAuth: type: http scheme: bearer description: API key (crumbs_...) schemas: SharedNoteData: type: object properties: title: type: string content: type: string checklistMode: type: boolean color: type: string enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk] attachments: type: array items: $ref: '#/components/schemas/Attachment' createdAt: type: string format: date-time updatedAt: type: string format: date-time Success: type: object properties: success: type: boolean Note: type: object properties: id: type: string format: uuid title: type: string content: type: string color: type: string enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk] pinned: type: boolean archived: type: boolean trashed: type: boolean trashedAt: type: string format: date-time nullable: true checklistMode: type: boolean sortOrder: type: integer createdAt: type: string format: date-time updatedAt: type: string format: date-time version: type: integer tags: type: array items: type: string attachments: type: array items: $ref: '#/components/schemas/Attachment' NoteInput: type: object properties: id: type: string format: uuid description: Optional — generated if omitted title: type: string content: type: string color: type: string enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk] pinned: type: boolean archived: type: boolean trashed: type: boolean checklistMode: type: boolean sortOrder: type: integer Tag: type: object properties: id: type: integer name: type: string Attachment: type: object properties: id: type: string noteId: type: string filename: type: string mimeType: type: string size: type: integer thumbnailPath: type: string nullable: true description: Server path to thumbnail file (null if no thumbnail) createdAt: type: string format: date-time SyncChange: type: object required: [noteId, operation, timestamp] properties: noteId: type: string format: uuid operation: type: string enum: [create, update, delete] timestamp: type: integer description: Unix timestamp in milliseconds data: $ref: '#/components/schemas/NoteInput' Collaborator: type: object properties: userId: type: integer displayName: type: string email: type: string addedAt: type: string format: date-time ApiKey: type: object properties: id: type: string format: uuid name: type: string keyPrefix: type: string description: First 12 characters of the key for display createdAt: type: string format: date-time lastUsedAt: type: string format: date-time nullable: true