> ## Documentation Index
> Fetch the complete documentation index at: https://docs.querying.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Rendering: copias de tus páginas que los agentes de IA pueden leer

> querying.ai renderiza cada página que publicas tal como la ve una persona y escribe el texto de sus imágenes en su texto alternativo. Tu CDN guarda estas copias y las envía a ChatGPT y otros agentes de IA, mientras las personas siguen recibiendo tu sitio.

Los agentes de IA leen el HTML que envía tu servidor. No ejecutan scripts, así que los precios, reseñas y
especificaciones que dibuja un script nunca les llegan, y el texto dentro de una imagen solo lo leen de su texto
alternativo. AI Rendering renderiza cada página que publicas tal como la ve una persona, quita los scripts y escribe el
texto de cada imagen en su texto alternativo. Tu CDN descarga estas copias y responde con ellas a los agentes de IA. Las
personas, los buscadores y cualquier otra solicitud llegan a tu sitio como antes, y querying.ai nunca está en el camino de
una solicitud a tu sitio.

[GEO Score](/es/research/geo-score.md) mide la diferencia: `rendering.score` es la parte de una página que contiene una copia.
Cuando tu CDN envía la copia, los agentes que la reciben leen esa parte.

## Cómo funciona

1. **Conecta tu sitio.** Recibes una clave de sitio una sola vez. Solo puede leer las copias de tu sitio.
2. **Publica páginas.** Envía la URL de una página. querying.ai la renderiza desde el país de tu sitio, guarda la copia y
   vuelve a renderizar la página cada `refreshDays` días (28 por defecto).
3. **Sincroniza las copias.** Cada 10 minutos tu CDN lee la lista de copias con la clave de sitio, descarga las copias nuevas
   y elimina las que ya no aparecen.
4. **Responde a los agentes de IA.** Cuando un agente de IA envía un GET, tu CDN responde con la copia guardada. Una página
   sin copia, una copia caducada y cualquier error van a tu servidor de origen.

## Conectar un sitio

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/ai-rendering/sites \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "host": "www.example.com" }'
```

```json theme={"theme":"css-variables"}
{
  "success": true,
  "site": {
    "id": "5b0e9f52-6a3c-4f0e-8d0b-2c7e4a1f9d33",
    "host": "www.example.com",
    "refreshDays": 28,
    "country": "US",
    "keyPrefix": "rs_8f2c4a1b",
    "pages": 0,
    "createdAt": "2026-10-23T01:00:00.000Z",
    "updatedAt": "2026-10-23T01:00:00.000Z"
  },
  "key": "rs_8f2c4a1b…"
}
```

Guarda la `key`: solo esta respuesta la contiene y tu CDN la envía. `POST /v1/ai-rendering/sites/{id}/key` da una nueva y
detiene la anterior de inmediato.

| Campo | Obligatorio | Descripción |
| - | - | - |
| `host` | Sí | El nombre de host público que sirve tu CDN, como `www.example.com`. Conecta cada host una vez, hasta 20 por cuenta. |
| `refreshDays` | No | Días entre renderizados de una página publicada, `1`–`28`, por defecto `28`. El archivo de una copia caduca a los 30 días. |
| `country` | No | Desde dónde se renderizan las páginas: `US` (por defecto) o `KR`, como el país del lector de GEO Score. Elige el país desde el que navegan tus clientes. |

Cámbialos después con `PATCH /v1/ai-rendering/sites/{id}`.

## Publicar una página

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/ai-rendering/sites/5b0e9f52-6a3c-4f0e-8d0b-2c7e4a1f9d33/pages \
  -H "Authorization: Bearer $QUERYING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://www.example.com/products/sunscreen" }'
```

La página se está renderizando:

```json theme={"theme":"css-variables"}
{
  "success": true,
  "page": {
    "id": "a3d1c9e0-7b52-4e1f-9c84-0f6b2d5e7a10",
    "url": "https://www.example.com/products/sunscreen",
    "path": "/products/sunscreen",
    "status": "rendering",
    "copy": null,
    "taskId": null,
    "renderedAt": null,
    "expiresAt": null,
    "publishedAt": "2026-10-23T01:00:00.000Z",
    "refreshAt": "2026-10-23T07:00:00.000Z",
    "refreshTaskId": "7c4e2a90-1d3b-4f6a-8e25-9b0c1f3d4e57"
  },
  "taskId": "7c4e2a90-1d3b-4f6a-8e25-9b0c1f3d4e57"
}
```

