{
	"openapi": "3.1.0",
	"info": {
		"title": "Kosu API",
		"version": "1.0.0",
		"description": "A mindful content queue. Built for AI agents to read, manage, and suggest content for a human's queue.\n\nKosu is in beta. The v1 API may change.",
		"contact": {
			"email": "matt@mattspear.co",
			"url": "https://usekosu.com/docs"
		}
	},
	"servers": [
		{
			"url": "https://usekosu.com/api/v1",
			"description": "Production"
		}
	],
	"security": [
		{
			"BearerAuth": []
		}
	],
	"paths": {
		"/items": {
			"get": {
				"operationId": "listItems",
				"summary": "List items",
				"description": "List items by state. Queue uses cursor-based pagination (stable during drag-reorder). Other states use offset-based pagination.",
				"parameters": [
					{
						"name": "state",
						"in": "query",
						"description": "Filter by item state.",
						"schema": {
							"type": "string",
							"enum": ["queue", "read", "archived", "deleted", "suggested"],
							"default": "queue"
						}
					},
					{
						"name": "limit",
						"in": "query",
						"description": "Max items to return (1-100).",
						"schema": {
							"type": "integer",
							"minimum": 1,
							"maximum": 100,
							"default": 50
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"description": "Cursor for next page. Only used when state is `queue`. Returned as `next_cursor` in previous response.",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "offset",
						"in": "query",
						"description": "Offset for pagination. Only used when state is not `queue`.",
						"schema": {
							"type": "integer",
							"minimum": 0
						}
					}
				],
				"responses": {
					"200": {
						"description": "List of items.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ItemList"
								},
								"example": {
									"object": "list",
									"data": [
										{
											"object": "item",
											"id": "itm_abc123",
											"url": "https://example.com/article",
											"title": "Example Article",
											"source": "article",
											"state": "queue",
											"channel_name": null,
											"thumbnail": null,
											"duration": null,
											"duration_minutes": null,
											"created_at": "2026-02-25T10:00:00.000Z",
											"updated_at": "2026-02-25T10:00:00.000Z",
											"opened_at": null,
											"read_at": null,
											"archived_at": null
										}
									],
									"has_more": false,
									"next_cursor": null
								}
							}
						}
					},
					"400": {
						"$ref": "#/components/responses/BadRequest"
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					}
				}
			},
			"post": {
				"operationId": "createItem",
				"summary": "Add item",
				"description": "Add a URL to the queue. Kosu deduplicates by canonical URL per user:\n- If the URL is already active in queue → returns the existing item (200).\n- If the URL was archived, read, or deleted → reactivates it at the bottom of the queue (201).\n- If the URL is new → creates a new item at the bottom of the queue (201).\n\nKosu automatically fetches metadata (title, thumbnail, source type) from the URL.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/CreateItemRequest"
							},
							"example": {
								"url": "https://example.com/article"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Item already exists in queue. Returned unchanged.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ItemWithMeta"
								}
							}
						}
					},
					"201": {
						"description": "Item created or reactivated.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ItemWithMeta"
								},
								"example": {
									"object": "item",
									"id": "itm_abc123",
									"url": "https://example.com/article",
									"title": "Example Article",
									"source": "article",
									"state": "queue",
									"channel_name": null,
									"thumbnail": null,
									"duration": null,
									"duration_minutes": null,
									"created_at": "2026-02-25T10:00:00.000Z",
									"updated_at": "2026-02-25T10:00:00.000Z",
									"opened_at": null,
									"read_at": null,
									"archived_at": null,
									"meta": {
										"created": true,
										"reactivated": false
									}
								}
							}
						}
					},
					"400": {
						"$ref": "#/components/responses/BadRequest"
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"402": {
						"$ref": "#/components/responses/QuotaExceeded"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					}
				}
			}
		},
		"/items/{id}": {
			"parameters": [
				{
					"name": "id",
					"in": "path",
					"required": true,
					"description": "Item ID (prefixed with `itm_`).",
					"schema": {
						"type": "string",
						"example": "itm_abc123"
					}
				}
			],
			"get": {
				"operationId": "getItem",
				"summary": "Get item",
				"description": "Fetch a single item by ID.",
				"responses": {
					"200": {
						"description": "The item.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Item"
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					}
				}
			},
			"patch": {
				"operationId": "updateItem",
				"summary": "Update item",
				"description": "Update an item. The request body must contain exactly one of four update types:\n\n**State transition** — Move to a new state: `read`, `archived`, `queue` (requeue), or `suggested` (Pro).\n\n**Record open** — Mark the item as opened. This is a passive touch that extends decay protection by 1 day (max once per 24h).\n\n**Reorder** — Move the item to a new position in the queue using relative positioning.\n\n**Field update** — Update metadata fields (title, source, thumbnail, etc.).",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"oneOf": [
									{
										"$ref": "#/components/schemas/StateUpdate"
									},
									{
										"$ref": "#/components/schemas/OpenedUpdate"
									},
									{
										"$ref": "#/components/schemas/PositionUpdate"
									},
									{
										"$ref": "#/components/schemas/FieldsUpdate"
									}
								]
							},
							"examples": {
								"markRead": {
									"summary": "Mark as read",
									"value": {
										"state": "read"
									}
								},
								"archive": {
									"summary": "Archive",
									"value": {
										"state": "archived"
									}
								},
								"requeue": {
									"summary": "Requeue (restore to queue)",
									"value": {
										"state": "queue"
									}
								},
								"recordOpen": {
									"summary": "Record open",
									"value": {
										"opened": true
									}
								},
								"moveToTop": {
									"summary": "Move to top of queue",
									"value": {
										"position": {
											"after": null
										}
									}
								},
								"moveAfter": {
									"summary": "Move after another item",
									"value": {
										"position": {
											"after": "itm_other123"
										}
									}
								},
								"updateTitle": {
									"summary": "Update title",
									"value": {
										"title": "Better Title"
									}
								}
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Updated item.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Item"
								}
							}
						}
					},
					"400": {
						"$ref": "#/components/responses/BadRequest"
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"402": {
						"$ref": "#/components/responses/QuotaExceeded"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					}
				}
			},
			"delete": {
				"operationId": "deleteItem",
				"summary": "Delete item",
				"description": "Soft-delete an item. The item moves to `deleted` state and can be restored within 30 days. Idempotent — deleting an already-deleted item succeeds.",
				"responses": {
					"200": {
						"description": "Item deleted.",
						"content": {
							"application/json": {
								"schema": {
									"allOf": [
										{
											"$ref": "#/components/schemas/Item"
										},
										{
											"type": "object",
											"properties": {
												"deleted": {
													"type": "boolean",
													"const": true
												}
											}
										}
									]
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					}
				}
			}
		},
		"/suggestions": {
			"get": {
				"operationId": "listSuggestions",
				"summary": "List suggestions",
				"description": "List active suggestions. Suggestions are items proposed by agents awaiting user review. Pro feature.",
				"parameters": [
					{
						"name": "limit",
						"in": "query",
						"description": "Max suggestions to return (1-100).",
						"schema": {
							"type": "integer",
							"minimum": 1,
							"maximum": 100,
							"default": 50
						}
					}
				],
				"responses": {
					"200": {
						"description": "List of suggestions.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/SuggestionList"
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					}
				}
			},
			"post": {
				"operationId": "createSuggestion",
				"summary": "Add suggestion",
				"description": "Suggest a URL for the user's review. Pro feature.\n\nIf the URL is already an active item (queue, read, or archived), returns 409. If the URL was previously suggested, updates the existing suggestion.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/CreateSuggestionRequest"
							},
							"example": {
								"url": "https://example.com/article",
								"reason": "Relevant to your interest in distributed systems"
							}
						}
					}
				},
				"responses": {
					"201": {
						"description": "Suggestion created.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Suggestion"
								}
							}
						}
					},
					"200": {
						"description": "Existing suggestion updated.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Suggestion"
								}
							}
						}
					},
					"400": {
						"$ref": "#/components/responses/BadRequest"
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"402": {
						"$ref": "#/components/responses/QuotaExceeded"
					},
					"409": {
						"description": "URL is already an active item in the queue.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"example": {
									"error": {
										"type": "invalid_request_error",
										"code": "item_already_active",
										"message": "This URL is already an active item in the queue. Remove or archive it first."
									}
								}
							}
						}
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					}
				}
			}
		},
		"/suggestions/{id}": {
			"parameters": [
				{
					"name": "id",
					"in": "path",
					"required": true,
					"description": "Suggestion ID (prefixed with `sug_`).",
					"schema": {
						"type": "string",
						"example": "sug_abc123"
					}
				}
			],
			"post": {
				"operationId": "actOnSuggestion",
				"summary": "Accept or dismiss suggestion",
				"description": "Accept a suggestion to move it into the queue, or dismiss it.\n\n- **accept**: Moves the suggestion to the bottom of the queue. Returns the item.\n- **dismiss**: Soft-deletes the suggestion.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/SuggestionAction"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Suggestion accepted (returns item) or dismissed.",
						"content": {
							"application/json": {
								"schema": {
									"oneOf": [
										{
											"$ref": "#/components/schemas/Item"
										},
										{
											"type": "object",
											"properties": {
												"object": {
													"type": "string",
													"const": "suggestion"
												},
												"id": {
													"type": "string"
												},
												"dismissed": {
													"type": "boolean",
													"const": true
												}
											},
											"required": ["object", "id", "dismissed"]
										}
									]
								}
							}
						}
					},
					"400": {
						"$ref": "#/components/responses/BadRequest"
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"402": {
						"$ref": "#/components/responses/QuotaExceeded"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					}
				}
			}
		}
	},
	"components": {
		"securitySchemes": {
			"BearerAuth": {
				"type": "http",
				"scheme": "bearer",
				"description": "API key from your Kosu settings page. Pass as `Authorization: Bearer <key>`."
			}
		},
		"schemas": {
			"Item": {
				"type": "object",
				"properties": {
					"object": {
						"type": "string",
						"const": "item"
					},
					"id": {
						"type": "string",
						"description": "Prefixed item ID.",
						"example": "itm_abc123"
					},
					"url": {
						"type": ["string", "null"],
						"description": "Canonical URL."
					},
					"title": {
						"type": ["string", "null"],
						"description": "Item title (auto-fetched or user-provided)."
					},
					"source": {
						"type": "string",
						"enum": [
							"youtube",
							"podcast",
							"paper",
							"newsletter",
							"repo",
							"article",
							"book",
							"course",
							"other"
						],
						"description": "Content type (auto-inferred or user-provided)."
					},
					"state": {
						"type": "string",
						"enum": ["queue", "suggested", "read", "archived", "deleted"],
						"description": "Current lifecycle state."
					},
					"channel_name": {
						"type": ["string", "null"],
						"description": "Creator or channel name."
					},
					"thumbnail": {
						"type": ["string", "null"],
						"description": "Thumbnail URL."
					},
					"duration": {
						"type": ["string", "null"],
						"description": "Human-readable duration (e.g. `1h 30m`, `45m`)."
					},
					"duration_minutes": {
						"type": ["integer", "null"],
						"description": "Duration in minutes."
					},
					"created_at": {
						"type": ["string", "null"],
						"format": "date-time"
					},
					"updated_at": {
						"type": ["string", "null"],
						"format": "date-time"
					},
					"opened_at": {
						"type": ["string", "null"],
						"format": "date-time",
						"description": "First time the item was opened."
					},
					"read_at": {
						"type": ["string", "null"],
						"format": "date-time"
					},
					"archived_at": {
						"type": ["string", "null"],
						"format": "date-time"
					}
				},
				"required": [
					"object",
					"id",
					"url",
					"title",
					"source",
					"state",
					"created_at",
					"updated_at"
				]
			},
			"ItemWithMeta": {
				"allOf": [
					{
						"$ref": "#/components/schemas/Item"
					},
					{
						"type": "object",
						"properties": {
							"meta": {
								"type": "object",
								"properties": {
									"created": {
										"type": "boolean",
										"description": "True if a new item was created."
									},
									"reactivated": {
										"type": "boolean",
										"description": "True if an archived/read/deleted item was restored to queue."
									}
								},
								"required": ["created", "reactivated"]
							}
						},
						"required": ["meta"]
					}
				]
			},
			"Suggestion": {
				"type": "object",
				"properties": {
					"object": {
						"type": "string",
						"const": "suggestion"
					},
					"id": {
						"type": "string",
						"description": "Prefixed suggestion ID.",
						"example": "sug_abc123"
					},
					"url": {
						"type": ["string", "null"],
						"description": "Canonical URL."
					},
					"title": {
						"type": ["string", "null"]
					},
					"source": {
						"type": "string",
						"enum": [
							"youtube",
							"podcast",
							"paper",
							"newsletter",
							"repo",
							"article",
							"book",
							"course",
							"other"
						]
					},
					"channel_name": {
						"type": ["string", "null"]
					},
					"thumbnail": {
						"type": ["string", "null"]
					},
					"duration": {
						"type": ["string", "null"]
					},
					"duration_minutes": {
						"type": ["integer", "null"]
					},
					"reason": {
						"type": ["string", "null"],
						"description": "Why this was suggested."
					},
					"created_at": {
						"type": ["string", "null"],
						"format": "date-time"
					},
					"updated_at": {
						"type": ["string", "null"],
						"format": "date-time"
					}
				},
				"required": [
					"object",
					"id",
					"url",
					"title",
					"source",
					"created_at",
					"updated_at"
				]
			},
			"ItemList": {
				"type": "object",
				"properties": {
					"object": {
						"type": "string",
						"const": "list"
					},
					"data": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Item"
						}
					},
					"has_more": {
						"type": "boolean"
					},
					"next_cursor": {
						"type": ["string", "null"],
						"description": "Pass as `cursor` query param to fetch the next page. Only present for queue state."
					}
				},
				"required": ["object", "data", "has_more", "next_cursor"]
			},
			"SuggestionList": {
				"type": "object",
				"properties": {
					"object": {
						"type": "string",
						"const": "list"
					},
					"data": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Suggestion"
						}
					},
					"has_more": {
						"type": "boolean"
					},
					"next_cursor": {
						"type": ["string", "null"]
					}
				},
				"required": ["object", "data", "has_more", "next_cursor"]
			},
			"CreateItemRequest": {
				"type": "object",
				"properties": {
					"url": {
						"type": "string",
						"description": "URL to add. Kosu will canonicalize and fetch metadata.",
						"minLength": 1,
						"maxLength": 2048
					},
					"title": {
						"type": "string",
						"description": "Override auto-fetched title.",
						"minLength": 1,
						"maxLength": 256
					},
					"source": {
						"type": "string",
						"enum": [
							"youtube",
							"podcast",
							"paper",
							"newsletter",
							"repo",
							"article",
							"book",
							"course",
							"other"
						],
						"description": "Override auto-inferred content type."
					},
					"thumbnail": {
						"type": "string",
						"description": "Override auto-fetched thumbnail URL.",
						"maxLength": 2048
					},
					"channel_name": {
						"type": "string",
						"description": "Creator or channel name.",
						"maxLength": 256
					},
					"duration_minutes": {
						"type": "integer",
						"description": "Duration in minutes.",
						"minimum": 0,
						"maximum": 99999
					}
				},
				"required": ["url"]
			},
			"CreateSuggestionRequest": {
				"type": "object",
				"properties": {
					"url": {
						"type": "string",
						"description": "URL to suggest.",
						"minLength": 1,
						"maxLength": 2048
					},
					"reason": {
						"type": "string",
						"description": "Why you're suggesting this. Shown to the user.",
						"minLength": 1,
						"maxLength": 512
					},
					"title": {
						"type": "string",
						"minLength": 1,
						"maxLength": 256
					},
					"source": {
						"type": "string",
						"enum": [
							"youtube",
							"podcast",
							"paper",
							"newsletter",
							"repo",
							"article",
							"book",
							"course",
							"other"
						]
					},
					"thumbnail": {
						"type": "string",
						"maxLength": 2048
					},
					"channel_name": {
						"type": "string",
						"maxLength": 256
					},
					"duration_minutes": {
						"type": "integer",
						"minimum": 0,
						"maximum": 99999
					}
				},
				"required": ["url"]
			},
			"StateUpdate": {
				"type": "object",
				"description": "Transition item to a new state.",
				"properties": {
					"state": {
						"type": "string",
						"enum": ["read", "archived", "queue", "suggested"],
						"description": "`read` marks as read. `archived` archives. `queue` restores to queue bottom. `suggested` moves to suggestions (Pro)."
					},
					"reason": {
						"type": "string",
						"description": "Reason for suggesting (only used when state is `suggested`).",
						"minLength": 1,
						"maxLength": 512
					}
				},
				"required": ["state"]
			},
			"OpenedUpdate": {
				"type": "object",
				"description": "Record that the item was opened. Extends decay protection by 1 day (max once per 24h).",
				"properties": {
					"opened": {
						"type": "boolean",
						"const": true
					}
				},
				"required": ["opened"]
			},
			"PositionUpdate": {
				"type": "object",
				"description": "Move item to a new position in the queue.",
				"properties": {
					"position": {
						"type": "object",
						"properties": {
							"after": {
								"type": ["string", "null"],
								"description": "Place after this item ID. `null` means top of queue."
							},
							"before": {
								"type": ["string", "null"],
								"description": "Place before this item ID. `null` means bottom of queue."
							}
						}
					}
				},
				"required": ["position"]
			},
			"FieldsUpdate": {
				"type": "object",
				"description": "Update metadata fields. At least one field required.",
				"properties": {
					"title": {
						"type": "string",
						"minLength": 1,
						"maxLength": 256
					},
					"source": {
						"type": "string",
						"enum": [
							"youtube",
							"podcast",
							"paper",
							"newsletter",
							"repo",
							"article",
							"book",
							"course",
							"other"
						]
					},
					"thumbnail": {
						"type": "string",
						"maxLength": 2048
					},
					"channel_name": {
						"type": "string",
						"maxLength": 256
					},
					"duration_minutes": {
						"type": "integer",
						"minimum": 0,
						"maximum": 99999
					}
				}
			},
			"SuggestionAction": {
				"type": "object",
				"properties": {
					"action": {
						"type": "string",
						"enum": ["accept", "dismiss"],
						"description": "`accept` moves the suggestion to the queue. `dismiss` soft-deletes it."
					}
				},
				"required": ["action"]
			},
			"Error": {
				"type": "object",
				"properties": {
					"error": {
						"type": "object",
						"properties": {
							"type": {
								"type": "string",
								"enum": [
									"invalid_request_error",
									"authentication_error",
									"rate_limit_error",
									"quota_exceeded_error",
									"not_found_error",
									"api_error"
								],
								"description": "Error category."
							},
							"code": {
								"type": "string",
								"description": "Machine-readable error code."
							},
							"message": {
								"type": "string",
								"description": "Human-readable description."
							},
							"param": {
								"type": "string",
								"description": "The parameter that caused the error, if applicable."
							}
						},
						"required": ["type", "code", "message"]
					}
				},
				"required": ["error"]
			}
		},
		"responses": {
			"BadRequest": {
				"description": "Invalid request (validation error, malformed JSON).",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"example": {
							"error": {
								"type": "invalid_request_error",
								"code": "validation_error",
								"message": "Required",
								"param": "url"
							}
						}
					}
				}
			},
			"Unauthorized": {
				"description": "Missing or invalid API key.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"example": {
							"error": {
								"type": "authentication_error",
								"code": "invalid_api_key",
								"message": "Invalid API key"
							}
						}
					}
				}
			},
			"NotFound": {
				"description": "Resource not found.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"example": {
							"error": {
								"type": "not_found_error",
								"code": "not_found",
								"message": "Item not found"
							}
						}
					}
				}
			},
			"QuotaExceeded": {
				"description": "Queue limit reached. Upgrade to Pro for unlimited items.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"example": {
							"error": {
								"type": "quota_exceeded_error",
								"code": "quota_exceeded",
								"message": "Queue limit reached"
							}
						}
					}
				}
			},
			"RateLimited": {
				"description": "Rate limit exceeded. Retry after cooldown.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"example": {
							"error": {
								"type": "rate_limit_error",
								"code": "rate_limited",
								"message": "API key rate limit exceeded"
							}
						}
					}
				}
			}
		}
	}
}
