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/v1

Autenticação

Todas as chamadas exigem o header Authorization com sua API key:

header
Authorization: Bearer sk_live_...

Gere sua key em Painel → Site → API keys. Chaves podem ser revogadas ou rotacionadas sem afetar as outras.

GET/v1/site

Dados 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
curl https://api.jspress.app/v1/site \
  -H "Authorization: Bearer sk_live_..."

Resposta

json
{
  "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": "..." }
    }
  }
}
GET/v1/post-types

Tipos de conteúdo

Lista todos os Custom Post Types cadastrados — schema dos campos, slug, labels e taxonomias relacionadas.

Requisição

curl
curl https://api.jspress.app/v1/post-types \
  -H "Authorization: Bearer sk_live_..."

Resposta

json
{
  "data": [
    {
      "slug": "blog",
      "label": "Blog",
      "fields": ["title", "content", "featured_image"],
      "taxonomies": ["categoria", "tag"]
    }
  ]
}
GET/v1/posts

Lista de posts

Retorna posts com paginação, filtros e ordenação. Aceita filtro por tipo, taxonomia, status, idioma e busca full-text.

Parâmetros

NomeTipoDescrição
typestringSlug do post type (ex: blog).
taxonomystringSlug da taxonomia pra filtrar (usar com "term").
termstringSlug do termo dentro da taxonomia.
searchstringBusca full-text no título e no conteúdo.
languagestringFiltra posts pelo idioma (ex: pt-BR, en, es). Sem valor = retorna todos.
featured_on_homebooleanSó posts marcados com "Destacar na home" no editor. Aceita true/false/1/0.
orderstringDireção da ordenação: asc ou desc (default: desc — mais recentes primeiro).
order_bystringColuna 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.
pageintegerPágina (default: 1).
per_pageintegerItens por página (default: 20, max: 100).

Requisição

curl
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

json
{
  "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
  }
}
GET/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

NomeTipoDescrição
languagestringFiltra 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
curl "https://api.jspress.app/v1/posts/meu-post?language=pt-BR" \
  -H "Authorization: Bearer sk_live_..."

Resposta

json
{
  "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"
    }
  }
}
GET/v1/taxonomies

Lista de taxonomias

Retorna todas as taxonomias do site — categorias, tags ou qualquer classificação customizada.

Requisição

curl
curl https://api.jspress.app/v1/taxonomies \
  -H "Authorization: Bearer sk_live_..."

Resposta

json
{
  "data": [
    {
      "slug": "categoria",
      "label": "Categoria",
      "hierarchical": true
    }
  ]
}
GET/v1/terms

Termos de uma taxonomia

Retorna os termos dentro de uma taxonomia. Passe o slug da taxonomia como filtro.

Parâmetros

NomeTipoDescrição
taxonomystringSlug da taxonomia (obrigatório).

Requisição

curl
curl "https://api.jspress.app/v1/terms?taxonomy=categoria" \
  -H "Authorization: Bearer sk_live_..."

Resposta

json
{
  "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_ld e website_json_ld do /site num <script type="application/ld+json"> no layout global.
  • Em cada página de post, cola o json_ld do /posts/{'{'}slug{'}'} — schema BlogPosting completo.
  • Faz proxy dos endpoints /feed.xml, /robots.txt e /llms.txt nas rotas correspondentes do seu site.
  • Usa o /sitemap-urls como source dinâmico do seu gerador de sitemap (ex: @nuxtjs/sitemap).
GET/v1/feed.xml

RSS 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

NomeTipoDescrição
languagestringGera 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
curl "https://api.jspress.app/v1/feed.xml?language=pt-BR" \
  -H "Authorization: Bearer sk_live_..."

Resposta

xml
<?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>
GET/v1/robots.txt

robots.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
curl https://api.jspress.app/v1/robots.txt \
  -H "Authorization: Bearer sk_live_..."

Resposta

text
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
GET/v1/sitemap-urls

URLs 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

NomeTipoDescrição
languagestringFiltra as URLs pelo idioma (ex: pt-BR, en, es). Útil pra gerar um sitemap por versão do site.

Requisição

curl
curl "https://api.jspress.app/v1/sitemap-urls?language=en" \
  -H "Authorization: Bearer sk_live_..."

Resposta

json
{
  "data": [
    {
      "loc": "https://meusite.com/blog/meu-post",
      "lastmod": "2026-07-13T09:15:00Z",
      "changefreq": "weekly",
      "priority": 0.8
    }
  ]
}
GET/v1/llms.txt

llms.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

NomeTipoDescrição
languagestringGera o llms.txt só com posts do idioma escolhido (ex: pt-BR, en, es).

Requisição

curl
curl "https://api.jspress.app/v1/llms.txt?language=pt-BR" \
  -H "Authorization: Bearer sk_live_..."

Resposta

markdown
# 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.xml

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

typescript
// 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)

typescript
// 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.

json
{
  "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+headless

Node 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.

TypeDescrição
paragraphParágrafo padrão. é array de text nodes.
headingTítulo. vai de 1 a 6.
textTexto puro. pode ter bold, italic, underline, strike, code, link.
image e .
bulletList / orderedList / listItemListas com ou sem numeração.
blockquoteBloco de citação.
table / tableRow / tableHeader / tableCellTabelas. contém ou .
htmlBlock — HTML bruto colado pelo admin (embed, iframe, script). Renderize com v-html ou dangerouslySetInnerHTML.
embedPlayer de YouTube, Vimeo ou Spotify (detectado automaticamente ao colar URL no editor).
attrs.provider= .
attrs.videoId — o ID no provider.
attrs.contentType(só Spotify) = .
horizontalRuleLinha divisória .
hardBreakQuebra de linha manual .

Como renderizar um embed em Vue/React:

js
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.

json
{
  "error": {
    "code": "invalid_api_key",
    "message": "API key inválida ou revogada."
  }
}
StatusCódigoSignificado
401unauthorizedAPI key ausente ou inválida.
403forbiddenAPI key sem permissão pro recurso.
404not_foundRecurso não existe.
429rate_limitExcedeu o limite de requests.
500server_errorErro inesperado — reporte pra gente.