API Reference
The Readmint base URL is https://api-readmint.plxi.us. Every endpoint accepts and returns JSON and is authenticated with your API key.
curl -X POST https://api-readmint.plxi.us/v1/read \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/"
}'How reading works
Every read, preview and map starts with a plain fetch of the page (up to 2 MB of HTML). Mozilla’s Readability finds the main content and Turndown writes it as Markdown, with tables, lists and code blocks kept and every link and image made a full URL. Navigation, headers, footers, sidebars, cookie banners, pop-ups and chat widgets are left out. A page that isn’t an article (a home page, a pricing page) comes back without its navigation, header and footer.
- JavaScript. When the plain fetch finds almost no text, a page that asks for JavaScript, a site that turns plain clients away, a page over 2 MB or one that isn’t UTF-8, the page is loaded in a real Chromium browser instead, with cookie banners and pop-ups hidden.
renderedsays which happened.render: "always"and"never"override it. The price is the same. - One part of a page.
selectorreads the first element a CSS selector matches instead of finding the main content, andwait_for_selectorwaits for one to appear when the page is loaded in the browser. - Caching. Answers are kept for
max_ageseconds: an hour for reads, a day for previews, an hour for maps. A repeat within that time is answered from the cache (it’s still a call, and costs one page).max_age: 0reads the page fresh, andfetched_atsays when it was read. - Size.
contentis cut atmax_chars(100,000 by default, up to 1,000,000), withtruncated: true. Sendimages: falseto leave images out of the content. - What isn’t read. Only public http and https pages: private and reserved addresses are refused, every redirect included. Readmint doesn’t log in, solve CAPTCHAs or get past bot protection; those pages, HTTP errors and files that aren’t web pages are refused with a 4xx error and never charged. PDFs aren’t read yet.
Authentication
Send an X-API-Key header with every request. Keys are created and revoked in the dashboard; a key is shown once, when it is created, so store it somewhere safe.
X-API-Key: sk_live_...
Plans & limits
Limits apply per account, not per key: every key on the account (and teammates working in it) shares them. There is a per-minute rate, a cap on requests in flight at once, and a monthly quota. On the Free plan the quota resets on the 1st of each month (UTC); on paid plans it resets on your billing-period anniversary. Requests that fail validation are never charged.
| Plan | Price | Requests / min | In flight | Pages / month |
|---|---|---|---|---|
| Free | Free | 20 | 2 | 1,000 |
| Starter | $10/mo | 120 | 4 | 20,000 |
| Pro | $50/mo | 600 | 8 | 200,000 |
Errors
Errors return JSON with a stable error code, an optional message, and, on rate-limit and monthly-quota 429s, reset_at. A request that returns an error is never charged.
{
"error": "rate_limit_exceeded",
"reset_at": "2026-07-22T18:04:00Z"
}Fix the request
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | bad_request | The body is not valid JSON. | Send the body as JSON. |
| 400 | invalid_input | A body field failed the operation’s checks; message says which one and why. | Correct that field. |
| 401 | invalid_api_key | The X-API-Key header is missing, malformed, revoked or not a key we issued. | Check the key, or create a new one in the dashboard. |
| 403 | account_blocked | The account is suspended. | Contact [email protected]. |
| 404 | not_found | No endpoint at that path for that method: operations take POST, /v1/usage takes GET. | Check the path and the method. |
| 413 | payload_too_large | The body is larger than the endpoint takes: 1 MiB unless it says otherwise. | Send a smaller body. |
The page can’t be read (never charged)
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | invalid_url | The URL isn’t http or https, has a user name or password, or points to a private, loopback or reserved address, or redirects to one. | Send a public http(s) URL. |
| 415 | unsupported_content_type | The URL is a file, not a web page: a PDF, an image, a download. read also takes plain text and Markdown. | Send the URL of an HTML page. |
| 422 | upstream_status | The page answered with an HTTP error; status says which (404, 410, 500…). | Check the URL in a browser. Retry later for a 5xx. |
| 422 | blocked_by_site | The site showed a bot check or a CAPTCHA instead of the page. | Readmint doesn’t get past those; read another source. |
| 422 | unrenderable_url unreadable_page page_too_large | The browser couldn’t load the page, it kept redirecting, or it downloaded more than 50 MB. | Check the URL in a browser; try render: "always" or a selector. |
Wait, then retry
| Status | Code | When | What to do |
|---|---|---|---|
| 429 | rate_limit_exceeded | The per-minute request limit is reached (per account, across all its keys). Requests that then fail, for example on invalid input, count toward it too. | Retry after reset_at, the start of the next minute. |
| 429 | concurrency_limit_exceeded | The account has as many requests in flight as its plan allows. | Retry with backoff once one of them finishes. There is no reset_at. |
| 429 | monthly_quota_exceeded | The quota for this billing period is used up. | Retry after reset_at: the 1st of next month (UTC) on Free, the end of your billing period on paid plans. Or upgrade. |
Temporary: retry with backoff
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | body_read_failed | The connection failed while the body was being sent. | Retry. |
| 408 | request_body_timeout | The body took too long to arrive. | Retry, sending the whole body at once. |
| 500 | internal_error | Something failed on our side. | Retry with backoff. |
| 503 | capacity_unavailable | The API is handling as many requests as it can. | Retry with backoff. |
| 503 | database_unavailable limits_unavailable | Our database, or the store that counts rate limits, is unavailable. | Retry with backoff. |
| 503 | auth_busy | API key checks are busy. | Retry with backoff. |
| 503 | billing_period_stale billing_period_invalid | On a paid plan, the current billing period can’t be confirmed, for example because a renewal hasn’t been recorded yet. | Retry later. If it lasts, the account owner can open Billing in the dashboard to refresh it, or contact [email protected]. |
| 502 / 504 | upstream_unavailable upstream_timeout render_timeout | The site didn’t answer, or its page took too long to load. | Retry later. |
| 503 | browser_busy browser_unavailable | Every browser that loads pages is busy, or one is restarting. | Retry with backoff. |
| 504 | request_timeout | The operation ran past the API’s time limit. | Retry with backoff; if it keeps happening, send smaller input. |
Use with AI agents (MCP)
Readmint is also an MCP server, so Claude and other AI agents can call its main operations as tools (listed below). Tool calls are charged exactly like API calls, to the same plan and quota, and failed calls are free.
https://api-readmint.plxi.us/mcp
Claude (claude.ai and the desktop app): Settings → Connectors → Add custom connector, then paste the URL above. Claude asks you to sign in with your Plexsicity account; calls are then charged to your own account.
Claude Code, Cursor, VS Code and other clients: send an API key from the dashboard as a header. Calls are charged to the account that owns the key, so a team’s key spends the team’s quota. Clients that only take a bearer token can send Authorization: Bearer sk_live_… instead.
1. Connect your agent
claude mcp add --transport http readmint https://api-readmint.plxi.us/mcp \
--header "X-API-Key: sk_live_..."2. Tell it when to use the tools
Add this to the repository’s AGENTS.md or CLAUDE.md. Agents also get these rules from the server when they connect; the file makes them part of the project.
## Readmint
- To read a web page, call `read_url` instead of fetching its HTML yourself: it returns the main content as Markdown, without navigation or ads.
- To describe or show a link (its title, description and image), call `preview_url`.
- To find a site's pages before reading them, call `map_site`, then `read_url` the ones you need.
POST/v1/read
A web page's main content as clean Markdown, text or HTML, without menus, ads or cookie banners. Costs 1 page.
| Field | Type | Description |
|---|---|---|
| format? | "markdown" | "text" | "html" | What `content` is: Markdown (headings, lists, tables, links), plain text with paragraph breaks, or the cleaned HTML. Default "markdown". |
| images? | boolean | Keep images in the content (`` in Markdown). false leaves them out. Default true. |
| include? | array | Extra lists in the result: `links` (each link in the content, with its text), `images` (each image, with its alt text) and `metadata` (description, canonical URL, Open Graph, Twitter, icons, feeds and JSON-LD). |
| max_age? | integer | How old, in seconds, a cached copy of this read may be. 0 reads the page fresh. Default 3600. |
| max_chars? | integer | The most characters of content to return; past it the content is cut and `truncated` is true. Default 100000. |
| render? | "auto" | "always" | "never" | `auto` fetches the page directly and loads it in a real browser only when it has to (it's built by JavaScript, or the site turns plain clients away). `always` loads it in the browser; `never` never does. Default "auto". |
| selector? | string | A CSS selector: read only the first element it matches, instead of finding the main content. |
| url | string | Required. The page to read: a public http or https URL. |
| wait_for_selector? | string | When the page is loaded in the browser, wait up to 30 seconds for this CSS selector to appear first. |
{
"url": "https://example.com/"
}{
"article": true,
"byline": null,
"content": "This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.\n\n…\n\n[Learn more](https://iana.org/help/example-domains)",
"credits": 1,
"excerpt": "This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.",
"fetched_at": "2026-10-31T13:20:00Z",
"final_url": "https://example.com/",
"format": "markdown",
"lang": "en",
"published": null,
"rendered": true,
"site_name": null,
"status": 200,
"title": "Example Domain",
"truncated": false,
"url": "https://example.com/",
"word_count": 133
}curl -X POST https://api-readmint.plxi.us/v1/read \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/"
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: read_url.
POST/v1/preview
A link preview: title, description, image, site name, favicon and oEmbed details for any page. Costs 1 page.
| Field | Type | Description |
|---|---|---|
| max_age? | integer | How old, in seconds, a cached preview may be. 0 fetches it fresh. Default 86400. |
| render? | "auto" | "never" | `auto` loads the page in a real browser when a plain fetch finds no title (pages built by JavaScript, sites that turn plain clients away). `never` doesn't. Default "auto". |
| url | string | Required. The page to preview: a public http or https URL. |
{
"url": "https://example.com/"
}{
"author": null,
"canonical": null,
"content_type": "text/html",
"credits": 1,
"description": null,
"favicon": "https://example.com/favicon.ico",
"feeds": [],
"fetched_at": "2026-10-31T13:20:00Z",
"final_url": "https://example.com/",
"icons": [],
"image": null,
"lang": "en",
"oembed": null,
"oembed_url": null,
"published": null,
"rendered": false,
"site_name": "example.com",
"status": 200,
"theme_color": null,
"title": "Example Domain",
"type": null,
"url": "https://example.com/"
}curl -X POST https://api-readmint.plxi.us/v1/preview \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/"
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: preview_url.
POST/v1/map
A site's page URLs, from its sitemaps and the links on a start page. Costs 1 page.
| Field | Type | Description |
|---|---|---|
| include_subdomains? | boolean | Also keep URLs on other hosts under the same domain (docs.example.com for example.com). www. is always the same site. Default false. |
| links? | boolean | Read the links on the start page. Default true. |
| max_urls? | integer | The most URLs to return. Default 1000. |
| search? | string | Keep only URLs that contain this text (any case), e.g. "/blog/" or "pricing". |
| sitemap? | boolean | Read the site's sitemaps. Default true. |
| url | string | Required. The site's start page: a public http or https URL. |
{
"max_urls": 100,
"url": "https://example.com/"
}{
"count": 1,
"credits": 1,
"from_page": 0,
"from_sitemap": 0,
"sitemaps": [
"https://example.com/sitemap.xml"
],
"truncated": false,
"url": "https://example.com/",
"urls": [
"https://example.com/"
]
}curl -X POST https://api-readmint.plxi.us/v1/map \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"max_urls": 100,
"url": "https://example.com/"
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: map_site.
GET/v1/usage
Your plan and usage this billing period, for alerting before you reach the quota.
curl https://api-readmint.plxi.us/v1/usage \
-H "X-API-Key: sk_live_..."{
"plan": "starter",
"used": 487,
"limit": 20000,
"period_start": "2026-07-18T09:12:44Z",
"period_end": "2026-08-18T09:12:44Z"
}Webhooks
Add an HTTPS endpoint under Webhooks in the dashboard and Readmint POSTs a request.completed event to it after each charged request. Each endpoint gets its own signing secret (whsec_…), shown once when you add it.
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Readmint-Webhooks/1
X-Readmint-Event: request.completed
X-Readmint-Event-ID: 6f1c2a7e-…:0d4e9b2c-…
X-Readmint-Signature: sha256=3b5f0c9e41d7…
{"data": {"ms": 412, "credits": 1, "endpoint": "read"}, "event": "request.completed", "created_at": "2026-10-31T14:03:11.482107+00:00"}X-Readmint-Signature is sha256=<hex>: the HMAC-SHA256 of the raw body, keyed with the endpoint’s full secret including whsec_. Compute it over the bytes you received, before parsing:
import crypto from 'node:crypto';
// rawBody: the request body exactly as received (Buffer or string). Verify it
// before parsing -- JSON that has been parsed and re-serialised won't match.
// secret: the endpoint's full signing secret, "whsec_" prefix included.
function verifySignature(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const given = Buffer.from(String(signatureHeader || ''));
const want = Buffer.from(expected);
return given.length === want.length && crypto.timingSafeEqual(given, want);
}
// Express: take this route's body raw, verify, then parse.
app.post('/your/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifySignature(req.body, req.get('X-Readmint-Signature'), process.env.WEBHOOK_SECRET);
if (!ok) return res.sendStatus(401);
const { event, data } = JSON.parse(req.body);
res.sendStatus(200);
});X-Readmint-Event-ID, so use it to ignore duplicates. Recent attempts are listed under Deliveries on the Webhooks page. Endpoints must be public HTTPS URLs.