ReadKeep/openapi.json
Ilyas Hallak ec432a037c feat: Implement OAuth 2.0 authentication with PKCE and automatic fallback
Add complete OAuth 2.0 Authorization Code Flow with PKCE as alternative
to API token authentication, with automatic server detection and graceful
fallback to classic login.

**OAuth Core (RFC 7636 PKCE):**
- PKCEGenerator: S256 challenge generation for secure code exchange
- OAuth DTOs: Client registration, token request/response models
- OAuthClient, OAuthToken, AuthenticationMethod domain models
- API.swift: registerOAuthClient() and exchangeOAuthToken() endpoints
- OAuthRepository + POAuthRepository protocol

**Browser Integration (ASWebAuthenticationSession):**
- OAuthSession: Wraps native authentication session
- OAuthFlowCoordinator: Orchestrates 5-phase OAuth flow
- readeck:// URL scheme for OAuth callback handling
- State verification for CSRF protection
- User cancellation handling

**Token Management:**
- KeychainHelper: OAuth token storage alongside API tokens
- TokenProvider: getOAuthToken(), setOAuthToken(), getAuthMethod()
- AuthenticationMethod enum to distinguish token types
- AuthRepository: loginWithOAuth(), getAuthenticationMethod()
- Endpoint persistence in both Keychain and Settings

**Server Feature Detection:**
- ServerInfo extended with features array and supportsOAuth flag
- GET /api/info endpoint integration (backward compatible)
- GetServerInfoUseCase with optional endpoint parameter

**User Profile Integration:**
- ProfileApiClient: Fetch user data via GET /api/profile
- UserProfileDto with username, email, provider information
- GetUserProfileUseCase: Extract username from profile
- Username saved and displayed for OAuth users (like classic auth)

**Automatic OAuth Flow (No User Selection):**
- OnboardingServerView: 2-phase flow (endpoint → auto-OAuth or classic)
- OAuth attempted automatically if server supports it
- Fallback to username/password on OAuth failure or unsupported
- SettingsServerViewModel: checkServerOAuthSupport(), loginWithOAuth()

**Cleanup & Refactoring:**
- Remove all #if os(iOS) && !APP_EXTENSION conditionals
- Remove LoginMethodSelectionView (no longer needed)
- Remove switchToClassicLogin() method
- Factories updated with OAuth dependencies

**Testing:**
- PKCEGeneratorTests: Verify RFC 7636 compliance
- ServerInfoTests: Feature detection and backward compatibility
- Mock implementations for all OAuth components

**Documentation:**
- docs/OAuth2-Implementation-Plan.md: Complete implementation guide
- openapi.json: Readeck API specification

**Scopes Requested:**
- bookmarks:read, bookmarks:write, profile:read

OAuth users now have full feature parity with classic authentication.
Server auto-detects OAuth support via /info endpoint. Seamless UX with
browser-based login and automatic fallback.
2025-12-19 21:56:40 +01:00

