Docs · API v1
API pública do JSPress
Consuma o conteúdo dos seus sites de qualquer stack — Nuxt, Next, React, Vue, PHP, Python. Endpoints REST, respostas JSON, autenticação via Bearer token.
Introdução
JSPress é um CMS multi-tenant headless. Você modela conteúdo no painel (posts, produtos, imóveis, cursos — o que quiser) e consome via API REST no front que preferir. Sem plugin, sem tema, sem PHP.
Cada site tem sua própria API key com escopo isolado. Você pode ter dezenas de sites num único painel sem que um vaze conteúdo do outro.
Base URL
Todas as requisições começam com o prefixo abaixo:
https://api.jspress.app/v1Autenticação
Todas as chamadas exigem o header Authorization com sua API key:
Authorization: Bearer sk_live_...Gere sua key em Painel → Site → API keys. Chaves podem ser revogadas ou rotacionadas sem afetar as outras.
/v1/siteDados do site
Retorna metadados, configurações do site e SEO ready-to-use (favicon, OG image, theme color, redes sociais, JSON-LD de Organization + WebSite prontos pra colar num <script>). Os campos snippet_head e snippet_body_end já vêm com os scripts oficiais de GTM, GA4 e Meta Pixel concatenados (baseado em gtm_id, ga4_id, pixel_id configurados no painel) — o consumidor só injeta esses 2 campos direto, sem precisar gerar tags.
Requisição
curl https://api.jspress.app/v1/site \
-H "Authorization: Bearer sk_live_..."Resposta
{
"data": {
"name": "Meu Site",
"domain": "meusite.com",
"seo_title": "Meu Site — CMS headless",
"seo_description": "...",
"favicon_url": "https://.../favicon.png",
"og_image_url": "https://.../og.png",
"theme_color": "#e93d3d",
"gtm_id": "GTM-XXXXXXX",
"ga4_id": "G-XXXXXXXXXX",
"pixel_id": "123456789012345",
"snippet_head": "<!-- GTM + GA4 + Pixel gerados automaticamente + snippet custom -->",
"snippet_body_end": "<!-- noscript GTM + Pixel + snippet custom -->",
"contact_email": "...",
"contact_whatsapp": "...",
"social_instagram": "https://instagram.com/...",
"organization_json_ld": {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Meu Site",
"url": "https://meusite.com",
"logo": "https://.../favicon.png",
"sameAs": ["https://instagram.com/..."]
},
"website_json_ld": {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Meu Site",
"url": "https://meusite.com",
"inLanguage": "pt-BR",
"potentialAction": { "@type": "SearchAction", "target": "..." }
}
}
}/v1/post-typesTipos de conteúdo
Lista todos os Custom Post Types cadastrados — schema dos campos, slug, labels e taxonomias relacionadas.
Requisição
curl https://api.jspress.app/v1/post-types \
-H "Authorization: Bearer sk_live_..."Resposta
{
"data": [
{
"slug": "blog",
"label": "Blog",
"fields": ["title", "content", "featured_image"],
"taxonomies": ["categoria", "tag"]
}
]
}/v1/postsLista de posts
Retorna posts com paginação, filtros e ordenação. Aceita filtro por tipo, taxonomia, status, idioma e busca full-text.
Parâmetros
| Nome | Tipo | Descrição |
|---|---|---|
| type | string | Slug do post type (ex: blog). |
| taxonomy | string | Slug da taxonomia pra filtrar (usar com "term"). |
| term | string | Slug do termo dentro da taxonomia. |
| search | string | Busca full-text no título e no conteúdo. |
| language | string | Filtra posts pelo idioma (ex: pt-BR, en, es). Sem valor = retorna todos. |
| featured_on_home | boolean | Só posts marcados com "Destacar na home" no editor. Aceita true/false/1/0. |
| order | string | Direção da ordenação: asc ou desc (default: desc — mais recentes primeiro). |
| order_by | string | Coluna de ordenação: published_at (default) ou updated_at. Use updated_at pra páginas onde a última edição deve trazer o post pro topo. |
| page | integer | Página (default: 1). |
| per_page | integer | Itens por página (default: 20, max: 100). |
Requisição
curl "https://api.jspress.app/v1/posts?type=blog&featured_on_home=true&order_by=updated_at&per_page=10" \
-H "Authorization: Bearer sk_live_..."Resposta
{
"data": [
{
"id": "abc123",
"type": "blog",
"title": "Post de exemplo",
"slug": "post-de-exemplo",
"content": "...",
"language": "pt-BR",
"featured_on_home": true,
"no_index": false,
"published_at": "2026-07-10T14:30:00Z",
"updated_at": "2026-07-13T09:15:00Z"
}
],
"meta": {
"page": 1,
"per_page": 10,
"total": 42,
"total_pages": 5
}
}/v1/posts/{slug}Detalhe do post
Retorna o post completo (com content em Tiptap JSON), terms agrupados por taxonomia, e o campo json_ld — schema.org BlogPosting completo (headline, author, publisher, dateModified, mainEntityOfPage, articleSection, keywords) pronto pra colar num <script type="application/ld+json">.
Parâmetros
| Nome | Tipo | Descrição |
|---|---|---|
| language | string | Filtra pelo idioma quando o mesmo slug existe em várias versões (ex: pt-BR, en, es). Sites monolíngues podem omitir. |
Requisição
curl "https://api.jspress.app/v1/posts/meu-post?language=pt-BR" \
-H "Authorization: Bearer sk_live_..."Resposta
{
"data": {
"id": "abc123",
"title": "Meu post",
"slug": "meu-post",
"excerpt": "...",
"content": { /* Tiptap JSON doc */ },
"cover_url": "https://...",
"seo_title": null,
"seo_description": null,
"seo_og_image": null,
"focus_keyword": "cms headless",
"language": "pt-BR",
"featured_on_home": false,
"no_index": false,
"published_at": "2026-07-10T14:30:00Z",
"updated_at": "2026-07-13T09:15:00Z",
"terms": {
"category": [{ "id": "...", "name": "Guias", "slug": "guias" }],
"tag": [{ "id": "...", "name": "SEO", "slug": "seo" }]
},
"json_ld": {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Meu post",
"image": "https://...",
"datePublished": "2026-07-10T14:30:00Z",
"dateModified": "2026-07-13T09:15:00Z",
"author": { "@type": "Organization", "name": "Meu Site" },
"publisher": {
"@type": "Organization",
"name": "Meu Site",
"logo": { "@type": "ImageObject", "url": "https://.../favicon.png" }
},
"mainEntityOfPage": { "@type": "WebPage", "@id": "https://meusite.com/blog/meu-post" },
"articleSection": "Guias",
"keywords": "SEO",
"inLanguage": "pt-BR"
}
}
}/v1/taxonomiesLista de taxonomias
Retorna todas as taxonomias do site — categorias, tags ou qualquer classificação customizada.
Requisição
curl https://api.jspress.app/v1/taxonomies \
-H "Authorization: Bearer sk_live_..."Resposta
{
"data": [
{
"slug": "categoria",
"label": "Categoria",
"hierarchical": true
}
]
}/v1/termsTermos de uma taxonomia
Retorna os termos dentro de uma taxonomia. Passe o slug da taxonomia como filtro.
Parâmetros
| Nome | Tipo | Descrição |
|---|---|---|
| taxonomy | string | Slug da taxonomia (obrigatório). |
Requisição
curl "https://api.jspress.app/v1/terms?taxonomy=categoria" \
-H "Authorization: Bearer sk_live_..."Resposta
{
"data": [
{
"id": "t1",
"slug": "marketing",
"label": "Marketing",
"parent": null
}
]
}SEO & Discovery
O JSPress entrega prontos os artefatos que Google, Bing e LLMs (Claude, ChatGPT, Perplexity) esperam. Você faz proxy simples no seu domínio ou cola o JSON-LD num <script> — sem estudar schema.org, sem gerar sitemap manual, sem escrever RSS na unha.
Como usar no dia-a-dia:
- Cola o
organization_json_ldewebsite_json_lddo/sitenum<script type="application/ld+json">no layout global. - Em cada página de post, cola o
json_lddo/posts/{'{'}slug{'}'}— schema BlogPosting completo. - Faz proxy dos endpoints
/feed.xml,/robots.txte/llms.txtnas rotas correspondentes do seu site. - Usa o
/sitemap-urlscomo source dinâmico do seu gerador de sitemap (ex:@nuxtjs/sitemap).
/v1/feed.xmlRSS Feed do blog
RSS 2.0 dos posts publicados. Faça proxy no seu domínio e linke no <head> — leitores como Feedly e crawlers de LLM descobrem automaticamente.
Parâmetros
| Nome | Tipo | Descrição |
|---|---|---|
| language | string | Gera o feed só com posts do idioma escolhido (ex: pt-BR, en, es). Sites multi-idioma servem um feed por versão. |
Requisição
curl "https://api.jspress.app/v1/feed.xml?language=pt-BR" \
-H "Authorization: Bearer sk_live_..."Resposta
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Meu Site</title>
<link>https://meusite.com/blog</link>
<atom:link href="..." rel="self" type="application/rss+xml" />
<description>...</description>
<language>pt-BR</language>
<lastBuildDate>Mon, 13 Jul 2026 12:00:00 GMT</lastBuildDate>
<item>
<title>Meu post</title>
<link>https://meusite.com/blog/meu-post</link>
<guid isPermaLink="true">https://meusite.com/blog/meu-post</guid>
<pubDate>Fri, 10 Jul 2026 14:30:00 GMT</pubDate>
<description>...</description>
</item>
</channel>
</rss>/v1/robots.txtrobots.txt padrão
robots.txt base com Sitemap: já apontado pro seu domínio + Allow explícito pros bots dos LLMs (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot).
Requisição
curl https://api.jspress.app/v1/robots.txt \
-H "Authorization: Bearer sk_live_..."Resposta
User-agent: *
Allow: /
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: CCBot
Allow: /
Sitemap: https://meusite.com/sitemap.xml/v1/sitemap-urlsURLs pro sitemap
Lista JSON com todas as URLs dos posts publicados + lastmod. Itere e monte seu sitemap.xml — ou passe direto pro @nuxtjs/sitemap como source dinâmico.
Parâmetros
| Nome | Tipo | Descrição |
|---|---|---|
| language | string | Filtra as URLs pelo idioma (ex: pt-BR, en, es). Útil pra gerar um sitemap por versão do site. |
Requisição
curl "https://api.jspress.app/v1/sitemap-urls?language=en" \
-H "Authorization: Bearer sk_live_..."Resposta
{
"data": [
{
"loc": "https://meusite.com/blog/meu-post",
"lastmod": "2026-07-13T09:15:00Z",
"changefreq": "weekly",
"priority": 0.8
}
]
}/v1/llms.txtllms.txt (Anthropic proposal)
Markdown estruturado que descreve o site + posts pra LLMs (Claude, ChatGPT, Perplexity) entenderem em um scan. Aumenta a chance do seu conteúdo virar citação nas respostas geradas por IA.
Parâmetros
| Nome | Tipo | Descrição |
|---|---|---|
| language | string | Gera o llms.txt só com posts do idioma escolhido (ex: pt-BR, en, es). |
Requisição
curl "https://api.jspress.app/v1/llms.txt?language=pt-BR" \
-H "Authorization: Bearer sk_live_..."Resposta
# Meu Site
> Descrição do site em uma linha.
## Sobre
Meu Site — conteúdo publicado em pt-BR.
## Blog
- [Meu post](https://meusite.com/blog/meu-post): resumo do post
## Contato
- Email: ...
- WhatsApp: ...
## Links
- Site: https://meusite.com
- Feed RSS: https://meusite.com/feed.xml
- Sitemap: https://meusite.com/sitemap.xmlFavicon dinâmico via proxy
Google, Bing e browsers pedem /favicon.ico direto — antes de olhar o <link rel="icon"> no HTML. Se o site consumidor servir um favicon estático (típico dos templates Nuxt/Next/Astro), o favicon do CMS nunca aparece na SERP.
Padrão recomendado: criar uma server route /favicon.ico que faz proxy do favicon_url retornado pelo endpoint /site. O admin troca no CMS → o site atualiza sozinho, sem redeploy.
Exemplo (Nuxt / Nitro)
// server/routes/favicon.ico.get.ts (Nuxt / Nitro)
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const cmsUrl = (config.cmsApiUrl as string).replace(/\/$/, '')
const apiKey = config.cmsApiKey as string
try {
const site = await $fetch<{ data: { favicon_url: string | null } }>(
`${cmsUrl}/api/v1/site`,
{ headers: { Authorization: `Bearer ${apiKey}` } },
)
const url = site.data?.favicon_url
if (!url) return sendRedirect(event, '/favicon-default.ico', 302)
const res = await $fetch.raw(url, { responseType: 'arrayBuffer' })
setResponseHeader(event, 'Content-Type', res.headers.get('content-type') || 'image/x-icon')
setResponseHeader(event, 'Cache-Control', 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800')
return new Uint8Array(res._data as ArrayBuffer)
} catch {
return sendRedirect(event, '/favicon-default.ico', 302)
}
})Delete o public/favicon.ico do template. Enquanto ele existir, sobrescreve a server route. Deixe um /favicon-default.ico como fallback pra quando o CMS estiver offline ou o favicon_url estiver null.
Snippets de tracking (GTM, GA4, Pixel)
O admin do CMS preenche apenas os IDs (gtm_id, ga4_id, pixel_id) em Configurações. O endpoint /site devolve os campos snippet_head e snippet_body_end com o HTML/JS já pronto. Basta injetar no <head> e antes do </body> — o consumidor não precisa gerar snippet nenhum na mão.
Exemplo (Nuxt / Nitro)
// app/app.vue (ou layout global)
const { data: site } = await useAsyncData('site', () => $fetch('/api/site'))
useHead(() => {
const s = site.value?.data
const scripts: any[] = []
if (s?.snippet_head) {
scripts.push({ innerHTML: s.snippet_head, tagPosition: 'head' })
}
if (s?.snippet_body_end) {
scripts.push({ innerHTML: s.snippet_body_end, tagPosition: 'bodyClose' })
}
return { script: scripts }
})Se preferir hardcodar IDs no site (sem passar pelo CMS), os campos snippet_* virão vazios — o consumidor pode ignorar.
Paginação
Endpoints de lista aceitam ?page e ?per_page. A resposta traz um objeto meta com total de itens e páginas.
{
"meta": {
"page": 1,
"per_page": 20,
"total": 132,
"total_pages": 7
}
}Filtros
/v1/posts aceita filtros combinados por query string. Alguns exemplos:
# Posts do tipo blog
/v1/posts?type=blog
# Posts com termo "marketing" na taxonomia categoria
/v1/posts?taxonomy=categoria&term=marketing
# Busca full-text
/v1/posts?search=api+headlessNode types (content Tiptap)
O campo content do post é um documento Tiptap JSON. Cada node tem type, opcionalmente attrs e content (array de filhos). Você monta um switch por type no seu renderer.
| Type | Descrição |
|---|---|
| paragraph | Parágrafo padrão. é array de text nodes. |
| heading | Título. vai de 1 a 6. |
| text | Texto puro. pode ter bold, italic, underline, strike, code, link. |
| image | e . |
| bulletList / orderedList / listItem | Listas com ou sem numeração. |
| blockquote | Bloco de citação. |
| table / tableRow / tableHeader / tableCell | Tabelas. contém ou . |
| htmlBlock | — HTML bruto colado pelo admin (embed, iframe, script). Renderize com v-html ou dangerouslySetInnerHTML. |
| embed | Player de YouTube, Vimeo ou Spotify (detectado automaticamente ao colar URL no editor). attrs.provider= .attrs.videoId — o ID no provider. attrs.contentType(só Spotify) = . |
| horizontalRule | Linha divisória . |
| hardBreak | Quebra de linha manual . |
Como renderizar um embed em Vue/React:
function renderEmbed(node) {
const { provider, videoId, contentType } = node.attrs
if (provider === 'youtube') {
return `<iframe src="https://www.youtube.com/embed/${videoId}"
class="w-full aspect-video" frameborder="0"
allow="accelerometer; autoplay; encrypted-media; picture-in-picture"
allowfullscreen></iframe>`
}
if (provider === 'vimeo') {
return `<iframe src="https://player.vimeo.com/video/${videoId}"
class="w-full aspect-video" frameborder="0"
allow="autoplay; fullscreen"></iframe>`
}
if (provider === 'spotify') {
const h = contentType === 'track' ? 152
: (contentType === 'episode' || contentType === 'show') ? 232
: 352
return `<iframe src="https://open.spotify.com/embed/${contentType}/${videoId}"
width="100%" height="${h}" frameborder="0"
allow="encrypted-media"></iframe>`
}
return ''
}Erros
Erros são retornados como JSON no formato abaixo, com o status HTTP correspondente.
{
"error": {
"code": "invalid_api_key",
"message": "API key inválida ou revogada."
}
}| Status | Código | Significado |
|---|---|---|
| 401 | unauthorized | API key ausente ou inválida. |
| 403 | forbidden | API key sem permissão pro recurso. |
| 404 | not_found | Recurso não existe. |
| 429 | rate_limit | Excedeu o limite de requests. |
| 500 | server_error | Erro inesperado — reporte pra gente. |