# agentsite API — detailed reference
The AgentSite API is an HTTP render service that returns a clean markdown representation of any client-rendered SPA, plus a detailed JSON-LD schema bundle, OpenGraph meta, and a screenshot. One endpoint — /render?url= — serves the rendered page; /bundle?url= returns the raw RenderBundle JSON.
By AgentSite · Updated 2026-05-29
Designed for AI agents that don't execute JavaScript, the API exposes one primary endpoint — `/render?url=` — and a JSON-shaped sibling at `/bundle?url=` for callers that want the raw bundle without HTML wrapping.
> Brief version: [https://api.agentsite.app/llms.txt](https://api.agentsite.app/llms.txt) Machine-readable: [https://api.agentsite.app/openapi.yaml](https://api.agentsite.app/openapi.yaml)
* * *
## Public endpoints
| Method | Path | Body |
| --- | --- | --- |
| GET | /render?url={url} | Final asset bytes dispatched by URL path. `/robots.txt` → `text/plain`; `/sitemap.xml` → `application/xml`; `/llms.txt` / `/llms-full.txt` → `text/plain`; `/.well-known/*` → per-path; `*.md` → `text/markdown`; anything else → `text/html` (HTML branch fetches the customer's SPA shell from `<host>/<indexPath>` and runs `applyBundle` server-side). `Accept: application/json` on the HTML branch returns the legacy RenderBundle JSON for deployed snippets. The **Nginx** install pattern points at this for every path class. |
| GET | /bundle?url={url} | `application/json` — RenderBundle. Explicit-name endpoint for the JSON contract; new callers prefer this over `/render` + `Accept: application/json`. |
| GET | /render.md?url={url} | `text/markdown` — YAML frontmatter + body |
| GET | /snippet.js | drop-in Express handler for self-hosted SPA fallback |
There are also internal endpoints under `/site`, `/api-token`, `/billing`, etc. that are scoped to the dashboard at [https://agentsite.app](https://agentsite.app) and require a logged-in session — not part of the public agent-facing surface and not documented here.
* * *
## URL parameter
The full target URL goes in a single `url` query parameter, URL-encoded:
```
GET /render?url=https%3A%2F%2Fexample.com%2Fpath
```
The controller parses host + path-and-search internally. The query string **is** part of the cache key — `/page?utm_source=email` and `/page?utm_source=twitter` are separate cache entries. Canonicalize on the caller side if you don't want marketing parameters fragmenting the cache.
Only `http:` and `https:` URLs are accepted; everything else 400s.
* * *
## Auth
**`Authorization: Bearer <token>` is required on every call.** No token = 401. There is no public mode and no host-based attribution.
> **Agents: read [https://api.agentsite.app/agent.md](https://api.agentsite.app/agent.md) first — it's tuned for you.** This section is the customer-side reference.
Two token shapes share the bearer slot. `AuthGuard` tries JWT first; on miss, it falls back to a PAT lookup.
### JWT session token
Issued by Google OAuth / magic-link / cookie session. Identifies a human user. **1-hour TTL.** **Full scope by definition** — a logged-in human is already trusted with everything they own, so scope checks are bypassed entirely. This is what the dashboard at [https://agentsite.app](https://agentsite.app) uses.
### Personal Access Token (PAT, `asit_…`)
DB-backed (`api_token` table, SHA-256 hashed at rest, plaintext shown once on creation). **This is what agents and the snippet runtime use.** Carries an explicit `scopes text[]` allow-list + optional `expires_at`. Mint and revoke at [https://agentsite.app/settings/tokens](https://agentsite.app/settings/tokens).
Scope vocabulary:
| Scope | Grants |
| --- | --- |
| `sites:read` | `GET /site`, `GET /site/:id/*` (config, file previews, usage, paths, rendered pages, bot telemetry). |
| `sites:write` | `POST /site` (create), `PATCH /site/:id/*` (every setting), `POST /site/:id/regenerate*`, `POST /site/:id/cache/expire`, `POST /site/:id/snippet-probe`, `POST /site/:id/config/discover`. |
| `reports:read` | `GET /aeo-report/:id`, `GET /aeo-report/:id/schema-patch.json`. |
| `tokens:read` | `GET /api-token` (list the account's tokens). |
A PAT created today defaults to `['sites:read','sites:write','reports:read']` — comfortable for snippet runtime + most agent loops, including the Claude-driven onboarding flow (which calls `POST /site`). Mint a tighter token when handing an agent its own (e.g. `sites:read` only for a read-only auditor agent).
Expired PATs return 401 — same code path as a bad token.
### Session-only endpoints (reject PATs with 403)
Some endpoints reject PAT bearers regardless of scopes, because the action is too destructive to let an autonomous agent do without an interactive human in the loop:
- `DELETE /site/:id` — offboarding needs an interactive human.
- `POST /site/:id/rotate` — rotating a site token invalidates the running snippet.
- `POST /api-token`, `POST /api-token/:id/rotate`, `DELETE /api-token/:id` — a PAT can't mint or revoke another PAT.
- All `BillingController` writes — money-touching stays human.
- All `/admin/*` — admin uses JWT (cookie session) only.
Site **creation** (`POST /site`) is intentionally PAT-accessible (default `sites:write` scope is sufficient) so the Claude-driven onboarding flow can register a domain without bouncing the user back into a dashboard ritual.
### Per-operation scope contract
The OpenAPI artifact at [https://api.agentsite.app/openapi.yaml](https://api.agentsite.app/openapi.yaml) declares the PAT scope each operation requires in `security.bearer`; an empty array there means any authenticated bearer is accepted. Operations whose description begins `"(Session-only — PATs rejected)"` are JWT-only.
### Snippet-side token handling
> **Important:** the token is server-side only. Your snippet runs in _your_ Express server, with the token in env. Never ship the token to the browser; if you do, anyone can render arbitrary sites at your expense.
The pre-Chunk-10 `X-AgentSite-Token` header is gone — sending it returns **410 Gone** with a migration message. Re-download `/snippet.js` to get the current bytes.
### Public demo hosts
A small allow-list of "public demo" hosts (configured via the `RENDER_PUBLIC_DOMAINS` env var on the API; defaults to our own marketing domains) renders without a token. This is what powers the live demo on the landing page. The exception is host-pinned and carries no usage attribution — you cannot opt your own host into the public list, only the API operator can.
* * *
## Cache
The render service has its own cache (Postgres, keyed by site+path+engine, 24-hour TTL).
- **Default:** cache hit → return immediately. Cache miss → return a placeholder bundle (title `agentsite — generating`, empty markdown) and queue the real render. Retry the same URL ~2-5s later for the real bundle.
- **`Cache-Control: no-cache`** → bypass the cache, render synchronously, write through. Use this to prime a route or when you genuinely need a fresh render. Latency: 1-5s.
### Response headers
| Header | Values | Notes |
| --- | --- | --- |
| `X-Cache` | `HIT` / `MISS` / `BYPASS` | Standard CDN vocabulary. `HIT` = served from cache; `MISS` = not in cache; `BYPASS` = cache skipped. |
| `X-Render-Status` | `ready` / `generating` | `generating` means the body is a placeholder; the real render is queued. `ready` means real content. |
| `X-Render-Engine` | e.g. `playwright` | Diagnostic only. Not a stable contract. |
| `Last-Modified` | RFC 1123 date | When the bundle was rendered. Suppressed on placeholders. |
| `Expires` | RFC 1123 date | Cache expiry. Suppressed on placeholders. |
| `Cache-Control` | `no-store` (placeholder responses) | Prevents downstream caches from pinning the placeholder. |
### Identifying placeholders
```http
HTTP/1.1 200 OK
X-Cache: MISS
X-Render-Status: generating
Cache-Control: no-store
```
Body `title` will be `"agentsite — generating"` and `markdown` will be empty. Retry the same URL after a short delay.
* * *
## RenderBundle
```ts
interface RenderBundle {
url: string; // The full URL that was rendered.
title: string; // <title> or 'agentsite — generating' on a placeholder.
description?: string; // <meta name="description"> if present.
tldr?: string; // 40-60 word direct-answer paragraph — see "TL;DR" below.
ogImage?: string; // <meta property="og:image"> if present.
screenshotUrl?: string; // Public URL of a viewport screenshot (if storage is configured).
markdown: string; // The rendered page as markdown. Empty on placeholders.
schemaGraph?: SchemaThing[]; // Detected schema.org JSON-LD entries — see "Schema.org JSON-LD" below.
renderedAt: string; // ISO 8601.
expiresAt?: string; // ISO 8601 — when the cache entry expires.
engine?: string; // Diagnostic; do not depend on the value.
cacheStatus?: 'hit' | 'miss-generating' | 'forced-sync' | 'live';
}
```
The same fields are returned as YAML frontmatter on `/render.md` (with snake\_case keys: `og_image`, `screenshot_url`, `rendered_at`, `expires_at`, `cache_status`, `schema_jsonld`). The markdown body follows the closing `---`. The `schema_jsonld` value is the JSON-stringified schema graph — `JSON.parse` it to recover the structure.
### Markdown response example
```
---
url: "https://example.com/"
title: Example Domain
rendered_at: "2026-05-05T16:00:31.000Z"
expires_at: "2026-05-06T16:00:31.000Z"
engine: playwright
cache_status: hit
schema_jsonld: "[{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"headline\":\"Example Domain\",\"url\":\"https://example.com/\"}]"
---
# Example Domain
This domain is for use in documentation examples without needing permission. Avoid use in operations.
[Learn more](https://iana.org/domains/example)
```
* * *
## /env.js — runtime config injection
When a customer needs runtime environment variables in the browser (different config per environment without rebuilding their SPA), the snippet can serve `/env.js` directly from the Node process env. Pass an `envAllowList` option to the snippet:
```js
app.get('*', agentsite({
distDir: DIST,
site: 'https://yoursite.com',
token: process.env.AGENTSITE_TOKEN,
envAllowList: ['PUBLIC_*', 'BUILD_*', 'POSTHOG_KEY'],
}));
```
`index.html` loads it before `main.js`:
```html
<script src="/env.js"></script>
```
Behavior:
- Only keys matching the allow-list are emitted. Patterns ending in `*` match by prefix; exact strings match exactly.
- `AGENTSITE_TOKEN` is **hard-blocked** even if it matches a pattern. Defense in depth — the snippet will never leak your token through this route.
- Customer-shipped `dist/env.js` wins (express.static order).
- Without `envAllowList` configured, `/env.js` returns 404.
- Response: `window.env = { … };` followed by a newline, with `Cache-Control: no-cache` (the env can change per process restart).
This replaces the manual `jq + Dockerfile shell-script` pattern documented elsewhere in this file as the canonical Express-on-Docker setup. Less infrastructure to maintain; same result; no risk of accidentally exposing the token.
* * *
## Granular robots.txt
The snippet auto-serves `/robots.txt` on the customer's domain with a heavily commented, granular AI-bot policy. Customer-shipped `dist/robots.txt` always wins (express.static runs before the snippet). Default policy:
- **Allow** search/citation bots — content surfaces in AI search: `OAI-SearchBot`, `Claude-SearchBot`, `PerplexityBot`, `Bingbot`, `MSNBot`.
- **Allow** user-initiated bots — fetched only when a real user asks an AI to read a page: `ChatGPT-User`, `Claude-User`.
- **Disallow** training-data bots — fetched to train future foundation models: `GPTBot`, `ClaudeBot`, `Google-Extended`, `Meta-ExternalAgent`, `Applebot-Extended`.
- **Allow** `AgentSiteBot` — our own crawler.
- **Allow** `*` — Googlebot, Bingbot, classic crawlers.
Plus a `Sitemap:` line pointing at `https://{host}/sitemap.xml` and a comment pointing at `/llms.txt`. The file is heavily commented so a customer who curls it can see exactly what each section does and why.
API endpoint: `GET /robots-policy?site=yoursite.com` with the customer's bearer token. 24 h snippet cache.
* * *
## llms.txt + llms-full.txt
The customer's snippet automatically routes `/llms.txt` and `/llms-full.txt` requests on their own domain to a generated index built from the cached `RenderBundle` rows for that host. Customers who ship a real `dist/llms.txt` win automatically — `express.static` runs before the snippet handler, so a real file is served without ever reaching the generator.
The index follows the [llmstxt.org](https://llmstxt.org/) format:
```
# yoursite.com
> 40-60 word summary from the homepage tldr.
## Pages
- [Home](https://yoursite.com/): summary
- [About](https://yoursite.com/about): summary
- ...
## Optional
- [Sign in](https://yoursite.com/auth/sign-in): summary
- ...
```
`/llms-full.txt` adds the full markdown body of every entry under a `# {title}` heading per page. Same cap (300 entries by default).
Behavior:
- Capped at 300 entries; sorted shallow-paths-first then alphabetical.
- Auth/admin/settings/account/billing/api paths land in the **Optional** section.
- Each entry's summary prefers `tldr` over `description`, truncated to 160 chars.
- Snippet caches the API response for 1h.
- API endpoint: `GET /llms-index?site=yoursite.com` with the customer's bearer token.
If your site has zero cached bundles yet, the generator returns a short placeholder explaining that no pages have been rendered. Visit the site once with the snippet wired up and the index populates automatically.
* * *
## TL;DR
Each render produces a `tldr` field — a 40 to 60 word direct-answer paragraph derived from the page content. Two passes:
- **Heuristic** picks the first prose paragraph in the rendered markdown. Skips headings, list items, code blocks, blockquotes, table rows, and link-only nav rows. Returns the paragraph as-is if it already fits the 40-80 word window.
- **Always-on LLM-assist** (`gpt-4o-mini`) refines candidates that are too short or too long into a self-contained 40-60 word answer. Failure-safe — any LLM error returns the heuristic candidate (when it has at least 30 words) or undefined.
The `tldr` is used:
1. As `<meta name="description">` only-if-absent in the snippet (and the original `description` wins when present).
2. As the Article schema's `description` slot — preferred over the raw bundle description because it's pre-cleaned to direct-answer shape.
3. As a `tldr:` line in the `/render.md` YAML frontmatter so any downstream tool can recover it without re-extracting.
Pages with insufficient prose (< 200 chars markdown, or no qualifying paragraph) get `tldr: undefined` and the bundle keeps using the raw description.
* * *
## [Schema.org](http://Schema.org) JSON-LD
Every render extracts standards-compliant [schema.org](http://schema.org) JSON-LD into `schemaGraph`. The snippet then emits one `<script type="application/ld+json" data-agentsite="schema" data-type="…">` block per entry, **only-if-absent** against any JSON-LD the customer already shipped on the page. Customer overrides always win.
### Detected types
| @type | Trigger |
| --- | --- |
| Article | Any non-trivial page with a title. Pulls headline / description / image / publisher / dates from the bundle. |
| FAQPage | ≥2 `## Question?` heading + answer-paragraph pairs in the markdown. Always-on LLM-assist refines this from prose. |
| HowTo | A section heading followed by an ordered list of ≥2 steps where each step starts with an imperative verb. |
| BreadcrumbList | DOM nav matching `nav[aria-label*=breadcrumb i]`, `[role=navigation] ol`, `.breadcrumb`, or `ol.breadcrumbs`. |
| Organization | Host-level — built from `og:site_name` (or hostname), header logo, and social links from header/footer. |
### Carry-through rule
Before emitting anything, the renderer reads every existing `<script type="application/ld+json">` on the page and parses out the `@type` values (handles plain objects, arrays, and `{@graph: [...]}` containers). Any candidate matching one of those types is dropped — we never duplicate. The customer's hand-rolled blob stays in place verbatim, indentation and comments preserved.
### Snippet emission rule
```html
<!-- Cache metadata sentinel — not real schema. -->
<script type="application/ld+json" data-agentsite="bundle">{...}</script>
<!-- One per detected @type, only-if-absent against the customer's existing JSON-LD. -->
<script type="application/ld+json" data-agentsite="schema" data-type="Article">{...}</script>
<script type="application/ld+json" data-agentsite="schema" data-type="FAQPage">{...}</script>
<script type="application/ld+json" data-agentsite="schema" data-type="BreadcrumbList">{...}</script>
```
The two `data-agentsite` values are distinct on purpose: `bundle` carries the cache metadata we use for diagnostics; `schema` carries real [schema.org](http://schema.org). Filter by attribute when debugging.
### Always-on LLM-assist (FAQ scope)
After heuristics produce candidates, a single `gpt-4o-mini` call refines the FAQ candidate by extracting prose-embedded Q/A pairs that don't follow the `## Question?` heading convention. Skipped when markdown < 200 chars or when the customer already shipped FAQPage. Failure-safe: any LLM error returns the heuristic candidate unchanged.
### Failure mode
Schema extraction failure never breaks a render — the `schemaGraph` just comes back empty. The snippet emits nothing in that case; your existing tags stand on their own.
* * *
## Snippet
`/snippet.js` is a CommonJS Express handler. Customers `curl` it once, require it from their server, and use it as their SPA fallback. No npm dependency on us — re-curl to get fixes.
```js
const express = require('express');
const path = require('path');
const agentsite = require('./agentsite');
const app = express();
const DIST = path.join(__dirname, 'dist');
app.use(express.static(DIST, { index: false }));
app.get('*', agentsite({
distDir: DIST,
site: 'https://yoursite.com', // must match a registered domain
token: process.env.AGENTSITE_TOKEN, // optional
// apiBase, ttlMs, fetchTimeoutMs, log, logger all optional
}));
app.listen(3000);
```
### How head transforms work
The snippet treats your `index.html` as the canonical document and **refreshes attribute values in place** rather than re-emitting the head. Indentation, attribute order, comments, and ordering of your tags are preserved.
For each request:
1. Read the customer's `index.html` once at startup. (No pre-splitting.)
2. Call `GET /bundle?url=https://{site}{path}` with `Authorization: Bearer <token>`. (Older deployed snippets call `/render` with `Accept: application/json` — same JSON contract, kept working for back-compat.)
3. Run `applyBundle(indexHtml, bundle)`:
- **In-place refresh** (existing tag found → its content/href is updated): `<title>`, `<meta name="description">`, `og:title`, `og:description`, `og:image`, `twitter:title`, `twitter:description`, `twitter:image`.
- **Insert if absent** (developer per-page choice wins if present): `og:type` (default `website`), `twitter:card` (default `summary_large_image`), `og:url`, `rel="canonical"`.
- **Upsert** (replace if found, insert otherwise): `<script type="application/ld+json" data-agentsite="bundle">` — keeps cache metadata current.
- **[Schema.org](http://Schema.org) JSON-LD** (only-if-absent by `@type`): one `<script type="application/ld+json" data-agentsite="schema" data-type="…">` per entry in `bundle.schemaGraph`. Skipped for any `@type` already present in any existing `application/ld+json` block on the page (parses plain objects, arrays, and `@graph` containers).
- **Bottom insert before `</head>`** for any tag not already in the shell, in call order. New tags land just before `</head>` so the developer's lead-with-their-tags layout is preserved.
4. Wrap `bundle.markdown` in `<noscript><pre data-content-type="text/markdown" data-source="agentsite">` and splice it just before `</body>`. `<noscript>` is hidden when JavaScript runs (so it's invisible to humans on a normal browser visit) but is part of the parsed DOM, so crawlers and any tool that strips scripts still see the raw markdown. Earlier versions used `<script type="text/llms.txt">`, but a number of web-cloud previews and paste sandboxes strip `<script>` tags, eating the markdown.
5. Cold-render placeholders (`X-Render-Status: generating`) are returned as the **unmodified shell** — your defaults stand until the real bundle lands. The placeholder title doesn't leak into your page.
6. Fail-open on any error (network, 5xx, timeout): unmodified shell, never blocks your site.
### Markdown delivery on the customer's domain
The snippet detects two signals on incoming requests and serves the rendered markdown directly with `Content-Type: text/markdown; charset=utf-8`, bypassing the HTML transform:
- **`Accept: text/markdown`** — Mintlify-style content negotiation. Any explicit mention of `text/markdown` in the Accept header wins (q-values are accepted; `*/*` does NOT trigger).
- **`.md` path suffix** — per-page mirror. `/pricing.md` looks up the cached bundle for `/pricing` and returns its markdown body.
Behavior:
- The same in-memory bundle cache backs both forms — `/pricing.md` and a subsequent `/pricing` HTML request share one fetch.
- Suffix-driven `.md` requests with no bundle return **404** (a stale `/pricing.md` returning the homepage shell would be worse than nothing).
- Accept-driven requests with no bundle **fall through** to the HTML path — better to render something useful than 404 a browser whose Accept list happens to include markdown.
- Stripped lookup paths must be cleanly extensionless. `/foo.png.md` → 404 (the path is an asset, not an SPA route).
This pairs cleanly with `/render.md` on the API: the API serves markdown for any URL with a token; the snippet serves markdown for the customer's own host without one.
### Body-injection mode: `noscript` vs `preboot`
The snippet ships the rendered markdown into the HTML `<body>` in one of two shapes. Pick with `bodyMode` on the snippet, or override per-request with `?_agentsite_body=preboot` (or the `X-Agentsite-Body:` header) for A/B testing without redeploying.
- **`noscript` (default)** — `<noscript><pre data-content-type="text/markdown">…</pre></noscript>` spliced before `</body>`. Survives most agent fetchers including Claude Code's WebFetch, Perplexity, and GPTBot. Stripped by [Claude.ai](http://Claude.ai)'s in-chat web tool (which prunes `<noscript>` during HTML extraction).
- **`preboot`** — same escaped `<pre>` payload, wrapped in a visible `<div id="agentsite-preboot" aria-hidden="true">` outside your SPA's mount point. An inline `<script>` + `<style>` at the bottom of `<head>` sets `data-agentsite-js="1"` on `<html>` during HTML parsing and hides the div via CSS, so JS-capable browsers never paint it. Extractors that don't execute JS ([Claude.ai](http://Claude.ai)'s web tool, most readability libraries) read the markdown as plain text.
`preboot` is the same response for every UA — no UA branching, no cloaking. It's the same content as the rendered SPA page, just made legible to clients that don't run JS. If your SPA mounts into a node other than `#app`, that's fine: the preboot div sits as a sibling and is removed by the agentsite client-side cleanup (`document.getElementById('agentsite-preboot')?.remove()`) after your SPA mounts. Add the same line after your own mount call if you don't import `@agentsite/client`.
```js
app.use(agentsite({
distDir: DIST,
site: 'https://yoursite.com',
token: process.env.AGENTSITE_TOKEN,
bodyMode: 'preboot', // default: 'noscript'
}));
```
### Per-page image gotcha
`bundle.ogImage` reflects whatever `<meta property="og:image">` was in the DOM **after JavaScript ran during the headless render**. If your SPA uses a meta service (e.g. `useMeta()`, `@vueuse/head`, `react-helmet`) to set per-page `og:image` at runtime, the per-page image flows through correctly.
If your SPA only sets a global `og:image` at `index.html` load time and never updates it client-side, **every page shares that one image** in share-card scrapers. That's not a snippet bug — it's a consequence of agentsite seeing what an LLM crawler would see. Fix on the consumer side: have your route's mount/setup code call your meta service with the per-page image.
Cache-busting: `/snippet.js?v=<anything>` if the CDN-cached version is stale.
* * *
## Install
**Source of truth: [https://api.agentsite.app/install.md](https://api.agentsite.app/install.md)** — that page has the full step-by-step (Dockerfile, server.mjs, package.json, nginx.conf paste, per-platform deltas, verification command). Summary of the install patterns:
- **Express** — static SPA (your build produces a `dist/`): agentsite replaces your static file server. Single Node process, ~14-line Dockerfile.
- **Express-Sidecar** — non-streaming SSR framework (Next pages router, Nuxt 2, Remix, SvelteKit non-streaming, Rails, Django, WordPress, PHP-FPM): agentsite sidecars in front via `upstream:` mode. Two processes (your framework + agentsite), one container, started by `entrypoint.sh`.
- **Edge** — edge runtime / PaaS (Vercel / Cloudflare Pages / Netlify): Fetch-API port `agentsite-edge.mjs` + the platform's edge middleware shell.
- **Nginx** — thin nginx, no code (nginx-fronted static SPA): customer adds a few `location` + `proxy_pass` directives to their existing `nginx.conf`. agentsite owns the HTML byte stream including reach-back to `<host>/index.html` for the SPA shell. No Node sidecar, no edge function — install is a config paste and a reload. The nginx template must carry `proxy_set_header Authorization "Bearer <SITE_TOKEN>";` on every `location` block, otherwise non-public-domain hosts get 401.
Reference implementations live at `demos/after/{vue,react,next}` in the source repo — byte-for-byte the result of following the install guide for Express and Express-Sidecar. The Nginx E2E fixture lives at `devops/pattern-f-test/` — docker-compose with nginx + an api stub + a 20-assertion test script.
**Patterns on the roadmap** (not yet shipped — see [install.md](http://install.md) → "Roadmap"): SDK (framework SDK called from a render hook), njs (nginx-native via njs port), CloudFront (Lambda@Edge), Python (Django / Flask / FastAPI middleware), PHP (PHP-FPM hook / WordPress plugin).
* * *
## Examples
All examples need a token. Get one at [https://agentsite.app/auth/sign-up](https://agentsite.app/auth/sign-up).
### JSON bundle
```bash
curl -H "Authorization: Bearer $AGENTSITE_TOKEN" \
'https://api.agentsite.app/bundle?url=https%3A%2F%2Fyoursite.com%2Fpricing'
```
### Markdown for an LLM agent
```bash
curl -H "Authorization: Bearer $AGENTSITE_TOKEN" \
'https://api.agentsite.app/render.md?url=https%3A%2F%2Fyoursite.com%2Fpricing'
```
### Final injected HTML (Nginx install pattern)
```bash
curl -H "Authorization: Bearer $AGENTSITE_TOKEN" \
'https://api.agentsite.app/render?url=https%3A%2F%2Fyoursite.com%2F'
```
### Force a fresh render
```bash
curl -H "Authorization: Bearer $AGENTSITE_TOKEN" \
-H 'Cache-Control: no-cache' \
'https://api.agentsite.app/bundle?url=https%3A%2F%2Fyoursite.com%2F'
```
* * *
## Errors
| Status | When |
| --- | --- |
| 400 | `url` missing, malformed, or non-http(s). |
| 401 | `Authorization: Bearer <token>` missing or invalid. |
| 410 | Legacy `X-AgentSite-Token` header sent. Use `Authorization: Bearer` instead. |
| 500 | Renderer crashed. Retry; if persistent, file an issue. |
* * *
## Related concepts
The endpoints above generate the artifacts the AEO model describes:
- **[The Five Layers of AEO](/five-layer-aeo)** — the model the snippet's outputs map to. `/render.md` populates Layer 2; the JSON-LD graph populates Layer 3; the `tldr` field is the Layer-4 [direct answer](/direct-answer).
- **[SSR-junk and bot walls](/ssr-junk-bot-wall)** — the two Layer-1 failures the snippet is built to bypass. The curl probes in that page diagnose any site, including pre-install verification.
- **[Statistics and citations](/statistics-citations)** — the Layer-4 content pattern that earns citations once the bot can read the page.
- **[Agent readability](/agent-readability)** — the thesis: why all the above matters at all.
* * *
## Source
- Repository: [https://github.com/agentsite/integration](https://github.com/agentsite/integration)
- Brief reference: [https://api.agentsite.app/llms.txt](https://api.agentsite.app/llms.txt)
- OpenAPI: [https://api.agentsite.app/openapi.yaml](https://api.agentsite.app/openapi.yaml)
- Marketing site: [https://agentsite.app](https://agentsite.app)