5394 lines
203 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"openapi": "3.0.0",
"info": {
"version": "1.0.0",
"title": "Readeck API",
"description": "# Introduction\n\nThe Readeck API provides REST endpoints that can be used for any purpose, should it be a mobile application, a script, you name it.\n\n## API Endpoint\n\nYou can access this API on `https://readeck.ilyashallak.de/api`.\n\nMost of your requests and responses are using JSON as the exchange format.\n\n## Test the API\n\nOn this documentation, you can test every route.\n\nIf you don't provide an API token in [Authentication](#auth), you can still test all the routes but note that the given curl examples only work with an API token.\n\n# Token Authentication\n\nIf you're writing a script for yourself, the easiest way is to [generate an API token](../profile/tokens) that you can use using the `Bearer` HTTP authorization scheme.\n\nFor example, you first request will look like:\n\n```sh\ncurl -H \"Authorization: Bearer <TOKEN>\" https://readeck.ilyashallak.de/api/profile\n```\n\nOr, in NodeJS:\n\n```js\nfetch(\"https://readeck.ilyashallak.de/api/profile\", {\n headers: {\n \"Authorization\": \"Bearer <TOKEN>\",\n },\n})\n```\n\n\n# Authentication with OAuth\n\nIf you're writing an application that requires a user to grant the application permission to access their Readeck instance, you should not ask a user to create an API Token but instead, implement the necessary OAuth flow so that your application can retrieve a token in a user friendly way.\n\n## Available Scopes\n\nAn OAuth token grants the application some permissions based on the requested scopes. This are the available scopes you can request:\n\n| Name | Description |\n| :---------------- | ------------------------------ |\n| `bookmarks:read` | Read only access to bookmarks |\n| `bookmarks:write` | Write only access to bookmarks |\n| `profile:read` | Extended profile information |\n\nYou can see which scope applies on each route of this documentation. A route without a scope (and not \"public\") is not available with an OAuth token.\n\n## Client Registration\n\nBefore you can start the authorization flow, you first need to register a client on the Readeck instance.\n\n<details>\n<summary>Client Registration Flow</summary>\n<pre role=\"img\" aria-label=\"Client Registration sequence diagram\">\n ┌──────┐ ┌────────────┐\n â”Clientâ” â”Registrationâ”\n └──┬───┘ └─────┬──────┘\n │ │\n â”Client Registration Requestâ”\n â”POST /api/oauth/client │\n │──────────────────────────>│\n │ │\n â”Client Information Responseâ”\n │<──────────────────────────│\n ┌──┴───┐ ┌─────┴──────┐\n â”Clientâ” â”Registrationâ”\n └──────┘ └────────────┘\n</pre>\n</details>\n\nReadeck implement [OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591). You can register a client by querying the [Client Creation Route](#post-/oauth/client).\n\nUpon registration, you'll receive a `client_id` that you can use in the next authorization step.\n\nUnlike more traditional client implementations, Readeck OAuth clients are ephemeral:\n\n- You **must** register a new client each time you start an authorization flow.\n- The Client is valid for 10 minutes after creation.\n\n## OAuth Authorization Code Flow\n\nThe Authorization Code Flow is used by clients to exchange an authorization code for an access token.\n\nAfter the user returns to the client via the redirect URL, the application will get the authorization code from the URL and use it to request an access token.\n\nThis flow can only be used when, on the same device, the client can:\n\n- send the user to the authorization page\n- process the redirect URL to retrieve the authorization code\n\nOn a device without a browser, a client can use the [Device Code Flow](#overview--oauth-device-code-flow).\n\n<details>\n<summary>Authorization Code Flow</summary>\n\n<pre role=\"img\" aria-label=\"Authorization Code sequence diagram\">\n ┌────┐ ┌──────┐ ┌─────────────┐ ┌───┐\n â”Userâ” â”Clientâ” â”Authorizationâ” â”APIâ”\n └─┬──┘ └──┬───┘ └──────┬──────┘ └─┬─┘\n │ │ │ │\n â”Enter instance URL┠│ │\n │─────────────────>│ │ │\n │ │ │ │\n │ │──┐ │ │\n │ │ │ Generate PKCE verifier and challenge │ │\n │ │<─┘ │ │\n │ │ │ │\n │ │ Open Authorization URL │ │\n │ │ GET /authorize?... │ │\n │ │─────────────────────────────────────────>│ │\n │ │ │ │\n │ Redirect to login/authorization prompt │ │\n │<────────────────────────────────────────────────────────────│ │\n │ │ │ │\n â”Authorize Client │ │\n â”POST /authorize?... │ │\n │────────────────────────────────────────────────────────────>│ │\n │ │ │ │\n │ │ Authorization Code │ │\n │ │<─────────────────────────────────────────│ │\n │ │ │ │\n │ │──┐ │ │\n │ │ │ Check state │ │\n │ │<─┘ │ │\n │ │ │ │\n │ â”Request Token (with code and verifier) │ │\n │ â”POST /api/oauth/token │ │\n │ │─────────────────────────────────────────>│ │\n │ │ │ │\n │ │ │──┐ │\n │ │ │ │ Check PKCE │\n │ │ │<─┘ │\n │ │ │ │\n │ │ Access Token │ │\n │ │<─────────────────────────────────────────│ │\n │ │ │ │\n │ │ Request data with Access Token │ │\n │ │─────────────────────────────────────────────────────────>│\n │ │ │ │\n │ │ Response │ │\n │ │<─────────────────────────────────────────────────────────│\n ┌─┴──┐ ┌──┴───┐ ┌──────┴──────┐ ┌─┴─┐\n â”Userâ” â”Clientâ” â”Authorizationâ” â”APIâ”\n └────┘ └──────┘ └─────────────┘ └───┘\n</pre>\n\n</details>\n\nWith a `client_id`, you can use the authorization code flow. You first need to build an authorization URL.\n\n### Authorization\n\nThe authorization URL is: `https://readeck.ilyashallak.de/authorize` and it receives the following query parameters:\n\n| Name | Description |\n| :---------------------- | :--------------------------------------------------------------------------- |\n| `client_id` | OAuth Client ID |\n| `redirect_uri` | Redirection URI (must match exactly one given during client registration) |\n| `scope` | Space separated list of [scopes](#overview--available-scopes). At least one. |\n| `code_challenge` | [PKCE](#overview--pkce) Challenge (mandatory) |\n| `code_challenge_method` | Only `S256` is allowed |\n| `state` | Optional [client state](#overview--state) |\n\nSending a state is not mandatory but strongly advised to prevent cross site request forgery.\n\n### Authorization result\n\nOnce a user grants or denies an authorization request, it will be redirected to the `redirect_uri` with the following query parameters:\n\n| Name | Description |\n| :------ | :-------------------------------------------------------------------- |\n| `code` | The authorization code that the client must pass to the token request |\n| `state` | The state as initially set by the client |\n\nIn case of error (request denied by the user or something else), the redirection contains\nthe following query parameters:\n\n| Name | Description |\n| :------------------ | :------------------------------------------------------- |\n| `error` | Error code (can be `invalid_request` or `access_denied`) |\n| `error_description` | Error description |\n| `state` | The state as initially set by the client |\n\nOnce you receive a code, you can proceed to the [Token Request](#post-/oauth/token) to eventually receive an access token that will let you use the API.\n\n### PKCE\n\nThe authorization code flow requires that you use [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) with an S256 method only (the \"plain\" method is not allowed).\n\n1. The client creates a random **verifier** and produces a SHA-256 hash that is encoded in base64 to make a **challenge**.\n2. The **challenge** is added to the authorization URL as `code_challenge` query parameter.\n3. When requesting the token, the client sends the **verifier** as `code_verifier` parameter. Then the server, that kept track of the challenge can check it matches the received verifier.\n\n**Important**: The challenge must be base64 encoded, **with URL encoding** and **without padding**.\n\n<details part=\"details\">\n<summary>Javascript example of a verifier and challenge generation</summary>\n\n```js\n// This generates a 64 character long random alphanumeric string.\nfunction generateRandomString() {\n const alphabet =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"\n let res = \"\"\n const buf = new Uint8Array(64)\n crypto.getRandomValues(buf)\n for (let i in buf) {\n res += alphabet[buf[i] % alphabet.length]\n }\n return res\n}\n\n// This hashes the verifier and encodes the hash to URL safe base64.\nasync function pkceChallengeFromVerifier(v) {\n const b = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(v))\n return btoa(String.fromCharCode(...new Uint8Array(b)))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replaceAll(\"=\", \"\")\n}\n\nconst verifier = generateRandomString()\npkceChallengeFromVerifier(verifier).then((challenge) => {\n console.log(verifier)\n console.log(challenge)\n})\n```\n\n</details>\n\n### State\n\nThe `state` parameter that the client can add to the authorization URL is for the client only. When present, it is sent back in the redirection URI that contains the authorization code. The client can keep track of it and check it matches its initial value. It is strongly recommended to use it.\n\n## OAuth Device Code Flow\n\nThe Device Code Flow is used by browserless or input-constrained devices in the device flow to exchange a previously obtained device code for an access token. An e-reader is a good candidate for using this flow.\n\n<details>\n<summary>Device Code Flow</summary>\n<pre role=\"img\" aria-label=\"Device Code sequence diagram\">\n ┌────┐ ┌──────┐ ┌─────────────┐\n â”Userâ” â”Clientâ” â”Authorizationâ”\n └─┬──┘ └──┬───┘ └──────┬──────┘\n │ │ │\n │ │(1) Request device code │\n │ │───────────────────────────────────>│\n │ │ │\n │ │(2) Return device code, user code, │\n │ â”URL and interval │\n │ │<───────────────────────────────────│\n │ │ │\n │(3) Provide user code┠│\n │ and URL to user │ │\n │ <───────────────────│ │\n │ ┌────┐───────────────────────────────────┐\n │ â”Loop┠│ │\n │ └────┘ │ │\n │ │ │ │ │\n │ │ │(4) Poll for authorization │ │\n │ │ │───────────────────────────────────>│ │\n │ │ │ │ │\n │ │ │ authorization_pending┠│\n │ │ │<───────────────────────────────────│ │\n │ │ │ │ │\n │ └────────────────────────────────────────┘\n │ │ │\n │(5) Open authorization URL and enter user code │\n ├ ────────────────────────────────────────────────────────>│\n │ │ │\n │(5) Approve client access │\n ├ ────────────────────────────────────────────────────────>│\n │ │ │\n │ │ (6) Return access_tokenâ”\n │ │<───────────────────────────────────│\n │ │ │\n ┌─┴──┐ ┌──┴───┐ ┌──────┴──────┐\n â”Userâ” â”Clientâ” â”Authorizationâ”\n └────┘ └──────┘ └─────────────┘\n</pre>\n</details>\n\n1. The client request access from Readeck on the [Device Authorization route](#post-/oauth/device)\n2. Readeck issues a device code, an end-user code and provides the end-user verification URI. This information is valid for 5 minutes.\n3. The client instructs the user to visit the provided end-user verification URI. The client provides the user with the end-user code to enter in order to review the authorization request.\n4. While the user reviews the client's request (step 5), the client repeatedly polls the [Token route](#post-/oauth/token) to find out if the user completed the user authorization step. The client includes the device code and its client identifier. The token route can only be polled every 5 seconds.\n5. After authentication, Readeck prompts the user to input the user code provided by the device client and prompts the user to accept or decline the request.\n6. Readeck validates the device code provided by the client and responds with the access token if the client is granted access, an error if they are denied access, or a pending state, indicating that the client should continue to poll.\n\n<details>\n<summary>Python example of the device flow</summary>\n\n```python\nimport json\nimport time\n\nimport httpx\n\n\ndef main():\n client = httpx.Client(\n base_url=\"https://readeck.ilyashallak.de\",\n headers={\"Accept\": \"application/json\"},\n )\n\n # Create a client\n rsp = client.post(\n \"api/oauth/client\",\n data={\n \"client_name\": \"Test App\",\n \"client_uri\": \"https://example.net/\",\n \"software_id\": uuid.uuid4(),\n \"software_version\": \"1.0.2\",\n \"grant_types\": [\"urn:ietf:params:oauth:grant-type:device_code\"],\n },\n )\n rsp.raise_for_status()\n client_id = rsp.json()[\"client_id\"]\n\n # Get user code.\n rsp = client.post(\n \"api/oauth/device\",\n data={\n \"client_id\": client_id,\n \"scope\": \"bookmarks:read bookmarks:write\",\n },\n )\n rsp.raise_for_status()\n\n req_data = rsp.json()\n\n # The client keeps the device code for itself.\n device_code = req_data[\"device_code\"]\n\n # User code with a separator for better readability\n user_code = f\"{req_data['user_code'][0:4]}-{req_data['user_code'][4:]}\"\n\n # Refresh interval\n interval = req_data[\"interval\"]\n\n # Information the client must provide the user with.\n print(f\"CODE : {user_code}\")\n print(f\"URL : {req_data['verification_uri']}\")\n print(f\"COMPLETE URL : {req_data['verification_uri_complete']}\")\n\n # Now, the client waits for the user to accept or deny\n # the authorization request.\n wait = 0\n while True:\n if wait > 0:\n # wait before the request so we can use continue in the loop\n time.sleep(wait)\n else:\n wait = interval\n\n rsp = client.post(\n \"api/oauth/token\",\n data={\n \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n \"client_id\": client_id,\n \"device_code\": device_code,\n },\n )\n if rsp.status_code >= 500:\n rsp.raise_for_status()\n\n data = rsp.json()\n\n if data.get(\"access_token\"):\n print(\"Token retrieved!\")\n print(json.dumps(data, indent=2))\n return\n\n error = data.get(\"error\")\n match error:\n case \"access_denied\":\n # The user denied the request\n print(\"Access was denied\")\n return\n case \"slow_down\":\n # Server asks to slow down, we'll sleep 5s\n continue\n case \"authorization_pending\":\n # Still waiting\n print(\"Waiting for authorization...\")\n continue\n case \"expired_token\":\n # The request has expired\n print(\"Request has expired\")\n return\n case _:\n print(f\"Fatal error: {error}\")\n return\n\n\nif __name__ == \"__main__\":\n main()\n```\n\n</details>\n\n\n"
},
"servers": [
{
"url": "https://readeck.ilyashallak.de/api"
}
],
"components": {
"securitySchemes": {
"bearer": {
"type": "http",
"scheme": "Bearer"
}
},
"schemas": {
"message": {
"properties": {
"status": {
"type": "integer",
"description": "HTTP Status Code"
},
"message": {
"type": "string",
"description": "Information or error message"
}
}
},
"oauthError": {
"properties": {
"error": {
"type": "string",
"enum": [
"access_denied",
"authorization_pending",
"expired_token",
"invalid_client",
"invalid_client_metadata",
"invalid_grant",
"invalid_redirect_uri",
"invalid_request",
"invalid_scope",
"server_error",
"slow_down",
"unauthorized_client"
]
},
"error_description": {
"type": "string",
"description": "Error description, if any"
}
}
},
"oauthClientCreate": {
"required": [
"client_name",
"client_uri",
"software_id",
"software_version"
],
"properties": {
"client_name": {
"type": "string",
"description": "The client's name"
},
"client_uri": {
"type": "string",
"format": "uri",
"description": "An URL where client information can be found. HTTPS only.\n\nThe URL must resolve to a non local and non private IP address.\n"
},
"logo_uri": {
"type": "string",
"format": "uri",
"pattern": "^data:image/png;base64,[A-Za-z0-9+/]+$",
"maxLength": 8192,
"description": "An URL for the client's logo.\n\nOnly data URI of a base64 PNG encoded image is allowed.\n"
},
"software_id": {
"type": "string",
"description": "A unique identifier for the application, for example a UUID"
},
"software_version": {
"type": "string",
"description": "Version of the client"
},
"redirect_uris": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "A list of possible redirection URIs. URIs must be one of:\n\n- any https URI\n- http URI for loopback IP address (`127.0.0.0/8` or `[::1]`)\n- any other app link scheme (ie `my-app.org:/callback`)\n\nThis field is required when `grant_types` contains `authorization_code`.\n"
},
"token_endpoint_auth_method": {
"type": "string",
"description": "Client supported auth method",
"enum": [
"none"
],
"default": "none"
},
"grant_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"authorization_code",
"urn:ietf:params:oauth:grant-type:device_code"
],
"default": [
"authorization_code",
"urn:ietf:params:oauth:grant-type:device_code"
]
},
"description": "Client supported grant types"
},
"response_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"code"
],
"default": "code"
},
"description": "Client supported response type"
}
}
},
"oauthClientUpdate": {
"required": [
"client_id",
"client_name",
"client_uri",
"software_id",
"software_version",
"redirect_uris"
],
"properties": {
"client_id": {
"type": "string",
"description": "Client's ID"
},
"client_name": {
"type": "string",
"description": "The client's name"
},
"client_uri": {
"type": "string",
"format": "uri",
"description": "An URL where client information can be found. HTTPS only.\n\nThe URL must resolve to a non local and non private IP address.\n"
},
"logo_uri": {
"type": "string",
"format": "uri",
"pattern": "^data:image/png;base64,[A-Za-z0-9+/]+$",
"maxLength": 8192,
"description": "An URL for the client's logo.\n\nOnly data URI of a base64 PNG encoded image is allowed.\n"
},
"software_id": {
"type": "string",
"description": "A unique identifier for the application, for example a UUID"
},
"software_version": {
"type": "string",
"description": "Version of the client"
},
"redirect_uris": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "A list of possible redirection URIs. URIs must be one of:\n\n- any https URI\n- http URI for loopback IP address (`127.0.0.0/8` or `[::1]`)\n- any other app link scheme (ie `my-app.org:/callback`)\n\nThis field is required when `grant_types` contains `authorization_code`.\n"
},
"token_endpoint_auth_method": {
"type": "string",
"description": "Client supported auth method",
"enum": [
"none"
],
"default": "none"
},
"grant_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"authorization_code",
"urn:ietf:params:oauth:grant-type:device_code"
],
"default": [
"authorization_code",
"urn:ietf:params:oauth:grant-type:device_code"
]
},
"description": "Client supported grant types"
},
"response_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"code"
],
"default": "code"
},
"description": "Client supported response type"
}
}
},
"oauthClientResponse": {
"properties": {
"client_id": {
"type": "string",
"description": "Client ID"
},
"client_name": {
"type": "string",
"description": "Client's name"
},
"client_uri": {
"type": "string",
"format": "uri",
"description": "Client's website"
},
"logo_uri": {
"type": "string",
"format": "uri",
"description": "Client's logo"
},
"redirect_uris": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "List of available redirect URIs."
},
"software_id": {
"type": "string",
"description": "Client's unique software ID"
},
"software_version": {
"type": "string",
"description": "Client's version"
},
"token_endpoint_auth_method": {
"type": "string",
"description": "Client supported auth method",
"example": "none"
},
"grant_types": {
"type": "string",
"description": "Client supported grant types",
"example": "authorization_code"
},
"response_types": {
"type": "string",
"description": "Client supported response type",
"example": "code"
}
}
},
"oauthTokenCreate": {
"required": [
"grant_type",
"code",
"code_verifier"
],
"oneOf": [
{
"properties": {
"grant_type": {
"const": "authorization_code",
"description": "Authorization Code grant type"
},
"code": {
"type": "string",
"description": "The code as received in the authorization redirection"
},
"code_verifier": {
"type": "string",
"description": "The PKCE Verifier"
}
}
},
{
"properties": {
"grant_type": {
"const": "urn:ietf:params:oauth:grant-type:device_code",
"description": "Device Code grant type"
},
"device_code": {
"type": "string",
"description": "The device code"
},
"client_id": {
"type": "string",
"description": "The client ID"
}
}
}
]
},
"oauthTokenResponse": {
"properties": {
"id": {
"type": "string",
"description": "Token ID"
},
"access_token": {
"type": "string",
"description": "Token"
},
"token_type": {
"type": "string",
"description": "Token Type",
"enum": [
"Bearer"
]
},
"scope": {
"type": "string",
"description": "Space separated scope list"
}
}
},
"oauthDeviceCreate": {
"properties": {
"client_id": {
"type": "string",
"description": "The client ID"
},
"scope": {
"type": "string",
"description": "Space separated list of scopes granted to the device"
}
}
},
"oauthDevice": {
"properties": {
"device_code": {
"type": "string",
"description": "The device verification code"
},
"user_code": {
"type": "string",
"description": "The end-user verification code"
},
"verification_uri": {
"type": "string",
"description": "The end-user verification URI"
},
"verification_uri_complete": {
"type": "string",
"description": "The end-user verification URI that includes the `user_code`"
},
"expires_in": {
"type": "number",
"description": "The lifetime in seconds of the `device_code` and `user_code`"
},
"interval": {
"type": "number",
"description": "The minimum amount of time in seconds that the client must wait\nbetween polling requests to the token endpoint"
}
}
},
"bookmarkSummary": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "short-uid",
"description": "Bookmark's ID"
},
"href": {
"type": "string",
"format": "uri",
"description": "Link to the bookmark info"
},
"created": {
"type": "string",
"format": "date-time",
"description": "Creation date"
},
"updated": {
"type": "string",
"format": "date-time",
"description": "Last update"
},
"state": {
"type": "integer",
"enum": [
0,
1,
2
],
"description": "Indicates the state of the bookmark.\n- `0`: loaded\n- `1`: error\n- `2`: loading\n"
},
"loaded": {
"type": "boolean",
"description": "Becomes true when the bookmark is ready (regardless of its error state)"
},
"url": {
"type": "string",
"format": "uri",
"description": "Bookmark's original URL"
},
"title": {
"type": "string",
"description": "Bookmark's title"
},
"site_name": {
"type": "string",
"description": "Bookmark's site name"
},
"site": {
"type": "string",
"format": "hostname",
"description": "Bookmark's site host name"
},
"published": {
"type": [
"string"
],
"format": "date-time",
"nullable": true,
"description": "Publication date. Can be `null` when unknown."
},
"authors": {
"type": "array",
"items": {
"type": "string"
},
"description": "Author list"
},
"lang": {
"type": "string",
"description": "Language Code"
},
"text_direction": {
"type": "string",
"enum": [
"rtl",
"ltr"
],
"description": "Direction of the article's text. It can be empty when it's unknown.\n"
},
"document_type": {
"type": "string",
"description": "The bookmark document type. This is usualy the same value as `type` but it can differ\ndepending on the extraction process.\n"
},
"type": {
"type": "string",
"enum": [
"article",
"photo",
"video"
],
"description": "The bookmark type. Unlike `document_type`, this can only be one of the 3 values.\n"
},
"has_article": {
"type": "boolean",
"description": "Indicates whether the bookmarks contains an article. Please not that\nthere can be an article on any type.\n"
},
"description": {
"type": "string",
"description": "Bookmark's short description, when it exists. It's always an unformatted text.\n"
},
"is_deleted": {
"type": "boolean",
"description": "`true` when the bookmark is scheduled for deletion.\n"
},
"is_marked": {
"type": "boolean",
"description": "`true` when the bookmark is in the favorites.\n"
},
"is_archived": {
"type": "boolean",
"description": "`true` when the bookmark is in the archives.\n"
},
"read_progress": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "Reading progress percentage."
},
"labels": {
"type": "array",
"items": {
"type": "string"
},
"description": "Bookmark's labels"
},
"word_count": {
"type": "integer",
"minimum": 0,
"description": "Number of words in the article, used to compute the reading time."
},
"reading_time": {
"type": "integer",
"minimum": 0,
"description": "Duration of the article, in minutes. Either the actual duration for a\nvideo or a reading time based on the word count.\n"
},
"resources": {
"type": "object",
"description": "This contains a list of resources associated with the bookmark.\nThe only fields that are always present are `log` and `props`.\nThe `article` field is only present when a bookmark provides a\ntext content. Other image fields depend on what was found during\nextraction.\n",
"properties": {
"article": {
"$ref": "#/components/schemas/bookmarkResource",
"description": "Link to the article, when there is one."
},
"icon": {
"$ref": "#/components/schemas/bookmarkResourceImage",
"description": "Link and information for the site icon."
},
"image": {
"$ref": "#/components/schemas/bookmarkResourceImage",
"description": "Link and information for the article image."
},
"thumbnail": {
"$ref": "#/components/schemas/bookmarkResourceImage",
"description": "Link and information for the article thumbnail."
},
"log": {
"$ref": "#/components/schemas/bookmarkResource",
"description": "Link to the extraction log."
},
"props": {
"$ref": "#/components/schemas/bookmarkResource",
"description": "Link to the bookmark's extra properties."
}
}
}
}
},
"bookmarkResource": {
"type": "object",
"properties": {
"src": {
"type": "string",
"format": "uri",
"description": "URL of the resource"
}
}
},
"bookmarkResourceImage": {
"allOf": [
{
"$ref": "#/components/schemas/bookmarkResource"
},
{
"type": "object",
"properties": {
"height": {
"type": "integer",
"description": "Image height"
},
"width": {
"type": "integer",
"description": "Image width"
}
}
}
]
},
"bookmarkSyncList": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "short-uid",
"description": "Bookmark's ID"
},
"time": {
"type": "string",
"format": "date-time",
"description": "Last update date and time"
},
"type": {
"type": "string",
"description": "Update type",
"enum": [
"update",
"delete"
]
}
}
},
"bookmarkSyncParams": {
"type": "object",
"properties": {
"id": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of bookmark ID"
},
"sort": {
"type": "array",
"items": {
"type": "string",
"enum": [
"created",
"-created",
"updated",
"-updated"
]
},
"description": "Sorting parameters\n\nDefaults to `[updated]`\n"
},
"with_json": {
"type": "boolean",
"default": false,
"description": "Include a JSON payload for each returned bookmark.",
"examples": [
true
]
},
"with_html": {
"type": "boolean",
"default": false,
"description": "Include the HTML article for each returned bookmark."
},
"with_markdown": {
"type": "boolean",
"default": false,
"description": "Include the bookmark converted to Markdown."
},
"with_resources": {
"type": "boolean",
"default": false
},
"resource_prefix": {
"type": "string",
"default": ".",
"description": "A prefix added to resource URLs in HTML parts.\n\nEach image link in the HTML content will have this prefix added. To include the\nbookmark ID, use the `%` placeholder.\n\n**Note**: an empty prefix value will render raw, absolute URLs to each resource\non Readeck. The default value is `.` (direct relative prefix).\n",
"examples": [
"%/img",
"images"
]
}
}
},
"bookmarkInfo": {
"allOf": [
{
"$ref": "#/components/schemas/bookmarkSummary"
},
{
"type": "object",
"properties": {
"omit_description": {
"type": "boolean",
"description": "`true` when the description was found at the content's beginning\nand can be hidden.\n"
},
"read_anchor": {
"type": "string",
"description": "CSS selector of the last seen element."
},
"links": {
"description": "This contains the list of all the links collected in the\nretrieved article.\n",
"type": "array",
"items": {
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "Link URI"
},
"domain": {
"type": "string",
"format": "hostname",
"description": "Link's domain"
},
"title": {
"type": "string",
"description": "Link's title"
},
"is_page": {
"type": "boolean",
"description": "`true` when the destination is a web page\n"
},
"content_type": {
"type": "string",
"description": "MIME type of the destination"
}
}
}
}
}
}
]
},
"bookmarkCreate": {
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "URL to fetch"
},
"title": {
"type": "string",
"description": "Title of the bookmark"
},
"labels": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of labels to set to the bookmark"
},
"created": {
"type": "string",
"format": "date-time",
"description": "Creation date\n\nThis is optional and defaults to the current time.\n"
}
}
},
"bookmarkUpdate": {
"properties": {
"title": {
"type": "string",
"description": "New bookmark's title"
},
"is_marked": {
"type": "boolean",
"description": "Favortie state"
},
"is_archived": {
"type": "boolean",
"description": "Archive state"
},
"is_deleted": {
"type": "boolean",
"description": "If `true`, schedules the bookmark for deletion, otherwise, cancels any scheduled deletion\n"
},
"read_progress": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "Reading progress percentage"
},
"read_anchor": {
"type": "string",
"description": "CSS selector of the last seen element"
},
"labels": {
"type": "array",
"items": {
"type": "string"
},
"description": "Replaces the bookmark's labels"
},
"add_labels": {
"items": {
"type": "string"
},
"description": "Add the given labels to the bookmark"
},
"remove_labels": {
"items": {
"type": "string"
},
"description": "Remove the given labels from the bookmark"
}
}
},
"bookmarkUpdated": {
"required": [
"href",
"id",
"updated"
],
"properties": {
"href": {
"type": "string",
"format": "uri",
"description": "Bookmark URI"
},
"id": {
"type": "string",
"format": "short-uid",
"description": "Bookmark's ID"
},
"updated": {
"type": "string",
"format": "date-time",
"description": "Last update"
},
"title": {
"type": "string",
"description": "Bookmark Title"
},
"is_marked": {
"type": "string",
"description": "Favorite status"
},
"is_archived": {
"type": "string",
"description": "Archive status"
},
"is_deleted": {
"type": "string",
"description": "Scheduled deletion status"
},
"read_progress": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "Reading progress percentage"
},
"read_anchor": {
"type": "string",
"description": "CSS selector of the last seen element"
},
"labels": {
"type": "string",
"description": "New label list"
}
}
},
"bookmarkShareLink": {
"properties": {
"url": {
"type": "string",
"description": "Public URL"
},
"expires": {
"type": "string",
"format": "date-time",
"description": "Expiration date"
},
"title": {
"type": "string",
"description": "Bookmark title"
},
"id": {
"type": "string",
"description": "Bookmark ID"
}
}
},
"bookmarkShareEmail": {
"properties": {
"email": {
"type": "string",
"format": "email"
},
"format": {
"type": "string",
"enum": [
"html",
"epub"
]
}
},
"example": {
"email": "alice@localhost",
"format": "html"
}
},
"labelInfo": {
"properties": {
"name": {
"type": "string",
"description": "Label's name"
},
"count": {
"type": "integer",
"description": "Number of bookmarks with this label"
},
"href": {
"type": "string",
"format": "uri",
"description": "Link to the label info"
},
"href_bookmarks": {
"type": "string",
"format": "uri",
"description": "Link to the bookmarks with this label"
}
}
},
"labelUpdate": {
"properties": {
"name": {
"type": "string",
"description": "New label"
}
}
},
"annotationSummary": {
"properties": {
"id": {
"type": "string",
"format": "short-uid",
"description": "Highlight ID"
},
"href": {
"type": "string",
"format": "uri",
"description": "Link to the highlight"
},
"text": {
"type": "string",
"description": "Highlighted text"
},
"created": {
"type": "string",
"format": "date-time",
"description": "Highlight creation date"
},
"bookmark_id": {
"type": "string",
"format": "short-uid",
"description": "Bookmark ID"
},
"bookmark_href": {
"type": "string",
"format": "uri",
"description": "Link to the bookmark information"
},
"bookmark_url": {
"type": "string",
"format": "uri",
"description": "Original bookmark's URL"
},
"bookmark_title": {
"type": "string",
"description": "Title of the bookmark"
},
"bookmark_site_name": {
"type": "string",
"description": "Bookmark's site name"
}
}
},
"annotationInfo": {
"properties": {
"id": {
"type": "string",
"format": "short-uid",
"description": "Highlight ID"
},
"start_selector": {
"type": "string",
"description": "Start element's XPath selector"
},
"start_offset": {
"type": "integer",
"description": "Start element's text offset"
},
"end_selector": {
"type": "string",
"description": "End element's XPath selector"
},
"end_offset": {
"type": "integer",
"description": "End element's text offset"
},
"created": {
"type": "string",
"format": "date-time",
"description": "Highlight creation date"
},
"text": {
"type": "string",
"description": "Highlighted text"
}
}
},
"annotationCreate": {
"required": [
"start_selector",
"start_offset",
"end_selector",
"end_offset",
"color"
],
"properties": {
"start_selector": {
"type": "string",
"description": "Start element's XPath selector"
},
"start_offset": {
"type": "integer",
"description": "Start element's text offset"
},
"end_selector": {
"type": "string",
"description": "End element's XPath selector"
},
"end_offset": {
"type": "integer",
"description": "End element's text offset"
},
"color": {
"type": "color",
"description": "Annotation color"
}
}
},
"annotationUpdate": {
"required": [
"color"
],
"properties": {
"color": {
"type": "color",
"description": "Annotation color"
}
}
},
"collectionSummary": {
"properties": {
"updated": {
"type": "string",
"format": "date-time",
"description": "Last update date"
},
"name": {
"type": "string",
"description": "Collection's name"
},
"is_pinned": {
"type": "boolean",
"description": "`true` when the collection is pinned\n"
},
"is_deleted": {
"type": "boolean",
"description": "Collection is scheduled for deletion"
},
"search": {
"type": "string",
"description": "Search string"
},
"title": {
"type": "string",
"description": "Title filter"
},
"author": {
"type": "string",
"description": "Author filter"
},
"site": {
"type": "string",
"description": "Site (name, host or domain) filter"
},
"type": {
"type": "array",
"items": {
"type": "string",
"enum": [
"article",
"photo",
"video"
]
},
"description": "Type filter"
},
"labels": {
"type": "string",
"description": "Label filter"
},
"read_status": {
"type": "array",
"items": {
"type": "string",
"enum": [
"unread",
"reading",
"read"
]
},
"description": "Read progress status"
},
"is_marked": {
"type": "boolean",
"description": "Favorite filter"
},
"is_archived": {
"type": "boolean",
"description": "Archive filter"
},
"range_start": {
"type": "string",
"description": "From date filter"
},
"range_end": {
"type": "string",
"description": "To date filter"
}
}
},
"collectionInfo": {
"allOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "short-uid",
"description": "Collection ID"
},
"href": {
"type": "string",
"format": "uri",
"description": "Collection URL"
},
"created": {
"type": "string",
"format": "date-time",
"description": "Creation date"
}
}
},
{
"$ref": "#/components/schemas/collectionSummary"
}
]
},
"collectionCreate": {
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "Collection's name"
},
"is_pinned": {
"type": "boolean",
"description": "`true` when the collection is pinned\n"
},
"is_deleted": {
"type": "boolean",
"description": "Collection is scheduled for deletion"
},
"search": {
"type": "string",
"description": "Search string"
},
"title": {
"type": "string",
"description": "Title filter"
},
"author": {
"type": "string",
"description": "Author filter"
},
"site": {
"type": "string",
"description": "Site (name, host or domain) filter"
},
"type": {
"type": "array",
"items": {
"type": "string",
"enum": [
"article",
"photo",
"video"
]
},
"description": "Type filter"
},
"labels": {
"type": "string",
"description": "Label filter"
},
"read_status": {
"type": "array",
"items": {
"type": "string",
"enum": [
"unread",
"reading",
"read"
]
},
"description": "Read progress status"
},
"is_marked": {
"type": "boolean",
"description": "Favorite filter"
},
"is_archived": {
"type": "boolean",
"description": "Archive filter"
},
"range_start": {
"type": "string",
"description": "From date filter"
},
"range_end": {
"type": "string",
"description": "To date filter"
}
}
},
"collectionUpdate": {
"allOf": [
{
"$ref": "#/components/schemas/collectionCreate"
}
]
},
"baseImport": {
"properties": {
"label": {
"type": "string",
"description": "Label to add to all imported bookmarks."
},
"ignore_duplicates": {
"type": "boolean",
"default": true,
"description": "Ignore links that already exist."
},
"archive": {
"type": "boolean",
"default": false,
"description": "Moved imported bookmarks to archive."
},
"mark_read": {
"type": "boolean",
"default": false,
"description": "Mark imported bookmarks as read."
}
}
},
"wallabagImport": {
"allOf": [
{
"$ref": "#components/schemas/baseImport"
}
],
"required": [
"url",
"username",
"password",
"client_id",
"client_secret"
],
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "Your Wallabag instance's URL"
},
"username": {
"type": "string",
"description": "Your Wallabag username"
},
"password": {
"type": "string",
"description": "Your Wallabag password"
},
"client_id": {
"type": "string",
"description": "API Client ID"
},
"client_secret": {
"type": "string",
"description": "API Client Secret"
}
}
},
"authenticationForm": {
"type": "object",
"required": [
"username",
"password",
"application"
],
"properties": {
"username": {
"type": "string",
"description": "Username"
},
"password": {
"type": "string",
"description": "Password"
},
"application": {
"type": "string",
"description": "Application name. This can be anything."
},
"roles": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of roles to restrict the new token access."
}
},
"example": {
"username": "alice",
"password": "1234",
"application": "api doc"
}
},
"authenticationResult": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Token ID"
},
"token": {
"type": "string",
"description": "Authentication token. This is the value you must store in your application."
}
},
"example": {
"id": "RFutYEAVM95DUDLUDnhbQm",
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJqdxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
},
"userProfile": {
"type": "object",
"properties": {
"provider": {
"description": "Authentication provider information",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "short-uid",
"description": "Authentication provider ID (ie. the token ID)"
},
"name": {
"type": "string",
"description": "Provider name"
},
"application": {
"type": "string",
"description": "The registered application name"
},
"roles": {
"type": "array",
"items": {
"type": "string"
},
"description": "Roles granted for this session"
},
"permissions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Permissions granted for this session"
}
}
},
"user": {
"description": "User information",
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "Username"
},
"email": {
"type": "string",
"format": "email",
"description": "User email"
},
"created": {
"type": "string",
"format": "date-time",
"description": "Creation date"
},
"updated": {
"type": "string",
"format": "date-time",
"description": "Last update date"
},
"settings": {
"description": "User settings",
"type": "object",
"properties": {
"debug_info": {
"type": "boolean",
"description": "Enable debug information"
},
"reader_settings": {
"description": "Reader settings",
"type": "object",
"properties": {
"font": {
"type": "string"
},
"font_size": {
"type": "integer"
},
"line_height": {
"type": "integer"
}
}
}
}
}
}
}
},
"example": {
"provider": {
"name": "bearer token",
"id": "X4bmnMRcnDhQtu5y33qzTp",
"application": "internal",
"roles": [
"bookmarks:read",
"bookmarks:write"
],
"permissions": [
"api:bookmarks:collections:read",
"api:bookmarks:collections:write",
"api:bookmarks:export",
"api:bookmarks:read",
"api:bookmarks:write",
"api:opds:read",
"api:profile:read",
"api:profile:tokens:delete"
]
},
"user": {
"username": "alice",
"email": "alice@localhost",
"created": "2023-08-27T13:32:11.704606963Z",
"updated": "2023-12-17T09:08:31.909723372Z",
"settings": {
"debug_info": false,
"reader_settings": {
"font": "serif",
"font_size": 3,
"line_height": 3
}
}
}
}
}
}
},
"security": [
{
"bearer": []
}
],
"tags": [
{
"name": "info"
},
{
"name": "user profile"
},
{
"name": "bookmarks"
},
{
"name": "bookmark export"
},
{
"name": "bookmark sharing"
},
{
"name": "bookmark labels"
},
{
"name": "bookmark highlights"
},
{
"name": "bookmark collections"
},
{
"name": "bookmark sync",
"x-tag-expanded": false
},
{
"name": "bookmark import",
"x-tag-expanded": false
},
{
"name": "dev tools",
"x-tag-expanded": false
},
{
"name": "oauth",
"x-tag-expanded": false
}
],
"paths": {
"/info": {
"get": {
"tags": [
"info"
],
"security": [],
"x-badges": [
{
"color": "green",
"label": "public"
}
],
"summary": "Information",
"description": "This route returns public information about the Readeck instance.\n\nThe `version` entry contains information about the currently running version, with\nthe following fields:\n\n| Name | Description\n| :---------- | :----------\n| `release` | The major + minor + patch number\n| `canonical` | The full version; release + build number\n| `build` | The build number, if any.\n\nOn a stable release, `build` is empty and `release` is the same as `canonical`. On a nightly\nbuild, `build` contains the commit information. It looks like `175-g154ad5c1` where the\nfirst number is always incrementing one version after another.\n\n`features` is a list that advertises the available features. It can contain the following\nvalues:\n\n| Name | Description\n| :---------- | :----------\n| `email` | This instance can send emails\n| `oauth` | This instance support OAuth\n",
"responses": {
"200": {
"description": "Server information",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"version": {
"type": "object",
"properties": {
"canonical": {
"type": "string",
"description": "Canonical (full) version string"
},
"release": {
"type": "string",
"description": "Release version number"
},
"build": {
"type": "string",
"description": "Build number (can be empty)"
}
}
},
"features": {
"type": "array",
"items": {
"type": "string",
"enum": [
"email",
"oauth"
]
},
"description": "Available features"
}
}
}
}
}
}
}
}
},
"/oauth/client": {
"post": {
"tags": [
"oauth"
],
"security": [],
"x-badges": [
{
"color": "green",
"label": "public"
}
],
"summary": "Client Registration",
"description": "This route creates a new OAuth client. You must create a new client before you can request permissions and receive a token.\n\nPlease refer to the [Client Registration Documentation](#overview--client-registration) for more details.\n\nThis route implements [RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591).\n\nNote: checks on `token_endpoint_auth_method` and `response_types` are enforced but their respective values are not used internaly.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthClientCreate"
}
}
}
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthClientResponse"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthError"
}
}
}
}
}
}
},
"/oauth/token": {
"post": {
"tags": [
"oauth"
],
"security": [],
"x-badges": [
{
"color": "green",
"label": "public"
}
],
"summary": "Access Token",
"description": "A client must call this route once it received the necessary information from the\nredirect URI generated by the authorization route.\n\nPlease refer to:\n\n- [OAuth Authorization Code Flow](#overview--oauth-authorization-code-flow) for details about the authorization code flow\n- [OAuth Device Code Flow](#overview--oauth-device-code-flow) for details about the device code flow\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthTokenCreate"
}
}
}
},
"responses": {
"201": {
"description": "Access Token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthTokenResponse"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthError"
}
}
}
}
}
}
},
"/oauth/device": {
"post": {
"tags": [
"oauth"
],
"security": [],
"x-badges": [
{
"color": "green",
"label": "public"
}
],
"summary": "Device Authorization",
"description": "This route is the device authorization request. A client must call it first,\nwith its client ID and a scope list it wants to grant a device.\n\nPlease refer to [OAuth Device Code Flow](#overview--oauth-device-code-flow) for more information.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthDeviceCreate"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/oauthDeviceCreate"
}
}
}
},
"responses": {
"200": {
"description": "Device Authorization Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthDevice"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthError"
}
}
}
}
}
}
},
"/oauth/revoke": {
"post": {
"tags": [
"oauth"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Access token revoked"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauthError"
}
}
}
}
},
"summary": "Revoke Token",
"description": "This route lets a client revokes an access token. It must authenticate using the same\naccess token than the one provided in the request body.\n\nYou can use this route if you want to provide a log-out option to users.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"required": [
"token"
],
"properties": {
"token": {
"type": "string",
"description": "Access token to revoke"
}
}
}
}
}
}
}
},
"/profile": {
"get": {
"tags": [
"user profile"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | profile:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Profile information",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/userProfile"
}
}
}
}
},
"summary": "User Profile",
"description": "This route returns the current user's profile information. This includes the user information\nand preferences, and the authentication provider with its permissions.\n\n**Note**: This route is available with any OAuth scope but you only receive extended\nprofile information, such as `user.email`, with the `profile:read` scope.\n"
}
},
"/bookmarks": {
"get": {
"tags": [
"bookmarks"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
},
{
"color": "purple",
"label": "paginated"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"200": {
"headers": {
"Link": {
"description": "Link to other pages in paginated results",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"Current-Page": {
"description": "Current page number",
"schema": {
"type": "integer"
}
},
"Total-Count": {
"description": "Total number of items",
"schema": {
"type": "integer"
}
},
"Total-Pages": {
"description": "Total number of pages",
"schema": {
"type": "integer"
}
}
},
"description": "List of bookmark items",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/bookmarkSummary"
}
}
}
}
}
},
"parameters": [
{
"name": "limit",
"in": "query",
"description": "Number of items per page",
"schema": {
"type": "integer"
}
},
{
"name": "offset",
"in": "query",
"description": "Pagination offset",
"schema": {
"type": "integer"
}
},
{
"name": "sort",
"in": "query",
"description": "Sorting parameters",
"schema": {
"type": "array",
"items": {
"type": "string",
"enum": [
"created",
"-created",
"domain",
"-domain",
"duration",
"-duration",
"published",
"-published",
"site",
"-site",
"title",
"-title"
]
}
}
},
{
"name": "search",
"in": "query",
"description": "A full text search string",
"schema": {
"type": "string"
}
},
{
"name": "title",
"in": "query",
"description": "Bookmark title",
"schema": {
"type": "string"
}
},
{
"name": "author",
"in": "query",
"description": "Author's name",
"schema": {
"type": "string"
}
},
{
"name": "site",
"in": "query",
"description": "Bookmark site name or domain",
"schema": {
"type": "string"
}
},
{
"name": "type",
"in": "query",
"description": "Bookmark type",
"schema": {
"type": "array",
"items": {
"type": "string",
"enum": [
"article",
"photo",
"video"
]
}
}
},
{
"name": "labels",
"in": "query",
"description": "One or several labels",
"schema": {
"type": "string"
}
},
{
"name": "is_loaded",
"in": "query",
"description": "Filter by loaded state",
"schema": {
"type": "boolean"
}
},
{
"name": "has_errors",
"in": "query",
"description": "Filter bookmarks with or without errors",
"schema": {
"type": "boolean"
}
},
{
"name": "has_labels",
"in": "query",
"description": "Filter bookmarks with or without labels",
"schema": {
"type": "boolean"
}
},
{
"name": "is_marked",
"in": "query",
"description": "Filter by marked (favorite) status",
"schema": {
"type": "boolean"
}
},
{
"name": "is_archived",
"in": "query",
"description": "Filter by archived status",
"schema": {
"type": "boolean"
}
},
{
"name": "range_start",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "range_end",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "read_status",
"in": "query",
"description": "Read progress status",
"schema": {
"type": "array",
"items": {
"type": "string",
"enum": [
"unread",
"reading",
"read"
]
}
}
},
{
"name": "id",
"in": "query",
"description": "One or more bookmark ID",
"schema": {
"type": "string"
}
},
{
"name": "collection",
"in": "query",
"description": "A collection ID",
"schema": {
"type": "string"
}
}
],
"summary": "Bookmark List",
"description": "This route returns a paginated bookmark list.\n"
},
"post": {
"tags": [
"bookmarks"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
},
"Bookmark-Id": {
"schema": {
"type": "string"
},
"description": "ID of the created bookmark"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"summary": "Bookmark Create",
"description": "Creates a new bookmark",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bookmarkCreate"
}
}
}
}
}
},
"/bookmarks/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Bookmark ID",
"schema": {
"type": "string",
"format": "short-uid"
}
}
],
"get": {
"tags": [
"bookmarks"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Bookmark details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bookmarkInfo"
}
}
}
}
},
"summary": "Bookmark Details",
"description": "Retrieves a saved bookmark"
},
"patch": {
"tags": [
"bookmarks"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"200": {
"description": "Bookmark updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bookmarkUpdated"
}
}
}
}
},
"summary": "Bookmark Update",
"description": "This route updates some bookmark's properties. Every input value is optional.\nUpon success, it returns a mapping of changed values.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bookmarkUpdate"
}
}
}
}
},
"delete": {
"tags": [
"bookmarks"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"204": {
"description": "The bookmark was successfuly deleted."
}
},
"summary": "Bookmark Delete",
"description": "Deletes a saved bookmark"
}
},
"/bookmarks/{id}/article": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Bookmark ID",
"schema": {
"type": "string",
"format": "short-uid"
}
}
],
"get": {
"tags": [
"bookmark export"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "A `text/html` response, containing the article body.\nPlease note that it's only the fragment and not a full HTML document.\n",
"content": {
"text/html": {
"schema": {
"type": "string",
"description": "Article in HTML"
}
}
}
}
},
"summary": "Bookmark Article",
"description": "This route returns the bookmark's article if it exists.\n"
}
},
"/bookmarks/{id}/article.{format}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Bookmark ID",
"schema": {
"type": "string",
"format": "short-uid"
}
}
],
"get": {
"tags": [
"bookmark export"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"content": {
"application/epub+zip": {
"schema": {
"type": "string",
"format": "binary"
}
},
"text/markdown": {
"schema": {
"type": "string"
}
}
}
}
},
"summary": "Bookmark Export",
"description": "This route exports a bookmark to another format.",
"parameters": [
{
"name": "format",
"in": "path",
"required": true,
"description": "Export format",
"schema": {
"type": "string",
"enum": [
"epub",
"md"
]
}
}
]
}
},
"/bookmarks/{id}/share/link": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Bookmark ID",
"schema": {
"type": "string",
"format": "short-uid"
}
}
],
"get": {
"tags": [
"bookmark sharing"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Public link information",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bookmarkShareLink"
}
}
}
}
},
"summary": "Share by link",
"description": "This route produces a publicly accessible link to share a bookmark."
}
},
"/bookmarks/{id}/share/email": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Bookmark ID",
"schema": {
"type": "string",
"format": "short-uid"
}
}
],
"post": {
"tags": [
"bookmark sharing"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Message sent",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"summary": "Share by email",
"description": "This route sends a bookmark to an email address.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bookmarkShareEmail"
}
}
}
}
}
},
"/bookmarks/labels": {
"get": {
"tags": [
"bookmark labels"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Label list",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/labelInfo"
}
}
}
}
}
},
"summary": "Label List",
"description": "This route returns all the labels associated to a bookmark for the current user.\n"
}
},
"/bookmarks/labels?name={name}": {
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"description": "Label",
"schema": {
"type": "string"
}
}
],
"get": {
"tags": [
"bookmark labels"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Label information",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/labelInfo"
}
}
}
}
}
},
"summary": "Label Info",
"description": "This route returns information about a given bookmark label."
},
"patch": {
"tags": [
"bookmark labels"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"200": {
"description": "Label renamed"
}
},
"summary": "Label Update",
"description": "This route renames a label.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/labelUpdate"
}
}
}
}
},
"delete": {
"tags": [
"bookmark labels"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"204": {
"description": "Label removed"
}
},
"summary": "Label Delete",
"description": "This route remove a label from all associated bookmarks.\n\nPlease note that it does not remove the bookmarks themselves.\n"
}
},
"/bookmarks/annotations": {
"get": {
"tags": [
"bookmark highlights"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
},
{
"color": "purple",
"label": "paginated"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"headers": {
"Link": {
"description": "Link to other pages in paginated results",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"Current-Page": {
"description": "Current page number",
"schema": {
"type": "integer"
}
},
"Total-Count": {
"description": "Total number of items",
"schema": {
"type": "integer"
}
},
"Total-Pages": {
"description": "Total number of pages",
"schema": {
"type": "integer"
}
}
},
"description": "Highlight list",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/annotationSummary"
}
}
}
}
}
},
"parameters": [
{
"name": "limit",
"in": "query",
"description": "Number of items per page",
"schema": {
"type": "integer"
}
},
{
"name": "offset",
"in": "query",
"description": "Pagination offset",
"schema": {
"type": "integer"
}
}
],
"summary": "Highlight List",
"description": "This route returns all the highlights created by the current user.\n"
}
},
"/bookmarks/{id}/annotations": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Bookmark ID",
"schema": {
"type": "string",
"format": "short-uid"
}
}
],
"get": {
"tags": [
"bookmark highlights"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Highlight list",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/annotationInfo"
}
}
}
}
}
},
"summary": "Bookmark Highlights",
"description": "This route returns a given bookmark's highlights.\n"
},
"post": {
"tags": [
"bookmark highlights"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"201": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/annotationInfo"
}
}
},
"description": "Highlight created"
}
},
"summary": "Highlight Create",
"description": "This route creates a new highlight on a given bookmarks.\n\nThe highlight format is similar to the [Range API](https://developer.mozilla.org/en-US/docs/Web/API/Range)\nwith some differences:\n\n- A range's start and end selectors are XPath selectors and must target an element.\n- The offset is the text length from the begining of the selector, regardless of the traversed\n potential children.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/annotationCreate"
}
}
}
}
}
},
"/bookmarks/{id}annotations/{annotation_id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Bookmark ID",
"schema": {
"type": "string",
"format": "short-uid"
}
},
{
"name": "annotation_id",
"in": "path",
"required": true,
"description": "Highlight ID",
"schema": {
"type": "string",
"format": "short-uid"
}
}
],
"patch": {
"tags": [
"bookmark highlights"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"200": {
"description": "Update result",
"content": {
"application/json": {
"schema": {
"properties": {
"updated": {
"type": "string",
"format": "date-time"
},
"annotations": {
"type": "array",
"items": {
"$ref": "#/components/schemas/annotationInfo"
}
}
}
}
}
}
}
},
"summary": "Highlight Update",
"description": "This route updates then given highlight in the given bookmark.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/annotationUpdate"
}
}
}
}
},
"delete": {
"tags": [
"bookmark highlights"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"204": {
"description": "Highlight removed"
}
},
"summary": "Highlight Delete",
"description": "This route removes the given highlight in the given bookmark.\n"
}
},
"/bookmarks/collections": {
"get": {
"tags": [
"bookmark collections"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
},
{
"color": "purple",
"label": "paginated"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"headers": {
"Link": {
"description": "Link to other pages in paginated results",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"Current-Page": {
"description": "Current page number",
"schema": {
"type": "integer"
}
},
"Total-Count": {
"description": "Total number of items",
"schema": {
"type": "integer"
}
},
"Total-Pages": {
"description": "Total number of pages",
"schema": {
"type": "integer"
}
}
},
"description": "Collection list",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/collectionInfo"
}
}
}
}
}
},
"parameters": [
{
"name": "limit",
"in": "query",
"description": "Number of items per page",
"schema": {
"type": "integer"
}
},
{
"name": "offset",
"in": "query",
"description": "Pagination offset",
"schema": {
"type": "integer"
}
}
],
"summary": "Collection List",
"description": "This route returns all the current user's collections.\n"
},
"post": {
"tags": [
"bookmark collections"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"201": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"summary": "Collection Create",
"description": "This route creates a new collection.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/collectionCreate"
}
}
}
}
}
},
"/bookmarks/collections/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Collection ID",
"schema": {
"type": "string",
"format": "short-uid"
}
}
],
"get": {
"tags": [
"bookmark collections"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Collection information",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/collectionInfo"
}
}
}
}
},
"summary": "Collection Details",
"description": "This route returns a given collection information.\n"
},
"patch": {
"tags": [
"bookmark collections"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"200": {
"description": "Updated fields",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/collectionSummary"
}
}
}
}
},
"summary": "Collection Update",
"description": "This route updates a given collection. It returns a mapping of updated fields.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/collectionUpdate"
}
}
}
}
},
"delete": {
"tags": [
"bookmark collections"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:write"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"204": {
"description": "Collection deleted"
}
},
"summary": "Collection Delete",
"description": "This route deletes a given collection.\n"
}
},
"/bookmarks/import/browser": {
"post": {
"tags": [
"bookmark import"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"$ref": "#components/schemas/baseImport"
},
{
"properties": {
"data": {
"type": "string",
"format": "binary",
"description": "Import file"
}
}
}
]
}
}
}
},
"summary": "Import Browser Bookmarks",
"description": "This route creates bookmarks from an HTML file generated by an export of a browser's\nbookmarks.\n"
}
},
"/bookmarks/import/csv": {
"post": {
"tags": [
"bookmark import"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"$ref": "#components/schemas/baseImport"
},
{
"properties": {
"data": {
"type": "string",
"format": "binary",
"description": "Import file"
}
}
}
]
}
}
}
},
"summary": "Import CSV files",
"description": "This route creates bookmarks from a CSV file. Compatible with Instapaper.\n\nThe uploaded file must contain a first row with the column names. Column names are case insensitive and, except for url, every column is optional.\n\nHere are the columns you can set:\n\n| Field | Alias | Description\n| ------------------------ | :------------- | :----------\n| `url` (**required**) | | Link address\n| `title` | | Bookmark title\n| `state` | `folder` | Bookmark's archived state; only valid value is \"archive\"\n| `created` | `timestamp` | Creation date, can be a UNIX timestamp or an RFC-3339 formatted date\n| `labels` | `tags` | A JSON encoded list of labels\n\nExample:\n\n```\nurl,title,state,created,labels\nhttps://www.the-reframe.com/all-in-the-same-boat/,\"All In The Same Boat\",,2025-01-12T10:45:56,\"[\"\"label 1\"\",\"\"label 2\"\"]\"\n```\n"
}
},
"/bookmarks/import/goodlinks": {
"post": {
"tags": [
"bookmark import"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"$ref": "#components/schemas/baseImport"
},
{
"properties": {
"data": {
"type": "string",
"format": "binary",
"description": "Import file"
}
}
}
]
}
}
}
},
"summary": "Import GoodLinks files.",
"description": "This route creates bookmarks from a GoodLinks export file.\n"
}
},
"/bookmarks/import/linkwarden": {
"post": {
"tags": [
"bookmark import"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"$ref": "#components/schemas/baseImport"
},
{
"properties": {
"data": {
"type": "string",
"format": "binary",
"description": "Import file"
}
}
}
]
}
}
}
},
"summary": "Import Linkwarden files.",
"description": "This route creates bookmarks from a Linkwarden export file.\n"
}
},
"/bookmarks/import/pocket-file": {
"post": {
"tags": [
"bookmark import"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"$ref": "#components/schemas/baseImport"
},
{
"properties": {
"data": {
"type": "string",
"format": "binary",
"description": "Import file"
}
}
}
]
}
}
}
},
"summary": "Import Pocket Saves",
"description": "This route creates bookmarks from an HTML file generated by Pocket export tool.\nGo to [https://getpocket.com/export](https://getpocket.com/export) to generate\nsuch a file.\n"
}
},
"/bookmarks/import/readwise": {
"post": {
"tags": [
"bookmark import"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"$ref": "#components/schemas/baseImport"
},
{
"properties": {
"data": {
"type": "string",
"format": "binary",
"description": "Import file"
}
}
}
]
}
}
}
},
"summary": "Import Readwise files.",
"description": "This route creates bookmarks from a Readwise export file (CSV).\n"
}
},
"/bookmarks/import/text": {
"post": {
"tags": [
"bookmark import"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"allOf": [
{
"$ref": "#components/schemas/baseImport"
},
{
"properties": {
"data": {
"type": "string",
"format": "binary",
"description": "Import file"
}
}
}
]
}
}
}
},
"summary": "Import a Text File",
"description": "This route creates bookmarks from a text file that contains one URL\nper line.\n"
}
},
"/bookmarks/import/wallabag": {
"post": {
"tags": [
"bookmark import"
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"202": {
"headers": {
"Location": {
"description": "URL of the created resource",
"schema": {
"type": "string",
"format": "uri"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
}
},
"summary": "Import Wallabag Articles",
"description": "This route imports articles from Wallabag using its API.\n\nYou must create an API client in Wallabag and use its \"Client ID\" and \"Client Secret\"\nin this route's payload.\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/wallabagImport"
}
}
}
}
}
},
"/bookmarks/sync": {
"get": {
"tags": [
"bookmark sync"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"200": {
"description": "Item list",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/bookmarkSyncList"
}
}
}
}
}
},
"summary": "Bookmark Sync List",
"description": "This route returns a non-paginated list of all bookmarks ordered by last update dates.\n\nYou can retrieve only bookmarks updated and deleted since a given date by using the\n`since` parameter. Please note that without the parameter, it only returns updated\nbookmarks.\n",
"parameters": [
{
"name": "since",
"in": "query",
"description": "A datetime to retrieve updated and deleted IDs including and after this value.",
"schema": {
"type": "string",
"format": "date-time"
}
}
]
},
"post": {
"tags": [
"bookmark sync"
],
"x-badges": [
{
"color": "blue",
"label": "oauth | bookmarks:read"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"422": {
"description": "This HTTP response is sent when the input data is not valid. It contains an object\nwith all the detected errors.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"is_valid": {
"type": "boolean",
"description": "`true` if the input is valid\n"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of global input errors"
},
"fields": {
"type": "object",
"description": "All the fields, with and without errors\n",
"additionalProperties": {
"properties": {
"is_null": {
"type": "boolean",
"description": "`true` if the input value is null\n"
},
"is_bound": {
"type": "boolean",
"description": "`true` when the value is bound to the form\n"
},
"value": {
"type": "any",
"description": "Item's value; can be any type"
},
"errors": {
"type": "[string]",
"nullable": true,
"description": "List of errors for this field"
}
}
}
}
}
}
}
}
},
"200": {
"description": "Item list",
"content": {
"multipart/mixed": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
},
"summary": "Bookmark Sync",
"description": "This route returns a `multipart/mixed` response with all the bookmarks passed in `id` (or all of them if unset).\n\nThe response's content is a stream and should be processed while the data is received, part by part.\n\n<details>\n<summary>Multipart Response Example</summary>\n\n```\n--910345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b\nBookmark-Id: VnopmpKQ3CmQ6apY9mgDws\nContent-Disposition: attachment; filename=\"info.json\"\nContent-Type: application/json; charset=utf-8\nDate: 2025-06-20T10:53:47Z\nFilename: info.json\nLast-Modified: 2025-07-03T12:49:30Z\nLocation: http://localhost:8000/api/bookmarks/VnopmpKQ3CmQ6apY9mgDws\nType: json\n\n...content\n\n--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b\nBookmark-Id: VnopmpKQ3CmQ6apY9mgDws\nContent-Disposition: attachment; filename=\"index.html\"\nContent-Type: text/html; charset=utf-8\nDate: 2025-06-20T10:53:47Z\nFilename: index.html\nLast-Modified: 2025-07-03T12:49:30Z\nType: html\n\n...content\n\n--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b\nBookmark-Id: VnopmpKQ3CmQ6apY9mgDws\nContent-Disposition: attachment; filename=\"image.jpeg\"\nContent-Length: 86745\nContent-Type: image/jpeg\nFilename: image.jpeg\nGroup: image\nLocation: http://localhost:8000/bm/Vn/VnopmpKQ3CmQ6apY9mgDws/img/image.jpeg\nPath: image.jpeg\nType: resource\n\n...content\n\n--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b\nBookmark-Id: VnopmpKQ3CmQ6apY9mgDws\nContent-Disposition: attachment; filename=\"Wj66qLatSeikPc31FwvqyS.jpg\"\nContent-Length: 171749\nContent-Type: image/jpeg\nFilename: Wj66qLatSeikPc31FwvqyS.jpg\nGroup: embedded\nLocation: http://localhost:8000/bm/Vn/VnopmpKQ3CmQ6apY9mgDws/_resources/Wj66qLatSeikPc31FwvqyS.jpg\nPath: Wj66qLatSeikPc31FwvqyS.jpg\nType: resource\n\n...content\n\n--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b--\n```\n</details>\n\n### Part types\n\nA \"part\" is a chunk found between the multipart boundary.\n\nEach part has a `Type` header that takes the following values:\n\n| value | description |\n| :--------- | :-------------------------------------------------------------------------------------- |\n| `json` | controlled by `with_json`. It contains the same output as an API bookmark information. |\n| `html` | controlled by `with_html`. It contains the HTML content (article), if any. |\n| `markdown` | controlled by `with_markdown`. It contains the bookmark converted to Markdown. |\n| `resource` | controlled by `with_resources`. Each part is a resource (icon, images, article images). |\n\n**Note**: There is only one part per bookmark for `json`, `html` and `markdown` types.\n\nEach part has a `Bookmark-Id` attribute that indicates the bookmark it belongs to.\n\n### Resources\n\nA resource is a `Type: resource` part which is usually an image.\n\nEach resource part contains a `Path` header that's based on the `resource_prefix` parameter.\n\nEach `Type: resource` part contains a `Group` header that can take the following values:\n\n| value | description |\n| :---------- | :------------------------------------------------------------------------------- |\n| `icon` | the bookmark's icon, |\n| `image` | the bookmark's image (main picture for photo types, and placeholder for videos), |\n| `thumbnail` | thumbnail of the image, |\n| `embedded` | included in the article itself. |\n\n\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bookmarkSyncParams"
}
}
}
}
}
},
"/cookbook/extract": {
"parameters": [
{
"name": "url",
"in": "query",
"required": true,
"schema": {
"type": "string",
"format": "uri"
},
"description": "URL to extract"
},
{
"name": "Accept",
"in": "header",
"required": false,
"schema": {
"type": "string",
"enum": [
"application/json",
"text/html"
]
}
}
],
"get": {
"tags": [
"dev tools"
],
"x-badges": [
{
"color": "red",
"label": "admin only"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Extraction result.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "The extracted URL"
},
"logs": {
"type": "array",
"items": {
"type": "string"
},
"description": "Extraction log"
},
"errors": {
"type": "array",
"items": {
"type": "string"
},
"description": "Extraction errors, if any"
},
"meta": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"description": "Contains the meta tags extracted from the page.\n"
},
"properties": {
"properties": {
"json-ld": {
"type": "array",
"items": {
"type": "object"
},
"description": "A list of JSON-LD documents retrieved during the extraction"
},
"link": {
"type": "array",
"items": {
"type": "object",
"patternProperties": {
"^@.+": {
"type": "string",
"description": "Link attribute, always starting with `@`"
}
}
},
"description": "A list of all `link` tags retrieved during the extraction"
},
"meta": {
"type": "array",
"items": {
"type": "object",
"patternProperties": {
"^@.+": {
"type": "string",
"description": "Meta attribute, always starting with `@`"
}
}
},
"description": "A list of all `meta` tags retrieved during the extraction"
}
}
},
"domain": {
"type": "string",
"format": "hostname",
"description": "Page's domain name"
},
"title": {
"type": "string",
"description": "Page's title"
},
"authors": {
"type": "[string]",
"description": "Page's author list"
},
"site": {
"type": "string",
"format": "hostname",
"description": "Page's site"
},
"site_name": {
"type": "string",
"description": "Page's site name"
},
"lang": {
"type": "string",
"description": "Language Code"
},
"text_direction": {
"type": "string",
"enum": [
"rtl",
"ltr"
],
"description": "Direction of the article's text. It can be empty when it's unknown.\n"
},
"date": {
"type": [
"string"
],
"format": "date-time",
"nullable": true,
"description": "Publication date. Can be `null` when unknown."
},
"document_type": {
"type": "string",
"description": "The detected document type. The value is usualy `article`, `photo` or `video`\nbut can vary, based on the extraction process.\n"
},
"description": {
"type": "string",
"description": "Page's short description, when it exists. It's always an unformatted text.\n"
},
"html": {
"type": "string",
"description": "The HTML content after processing.\n"
},
"embed": {
"type": "string",
"description": "The oembed HTML fragment, when it exists. It usualy contains an iframe when\nextracting videos.\n"
},
"images": {
"properties": {
"additionalProperties": {
"properties": {
"size": {
"type": "[integer]",
"description": "The image size in pixels"
},
"encoded": {
"type": "string",
"description": "The base64 URI encoded image"
}
}
}
}
}
}
}
},
"text/html": {
"schema": {
"type": "string",
"description": "HTML after extraction"
}
}
}
}
},
"summary": "Extract Link",
"description": "This route extracts a link and returns the extraction result.\n\nYou can pass an `Accept` header to the request, with one of the following values:\n\n- `application/json` (default) returns a JSON response\n- `text/html` returns an HTML response with all the media included as base64 encoded\n URLs.\n"
},
"post": {
"tags": [
"dev tools"
],
"x-badges": [
{
"color": "red",
"label": "admin only"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Extraction result.\n",
"content": {
"application/json": {
"schema": {
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "The extracted URL"
},
"logs": {
"type": "array",
"items": {
"type": "string"
},
"description": "Extraction log"
},
"errors": {
"type": "array",
"items": {
"type": "string"
},
"description": "Extraction errors, if any"
},
"meta": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"description": "Contains the meta tags extracted from the page.\n"
},
"properties": {
"properties": {
"json-ld": {
"type": "array",
"items": {
"type": "object"
},
"description": "A list of JSON-LD documents retrieved during the extraction"
},
"link": {
"type": "array",
"items": {
"type": "object",
"patternProperties": {
"^@.+": {
"type": "string",
"description": "Link attribute, always starting with `@`"
}
}
},
"description": "A list of all `link` tags retrieved during the extraction"
},
"meta": {
"type": "array",
"items": {
"type": "object",
"patternProperties": {
"^@.+": {
"type": "string",
"description": "Meta attribute, always starting with `@`"
}
}
},
"description": "A list of all `meta` tags retrieved during the extraction"
}
}
},
"domain": {
"type": "string",
"format": "hostname",
"description": "Page's domain name"
},
"title": {
"type": "string",
"description": "Page's title"
},
"authors": {
"type": "[string]",
"description": "Page's author list"
},
"site": {
"type": "string",
"format": "hostname",
"description": "Page's site"
},
"site_name": {
"type": "string",
"description": "Page's site name"
},
"lang": {
"type": "string",
"description": "Language Code"
},
"text_direction": {
"type": "string",
"enum": [
"rtl",
"ltr"
],
"description": "Direction of the article's text. It can be empty when it's unknown.\n"
},
"date": {
"type": [
"string"
],
"format": "date-time",
"nullable": true,
"description": "Publication date. Can be `null` when unknown."
},
"document_type": {
"type": "string",
"description": "The detected document type. The value is usualy `article`, `photo` or `video`\nbut can vary, based on the extraction process.\n"
},
"description": {
"type": "string",
"description": "Page's short description, when it exists. It's always an unformatted text.\n"
},
"html": {
"type": "string",
"description": "The HTML content after processing.\n"
},
"embed": {
"type": "string",
"description": "The oembed HTML fragment, when it exists. It usualy contains an iframe when\nextracting videos.\n"
},
"images": {
"properties": {
"additionalProperties": {
"properties": {
"size": {
"type": "[integer]",
"description": "The image size in pixels"
},
"encoded": {
"type": "string",
"description": "The base64 URI encoded image"
}
}
}
}
}
}
}
},
"text/html": {
"schema": {
"type": "string",
"description": "HTML after extraction"
}
}
}
}
},
"summary": "Extract Link with content",
"description": "This route extracts a link and returns the extraction result.\n\nYou can pass an `Accept` header to the request, with one of the following values:\n\n- `application/json` (default) returns a JSON response\n- `text/html` returns an HTML response with all the media included as base64 encoded\n URLs.\n\nTo pass the entire contents of a page together with its url, send this request with\nthe `Content-Type: text/html` header and the page contents as the request body. This\nway, the article fetcher can use that instead of having to fetch the page at url.\n",
"parameters": [
{
"name": "Content-Type",
"in": "header",
"required": false,
"schema": {
"type": "string",
"enum": [
"text/html"
]
}
}
],
"requestBody": {
"required": false,
"content": {
"text/html": {
"schema": {
"type": "string",
"description": "Optional HTML content of the resource at url"
}
}
}
}
}
},
"/cookbook/urls": {
"get": {
"tags": [
"dev tools"
],
"x-badges": [
{
"color": "red",
"label": "admin only"
}
],
"responses": {
"401": {
"description": "Unauthorized. The request token found in the Authorization header is not valid.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"403": {
"description": "Forbidden. The user doesn't have permission to perform the request but has other permissions.\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/message"
}
}
}
},
"200": {
"description": "Test URLs for content extraction",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
}
}
}
}
}
}
},
"summary": "List test URLs",
"description": "Lists test URLs gathered from `pkg/extract/contentscripts/assets/site-config`\n"
}
}
}
}