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/"
}'
New accounts include 1,000 free pages per month, no card required. Create an account and grab your key from the dashboard.

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. rendered says which happened. render: "always" and "never" override it. The price is the same.
  • One part of a page. selector reads the first element a CSS selector matches instead of finding the main content, and wait_for_selector waits for one to appear when the page is loaded in the browser.
  • Caching. Answers are kept for max_age seconds: 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: 0 reads the page fresh, and fetched_at says when it was read.
  • Size. content is cut at max_chars (100,000 by default, up to 1,000,000), with truncated: true. Send images: false to 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_...
Keep keys on your server. Anyone with your key can spend your quota, so never put it in browser JavaScript or a public repository.

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.

PlanPriceRequests / minIn flightPages / month
FreeFree2021,000
Starter$10/mo120420,000
Pro$50/mo6008200,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

StatusCodeWhenWhat 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)

StatusCodeWhenWhat 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

StatusCodeWhenWhat 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

StatusCodeWhenWhat 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.
A response without a JSON body comes from the proxies in front of the API, for example a 413 for a body far over the limit or a 502 while the API restarts. Retry the 5xx ones with backoff.

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.
Tools: read_url, preview_url, map_site. Tools take URLs, not file uploads. Results are JSON, also returned as structured content.

POST/v1/read

A web page's main content as clean Markdown, text or HTML, without menus, ads or cookie banners. Costs 1 page.

FieldTypeDescription
format?"markdown" | "text" | "html"What `content` is: Markdown (headings, lists, tables, links), plain text with paragraph breaks, or the cleaned HTML. Default "markdown".
images?booleanKeep images in the content (`![alt](url)` in Markdown). false leaves them out. Default true.
include?arrayExtra 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?integerHow old, in seconds, a cached copy of this read may be. 0 reads the page fresh. Default 3600.
max_chars?integerThe 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?stringA CSS selector: read only the first element it matches, instead of finding the main content.
urlstringRequired. The page to read: a public http or https URL.
wait_for_selector?stringWhen the page is loaded in the browser, wait up to 30 seconds for this CSS selector to appear first.
Example request body
{
  "url": "https://example.com/"
}
Example response · 200
{
  "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.

FieldTypeDescription
max_age?integerHow 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".
urlstringRequired. The page to preview: a public http or https URL.
Example request body
{
  "url": "https://example.com/"
}
Example response · 200
{
  "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.

FieldTypeDescription
include_subdomains?booleanAlso keep URLs on other hosts under the same domain (docs.example.com for example.com). www. is always the same site. Default false.
links?booleanRead the links on the start page. Default true.
max_urls?integerThe most URLs to return. Default 1000.
search?stringKeep only URLs that contain this text (any case), e.g. "/blog/" or "pricing".
sitemap?booleanRead the site's sitemaps. Default true.
urlstringRequired. The site's start page: a public http or https URL.
Example request body
{
  "max_urls": 100,
  "url": "https://example.com/"
}
Example response · 200
{
  "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_..."
Response 200
{
  "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"}
Verify the signature

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);
});
Reply with any 2xx within 8 seconds. Anything else, including a redirect, a timeout or a connection error, is retried after 1, 2, 4, 8, 16 and 32 minutes, then 1 hour: 8 attempts over about two hours. Every attempt carries the same 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.