Un renderizado tarda unos 30 segundos. Síguelo con `GET /v1/async/task/{taskId}` o lee el `status` de la página en
`GET /v1/ai-rendering/sites/{id}`:

* `ready`: la copia está en tu lista de copias. `copy` es su archivo HTML.
* `rendering`: se está creando la primera copia.
* `failed`: el primer renderizado falló. La página se vuelve a renderizar en `refreshAt`.
* `expired`: la copia caducó y salió de la lista hasta el próximo renderizado.

Un sitio admite hasta 1.000 páginas. Enviar de nuevo una página publicada la renderiza ahora. Una página es su ruta y su
consulta, sin parámetros de seguimiento como `utm_*` y `gclid`. `DELETE /v1/ai-rendering/sites/{id}/pages/{pageId}` retira
una página.

Para publicar la copia que hizo un [GEO Score](/es/research/geo-score.md) sin volver a renderizar, envía su id de tarea:
`{ "taskId": "0b6c1f7e-…" }`. Debe ser un GEO Score completado de tu cuenta, medido desde el país del sitio.

## Configurar tu CDN

Guarda esto como `ai-rendering.mjs` junto al código de tu CDN. `syncCopies` pone un almacén al día con tu lista de copias, y
`copyFor` responde desde él al GET de un agente de IA:

```js theme={"theme":"css-variables"}
export const COPIES_URL = "https://api.querying.ai/v1/ai-rendering/copies";

export const AGENTS = [
	"chatgpt-user",
	"codex",
	"claude-user",
	"claude-code",
	"perplexity-user",
	"gemini-deep-research",
	"duckassistbot",
	"amzn-user",
	"mistralai-user",
	"oai-searchbot",
];

export const DOWNLOADS_PER_SYNC = 40;

const EXPIRY_MARGIN_MS = 10 * 60_000;

const TRACKING_PARAM =
	/^(?:utm_[a-z0-9_]+|gclid|gbraid|wbraid|dclid|fbclid|msclkid|twclid|ttclid|srsltid|mc_cid|mc_eid|_ga|_gl)$/i;

export function pagePath(url) {
	const kept = [...url.searchParams].filter(([name]) => !TRACKING_PARAM.test(name));
	return (url.pathname || "/") + (kept.length ? "?" + new URLSearchParams(kept).toString() : "");
}

export async function copyKey(host, path) {
	const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(host.toLowerCase() + path));
	return "copy:" + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

export function agentRequest(request, agents = AGENTS) {
	if (request.method !== "GET") return false;
	const userAgent = (request.headers.get("user-agent") ?? "").toLowerCase();
	return agents.some((token) => userAgent.includes(token));
}

export async function syncCopies(options) {
	const {
		siteKey,
		store,
		copiesUrl = COPIES_URL,
		downloads = DOWNLOADS_PER_SYNC,
		fetch: send = fetch,
		now = Date.now,
	} = options;
	if (!siteKey) throw new Error("AI Rendering: no site key");
	const answer = await send(copiesUrl, { headers: { "X-Querying-Site-Key": siteKey } });
	if (!answer.ok) throw new Error(`AI Rendering: the copy list answered ${answer.status}`);
	const { host, copies } = await answer.json();
	const stored = await store.list();
	const listed = new Set();
	const result = { listed: copies.length, downloaded: 0, failed: 0, waiting: 0, removed: 0 };
	for (const copy of copies) {
		const key = await copyKey(host, copy.path);
		listed.add(key);
		if (stored.get(key) === copy.renderedAt || Date.parse(copy.expiresAt) - now() < EXPIRY_MARGIN_MS) continue;
		if (result.downloaded + result.failed >= downloads) {
			result.waiting++;
			continue;
		}
		try {
			const file = await send(copy.copy);
			if (!file.ok) throw new Error(String(file.status));
			await store.put(key, await file.arrayBuffer(), { renderedAt: copy.renderedAt, expiresAt: copy.expiresAt });
			result.downloaded++;
		} catch {
			result.failed++;
		}
	}
	for (const key of stored.keys())
		if (!listed.has(key)) {
			await store.delete(key);
			result.removed++;
		}
	return result;
}

export async function copyFor(request, store, agents = AGENTS) {
	if (!agentRequest(request, agents)) return null;
	try {
		const url = new URL(request.url);
		const copy = await store.get(await copyKey(url.hostname, pagePath(url)));
		if (!copy || !(Date.parse(copy.expiresAt) > Date.now())) return null;
		return new Response(copy.body, {
			status: 200,
			headers: {
				"Content-Type": "text/html; charset=utf-8",
				"Cache-Control": "private, no-store",
				Vary: "User-Agent",
				"X-Querying-Rendering": "HIT",
				"X-Querying-Rendered-At": copy.renderedAt,
			},
		});
	} catch {
		return null;
	}
}
```

