Cada petición a la Content API lleva una clave de sitio. Nos dice de qué sitio estás leyendo, y básicamente eso es todo lo que hace. Sin bailes de OAuth, sin refrescar tokens, sin firmar peticiones a medianoche. Una cadena en una cabecera, y estás dentro.
Qué es una clave de sitio
Una clave de sitio tiene esta pinta:
pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40- El prefijo
pk_seguido de exactamente 32 caracteres hexadecimales en minúscula:^pk_[a-f0-9]{32}$. - Pertenece a un solo sitio. La clave por sí sola decide de quién es el contenido que recibes: en toda la API no hay ningún parámetro «site id».
- Es de solo lectura y solo ve contenido publicado.
- Un sitio puede tener varias claves a la vez, lo que permite rotarlas sin dolor (más sobre eso más abajo).
El pk_ es un guiño a las «publishable keys» que quizá conozcas de los proveedores de pago: una clave pensada para vivir en código público. Enseguida vemos por qué no pasa nada.
Dónde conseguir una clave
Abre tu sitio en el CRM
Entra en Diil y elige el sitio cuyo contenido quieres leer.Ve a Ajustes → Claves de Content API
Aquí viven todas las claves del sitio.Crea una clave
Recibes una cadenapk_…nuevecita. Cópiala.Guárdala en una variable de entorno
No directamente en el código: tu yo del futuro, el que rota claves, te lo agradecerá. Los patrones para los frameworks más populares están más abajo.
Cómo enviar la clave
Pon la clave en la cabecera de petición x-crm-key. Es la forma estándar y funciona igual desde servidores que desde navegadores: CORS permite esa cabecera desde cualquier origen.
curl "https://back.sitecog.com/content/v1/pages/home?lang=en" \
-H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
const page = await res.json();GET /content/v1/pages/home?lang=en HTTP/1.1
Host: back.sitecog.com
x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40La alternativa ?key=
Si de verdad no puedes poner una cabecera, pasa la clave como parámetro de query:
curl "https://back.sitecog.com/content/v1/pages/home?lang=en&key=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"¿Cuándo está bien hacerlo?
- Comprobaciones rápidas: pegar una URL en la barra de direcciones para ver qué devuelve un endpoint.
- Herramientas que solo aceptan una URL: una integración no-code, un importador de feeds, un plugin de generador de sitios estáticos sin opción de cabeceras.
En todos los demás casos, mejor la cabecera. Las URLs suelen acabar en los logs del servidor, en el historial del navegador y en la analítica, y aunque la clave no es un secreto, no hay motivo para ir sembrándola por todas partes. Además, tus URLs quedan cortas y tu código, limpio.
En nuestros propios logs de servidor el valor de ?key= aparece enmascarado. Pero el historial del navegador, los proxies intermedios y tus propios logs no saben nada de eso, así que la cabecera sigue siendo la mejor opción.
Por qué la clave es segura en el navegador
Respuesta corta: porque no puede hacer nada que tus visitantes no puedan hacer ya abriendo tu web. Una clave de sitio es pública por diseño. Esto es lo que puede y lo que no puede hacer:
| Una clave de sitio… | |
|---|---|
| lee las páginas, secciones, bloques y entradas del blog publicados de su sitio | sí |
| cambia, crea o borra algo | no: la API es de solo lectura |
| ve borradores o entradas del blog sin publicar | no: solo contenido publicado |
| lee contenido de tus otros sitios | no: una clave, un sitio |
Es la misma idea que la publishable key de un proveedor de pagos: identifica de quién son los datos que hay que mostrar, no da poder sobre ellos. Todo lo que puede leer va a estar de todas formas en tu web pública.
Lo único que sí puede hacer una clave copiada es gastar tu cuota de peticiones: cada clave tiene su propio límite de 600 peticiones por minuto (mira Límites). Si alguien se pone a hacerlo, rota la clave: es cosa de un par de minutos.
Guardar la clave en variables de entorno
Si la clave es pública, ¿para qué complicarse con variables de entorno? Porque las claves cambian: rotar una clave debería ser un cambio de configuración, no de código. Además, la mayoría de los frameworks necesitan un prefijo para dejar pasar una variable al código del navegador:
# Next.js — Server Components, Route Handlers (nunca llega al navegador)
CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Next.js — Client Components (se incrusta en el bundle al compilar)
NEXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Vite (React, Vue, Svelte…) — se incrusta al compilar
VITE_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Nuxt — sobrescribe runtimeConfig.public.crmKey
NUXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40// app/page.tsx — un Server Component
export default async function Home() {
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
next: { revalidate: 60 },
});
const page = await res.json();
return <h1>{page.content.hero.content.hero_title.content.en}</h1>;
}'use client';
import { useEffect, useState } from 'react';
export function HeroTitle() {
const [title, setTitle] = useState('');
useEffect(() => {
fetch('https://back.sitecog.com/content/v1/blocks/hero_title?section=hero&lang=en', {
headers: { 'x-crm-key': process.env.NEXT_PUBLIC_CRM_KEY! },
})
.then((r) => r.json())
.then((block) => setTitle(block.content.en ?? ''));
}, []);
return <h1>{title}</h1>;
}// src/content.ts
export async function getPage(marker: string, lang = 'en') {
const res = await fetch(`https://back.sitecog.com/content/v1/pages/${marker}?lang=${lang}`, {
headers: { 'x-crm-key': import.meta.env.VITE_CRM_KEY },
});
if (!res.ok) throw new Error(`Content API: ${res.status}`);
return res.json();
}export default defineNuxtConfig({
runtimeConfig: {
public: {
crmKey: '', // se rellena desde NUXT_PUBLIC_CRM_KEY
},
},
});<script setup>
const { public: { crmKey } } = useRuntimeConfig();
const { data: page } = await useFetch('https://back.sitecog.com/content/v1/pages/home', {
query: { lang: 'en' },
headers: { 'x-crm-key': crmKey },
});
</script>
<template>
<h1>{{ page.content.hero.content.hero_title.content.en }}</h1>
</template>Revocar y rotar claves
Cualquier clave se puede revocar en el CRM, en la misma lista de Ajustes → Claves de Content API. Una clave revocada deja de funcionar: todas las peticiones con ella reciben 401 invalid_key. Para cambiar de clave sin una sola petición fallida, solápalas:
Crea una clave nueva
Deja la antigua en paz de momento: las dos siguen funcionando a la vez.Despliega con la clave nueva
Actualiza la variable de entorno en todos los sitios donde se usaba la antigua, recompila si tu framework la incrusta y despliega.Comprueba que el sitio sigue cargando contenido
Abre un par de páginas, echa un vistazo a los logs. ¿Ningún 401? Perfecto.Revoca la clave antigua
Ahora ya puedes desenchufarla sin miedo.
Errores de clave
| Estado | Cuerpo | Qué ha pasado |
|---|---|---|
| 401 | {"message":"invalid_key"} | La clave falta, está mal formada (no es pk_ + 32 hex) o está revocada. |
| 429 | {"message":"rate_limit_exceeded"} | Demasiadas peticiones, o demasiadas claves incorrectas desde tu IP en este minuto (mira abajo). |
El bloqueo por claves incorrectas
Para que adivinar claves no sirva de nada, contamos las peticiones con claves no válidas por IP. Más de 20 peticiones con clave incorrecta en un minuto desde una IP, y esa IP recibe 429 el resto del minuto, incluso con una clave válida. No hay cabeceras Retry-After; la ventana se reinicia al empezar el minuto siguiente.
La forma clásica de activarlo sin querer: revocas una clave antigua, pero un servidor o un cron olvidado la sigue usando. El renderizado en servidor no para de disparar 401 y, un minuto después, todo el servidor está bloqueado, clave nueva incluida. Así que:
const res = await fetch(url, { headers: { 'x-crm-key': process.env.CRM_KEY } });
if (res.status === 401) {
// Una clave incorrecta o revocada no se arregla sola. Reintentar solo gasta
// el cupo de claves incorrectas y hace que bloqueen esta IP.
throw new Error('Content API: invalid site key, check CRM_KEY');
}
if (res.status === 429) {
// Espera hasta el minuto siguiente (unos 60 s, más un poco de jitter).
}Todos los demás códigos están en la página Errores.