**Cloudflare Worker**

El Worker guarda las copias en Workers KV y las sincroniza cada 10 minutos con un cron trigger. Cada copia caduca en KV
junto con su archivo.

```bash theme={"theme":"css-variables"}
npx wrangler kv namespace create AI_RENDERING
npx wrangler secret put QUERYING_SITE_KEY
```

`wrangler.toml`:

```toml theme={"theme":"css-variables"}
name = "ai-rendering"
main = "cloudflare-worker.mjs"
compatibility_date = "2026-10-01"
routes = [{ pattern = "www.example.com/*", zone_name = "example.com" }]
kv_namespaces = [{ binding = "AI_RENDERING", id = "<namespace id>" }]

[triggers]
crons = ["*/10 * * * *"]
```

`cloudflare-worker.mjs`:

```js theme={"theme":"css-variables"}
import { copyFor, syncCopies } from "./ai-rendering.mjs";

export function kvStore(kv) {
	return {
		async get(key) {
			const { value, metadata } = await kv.getWithMetadata(key, "arrayBuffer");
			return value && metadata ? { body: value, ...metadata } : null;
		},
		async list() {
			const stored = new Map();
			let cursor;
			do {
				const page = await kv.list({ prefix: "copy:", cursor });
				for (const key of page.keys) stored.set(key.name, key.metadata?.renderedAt);
				cursor = page.list_complete ? undefined : page.cursor;
			} while (cursor);
			return stored;
		},
		put: (key, body, meta) =>
			kv.put(key, body, { expiration: Math.floor(Date.parse(meta.expiresAt) / 1000), metadata: meta }),
		delete: (key) => kv.delete(key),
	};
}

export default {
	async fetch(request, env) {
		return (await copyFor(request, kvStore(env.AI_RENDERING))) ?? fetch(request);
	},
	async scheduled(_controller, env, ctx) {
		ctx.waitUntil(syncCopies({ siteKey: env.QUERYING_SITE_KEY, store: kvStore(env.AI_RENDERING) }));
	},
};
```

En el plan Workers Free, KV admite 1.000 escrituras al día, así que la primera sincronización de un sitio grande puede
tardar más de un día.

**Otros CDN y servidores**

Usa el mismo módulo con un almacén de tu plataforma: un objeto con `get(key)`, `list()`, `put(key, body, meta)` y
`delete(key)`. `list()` devuelve cada clave guardada con el `renderedAt` con que se guardó. Ejecuta `syncCopies` de forma
programada y `copyFor` en cada solicitud de página. Si tu CDN guarda en caché solo por URL, añade el agente a la clave de
caché para que una persona nunca reciba una copia.

Cuando termine la siguiente sincronización, comprueba una página publicada tal como la abriría ChatGPT. La copia lleva
`X-Querying-Rendering: HIT`:

```bash theme={"theme":"css-variables"}
curl -sI -A "Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/bot)" https://www.example.com/products/sunscreen
```

## La lista de copias

Tu CDN lee `GET https://api.querying.ai/v1/ai-rendering/copies` con la clave de sitio en `X-Querying-Site-Key`:

```json theme={"theme":"css-variables"}
{
  "success": true,
  "host": "www.example.com",
  "copies": [
    {
      "path": "/products/sunscreen",
      "url": "https://www.example.com/products/sunscreen",
      "copy": "https://api.querying.ai/v1/evidence/1792717200-9f3c2a7d1e4b.html",
      "renderedAt": "2026-10-23T01:00:31.000Z",
      "expiresAt": "2026-11-22T01:00:00.000Z"
    }
  ]
}
```

Incluye cada página publicada cuya copia no ha caducado. `copy` se descarga sin clave. `renderedAt` cambia cuando la página
se vuelve a renderizar, así que descarga una copia solo cuando su `renderedAt` sea nuevo. Elimina las copias guardadas de
las páginas que ya no están en la lista. La lista y las descargas son gratuitas.

## Agentes

`AGENTS` en el módulo decide quién recibe la copia, por un token en cualquier parte del User-Agent, sin distinguir
mayúsculas.

* **Por defecto:** los agentes que una persona envía a una página mientras hace una pregunta (`chatgpt-user`, `codex`,
  `claude-user`, `claude-code`, `perplexity-user`, `gemini-deep-research`, `duckassistbot`, `amzn-user`, `mistralai-user`) y
  el rastreador de la búsqueda de ChatGPT, `oai-searchbot`. La búsqueda de ChatGPT responde desde la página que guarda su
  rastreador, que es lo que mide el `score` de GEO Score. Su índice cambia después de que OpenAI vuelva a rastrear la página.
* **Añade si quieres:** `claude-searchbot` y `perplexitybot`.
* **Déjalos fuera:** rastreadores de entrenamiento (GPTBot, ClaudeBot) y de búsqueda web (Googlebot, Bingbot). Googlebot
  ejecuta los scripts por sí mismo.

## Cumple las reglas de los buscadores

Enviar a los bots un contenido distinto del que ven las personas es cloaking. Google no considera cloaking una página
renderizada para bots mientras el contenido sea similar
([Dynamic rendering](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering)). OpenAI no ha
publicado ninguna política de cloaking, así que ajusta la copia a la regla de Google:

* La copia es la página que dibujó el navegador de una persona. querying.ai quita los scripts y solo añade el texto de dentro
  de las imágenes, en su texto alternativo, el lugar del texto de una imagen.
* Se sirve en la propia URL de la página.
* Se mantiene al día. querying.ai la vuelve a renderizar cada `refreshDays`, y el módulo deja de enviar una copia cuando
  caduca.
* Si editas una copia, solo quita o corrige: arregla un carácter mal leído, elimina una ventana emergente. No añadas
  afirmaciones, precios ni enlaces que las personas no ven.

## Qué es la copia

La copia es el HTML que dibujó el navegador de una persona después de desplazarse hasta el final de la página: el texto que
cargaron los scripts, la página de cada marco en su lugar, todos los scripts eliminados salvo los datos estructurados
(JSON-LD), y el texto de cada imagen grande añadido a su texto alternativo. Los enlaces y las fuentes de imagen siguen siendo
absolutos, así que la copia se lee como la página. Es la página en su último renderizado: un cambio de precio o de stock llega
a los agentes en el siguiente renderizado, como mucho `refreshDays` después, más el tiempo hasta que tu CDN sincroniza. El
texto de las imágenes se copia tal como se leyó, así que un carácter mal leído sale tal cual.

## Facturación

Un renderizado cuesta 5 créditos, más 5 por cada imagen cuyo texto se lee y 2 por cada imagen que tu cuenta leyó en los
últimos 90 días (el mismo archivo, leído de la misma forma), con un máximo de 12 imágenes. Al enviar se reservan 65 créditos y al
terminar se cobra lo que usó el renderizado. Un renderizado fallido no se cobra. Una actualización cuesta lo mismo y se cobra a la
cuenta del sitio; una cuenta sin créditos espera una hora antes de volver a intentar la página. Conectar un sitio, publicar la
copia de un GEO Score, la lista de copias y la descarga de copias son gratuitos. Tu CDN envía las copias, así que el tráfico de
agentes no cuesta nada en querying.ai. Consulta [Créditos y límites](/es/concepts/credits-and-limits.md).

## Errores

| Estado | Código | Causa |
| - | - | - |
| `401` | `UNAUTHORIZED` | La lista de copias no recibió clave de sitio, recibió una clave reemplazada o la de un sitio eliminado. |
| `402` | `INSUFFICIENT_CREDITS` | El saldo no cubre la reserva de 65 créditos de un renderizado. |
| `403` | `KEY_SCOPE_DENIED` | La clave de API está limitada a ciertos motores y `GEO_SCORE` no es uno de ellos. |
| `404` | `NOT_FOUND` | No hay ningún sitio, página o GEO Score de tu cuenta con ese id. |
| `409` | `SITE_EXISTS`, `SITE_LIMIT`, `PAGE_LIMIT`, `TASK_NOT_COMPLETED` | El host ya está conectado, la cuenta tiene 20 sitios, el sitio tiene 1.000 páginas o el GEO Score no ha terminado. |
| `410` | `COPY_EXPIRED` | La copia del GEO Score caducó. Publica la página por URL. |
| `422` | `VALIDATION_ERROR`, `NO_RENDERED_COPY` | Un campo no es válido, la URL está en otro host, el GEO Score se midió desde otro país o no generó copia. |
| `429`, `503` | | La concurrencia del plan o la cola de renderizado está llena. Reintenta tras `Retry-After`. |

Los endpoints están en la [referencia de la API](/es/api-reference/ai-rendering/create-site.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.