Publicação via API
Publique artigos no seu site ou CMS através de pedidos API assinados.
O Papigrows envia solicitações POST assinadas ao receptor HTTPS que você hospeda. Seu receptor salva e publica o artigo e responde com sua URL pública.
Configurar publicação via APILigue o seu endpoint de publicação
Crie um recetor que guarde e publique artigos no seu site. Adicione o endpoint e segredo partilhado nas definições do CMS. Testar e guardar envia connection.test com dados vazios; responda com HTTP 200 e ok: true. Apenas proprietários e administradores podem alterar esta ligação.
Use um endpoint HTTPS público sem parâmetros, fragmentos ou credenciais incorporadas.
Um segredo aleatório está pronto. Pode substituí-lo por um segredo próprio de 32 a 256 caracteres. Copie-o para o recetor e guarde-o no servidor.
{
"version": 1,
"event": "connection.test",
"id": "<event-id>",
"data": {}
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true }Permita que o recetor aceite o segredo antigo e o novo durante o teste e a gravação. O Papigrows usa o segredo guardado até a nova ligação passar o teste e ser guardada. Remova o antigo quando terminarem as entregas em curso.
Cabeçalhos da solicitação
Estes cabeçalhos acompanham o teste de conexão e as entregas de artigos. Os nomes não diferenciam maiúsculas de minúsculas.
| Nome do cabeçalho | Valor de exemplo | Significado |
|---|---|---|
Content-Type | application/json | Corpo da solicitação em JSON. |
Accept | application/json | Espera-se uma confirmação em JSON. |
X-Papigrows-Event-Id | <event-id> | Corresponde ao id do envelope. |
X-Papigrows-Timestamp | <unix-seconds> | Horário Unix em segundos para esta tentativa. |
X-Papigrows-Signature | v1=<hex-hmac-sha256> | v1= seguido do HMAC-SHA256 hexadecimal de timestamp + '.' + o corpo bruto exato. |
Idempotency-Key | <event-id> | O mesmo id estável do envelope nas novas tentativas. |
Verifique cada pedido
Antes de interpretar o JSON, verifique a assinatura v1 HMAC-SHA256 sobre o carimbo de data/hora, um ponto e o corpo original exato. Compare em tempo constante, rejeite carimbos com mais de cinco minutos ou futuros e mantenha o segredo no servidor. HTTPS e assinatura não substituem proteção contra duplicados.
Exemplo de pedido
POST /your-publishing-endpoint
Content-Type: application/json
X-Papigrows-Event-Id: <event-id>
X-Papigrows-Timestamp: <unix-seconds>
X-Papigrows-Signature: v1=<hex-hmac-sha256>
Idempotency-Key: <event-id>
{
"version": 1,
"event": "article.upsert",
"id": "<event-id>",
"data": {
"id": "<permanent-article-id>",
"version": 7,
"title": "Example",
"html": "<p>...</p>",
"slug": "example",
"excerpt": "...",
"coverImageUrl": "https://example.com/cover.jpg",
"locale": "en",
"hreflangAlternates": [
{ "locale": "sv", "url": "https://example.com/sv/blog/example" }
]
}
}Campos do corpo da solicitação
Estes campos descrevem article.upsert. Para connection.test, data fica vazio. A versão do protocolo do envelope é separada da versão do conteúdo do artigo.
| Nome do campo | Tipo de valor | Presença | Significado |
|---|---|---|---|
version | integer | Sempre | Versão do protocolo: 1 para o formato original ou 2 após uma atualização explícita do recetor. |
event | string | Sempre | Tipo de evento: article.upsert ou connection.test. |
id | string | Sempre | ID estável de entrega compartilhado com ambos os cabeçalhos de identidade. Armazene-o para evitar gravações duplicadas. |
data | object | Sempre | Conteúdo do evento. Vazio para connection.test. |
data.id | string | Sempre | ID permanente desta variante de idioma. Use-o como chave para criar ou atualizar. |
data.version | integer | Sempre | Revisão armazenada do conteúdo como inteiro não negativo. Rejeite revisões anteriores. |
data.title | string | Sempre | Título do artigo. |
data.html | string | Sempre | HTML a publicar, incluindo links hreflang disponíveis. |
data.slug | string | Quando disponível | Slug de URL sugerido. Mantenha uma URL pública estável nas edições futuras. |
data.excerpt | string | Quando disponível | Trecho do artigo, quando disponível. |
data.coverImageUrl | string | Quando disponível | URL da imagem de capa, quando disponível. |
data.locale | string | Quando disponível | Código de idioma desta variante do artigo. |
data.hreflangAlternates | array | Quando disponível | Outras variantes de idioma publicadas, quando disponíveis. |
data.hreflangAlternates[].locale | string | Para cada variante de idioma | Código de idioma da variante vinculada. |
data.hreflangAlternates[].url | string | Para cada variante de idioma | URL pública dessa variante publicada. |
Versão 2 e compatibilidade
A versão 2 inclui texto simples, identificadores do espaço de trabalho e do público, resumos, secções e etiquetas. O recetor deve confirmar expressamente a versão 2 durante o teste de ligação. Os recetores existentes mantêm a versão 1 até escolher, testar com êxito e guardar outra versão.
{
"version": 2,
"event": "connection.test",
"id": "<event-id>",
"data": {}
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true, "version": 2 }POST /your-publishing-endpoint
Content-Type: application/json
X-Papigrows-Event-Id: <event-id>
X-Papigrows-Timestamp: <unix-seconds>
X-Papigrows-Signature: v1=<hex-hmac-sha256>
Idempotency-Key: <event-id>
{
"version": 2,
"event": "article.upsert",
"id": "<event-id>",
"data": {
"id": "<permanent-article-id>",
"groupId": "<permanent-article-group-id>",
"workspaceId": "<owning-workspace-id>",
"primaryAudienceId": "<saved-audience-id>",
"businessAreaId": "<saved-business-area-id>",
"text": "...",
"summary": "...",
"section": "Advice",
"tags": ["Families"],
"version": 7,
"title": "Example",
"html": "<p>...</p>",
"slug": "example",
"excerpt": "...",
"coverImageUrl": "https://example.com/cover.jpg",
"locale": "en",
"hreflangAlternates": [
{ "locale": "sv", "url": "https://example.com/sv/blog/example" }
]
}
}A versão 2 mantém os campos originais e acrescenta estes campos obrigatórios. Os identificadores do público mantêm-se null nos artigos antigos sem segmentação guardada.
| Nome do campo | Tipo de valor | Significado |
|---|---|---|
data.groupId | UUID | Grupo estável partilhado pelas variantes linguísticas. |
data.workspaceId | UUID | Identificador guardado do espaço de trabalho proprietário. |
data.primaryAudienceId | UUID | null | Identificador guardado do público principal, ou null num artigo antigo sem segmentação. |
data.businessAreaId | UUID | null | Identificador guardado da área de negócio, ou null se faltar. |
data.text | string | Texto simples derivado do HTML limpo, não do Markdown original. As ligações não são carregadas. |
data.summary | string | null | Resumo guardado do artigo, ou null se faltar. |
data.section | string | null | Secção registada para esta publicação, ou null. |
data.tags | string[] | Etiquetas registadas para esta publicação; uma lista vazia significa que não há etiquetas. |
Feed público de conteúdo
Ative a ligação API do Papigrows para disponibilizar artigos publicados. Peça explicitamente a versão 2; sem versão, mantém-se a resposta original. Os espaços privados e os artigos não publicados continuam inacessíveis.
/api/content/feed/<workspace-slug>?version=2&locale=en&limit=20Cada artigo inclui os identificadores guardados, a versão do conteúdo, o texto simples e o resumo. A secção e as etiquetas vêm apenas do destino registado na publicação. As etiquetas são null quando essa prova falta. Mantenha version=2 e locale em cada pedido com cursor.
No feed público, text é null quando a conversão não está disponível. textUnavailable é body_too_large acima de 1 MiB ou conversion_failed se a conversão falhar; caso contrário é null. O HTML e a paginação mantêm-se.
Confirme a publicação
Quando o artigo estiver público, devolva HTTP 200 ou 201 com o mesmo ID, estado published e URL HTTPS público. Confirmações de fila, HTTP 202 ou 204, redirecionamentos, rascunhos e respostas inválidas não contam como publicação. Confirme apenas depois de guardar e publicar.
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "<permanent-article-id>",
"status": "published",
"url": "https://example.com/blog/example"
}Entrega e novas tentativas
article.upsert cria ou atualiza uma variante linguística com data.id como chave permanente. Guarde data.version e o ID do evento atomicamente com a alteração. Duplicados recebem a confirmação guardada; versões antigas nunca podem substituir conteúdo mais recente. Até três tentativas de cinco segundos cada. Falhas de rede, HTTP 429 e HTTP 5xx são repetidas após 250 ms e 1 segundo. Corpo e ID mantêm-se; carimbo e assinatura são renovados.
Os corpos das entregas são limitados a 1 MiB e as respostas JSON a 16 KiB. Limite o corpo bruto da solicitação antes de interpretá-lo.
O que a integração abrange
Apenas artigos, incluindo alterações republicadas no Papigrows. Eventos de tópicos e eliminação, filas em segundo plano e leitura de alterações remotas não estão incluídos. A edição por resposta de e-mail não pode atualizar um webhook em segurança; edite e publique no Papigrows. Continuam a aplicar-se plano, qualidade e automação. Após falha incerta, verifique o recetor antes de tentar manualmente; não existe ciclo infinito.
Exemplo de verificação da assinatura
Este auxiliar Node.js verifica apenas a autenticidade. O recetor deve também limitar o tamanho, validar o esquema, gerir duplicados e versões em armazenamento persistente, publicar e devolver a confirmação abaixo.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(headers, rawBody, secret) {
const timestamp = headers.get("x-papigrows-timestamp") ?? "";
const signature = headers.get("x-papigrows-signature") ?? "";
if (!/^\d{10}$/.test(timestamp)) return false;
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (age < 0 || age > 300) return false;
if (!/^v1=[a-f0-9]{64}$/.test(signature)) return false;
const expected = createHmac("sha256", secret)
.update(timestamp + ".").update(rawBody).digest();
const received = Buffer.from(signature.slice(3), "hex");
if (received.length !== expected.length
|| !timingSafeEqual(received, expected)) return false;
try {
const event = JSON.parse(rawBody.toString());
return typeof event.id === "string"
&& /^[a-f0-9]{64}$/.test(event.id)
&& headers.get("x-papigrows-event-id") === event.id
&& headers.get("idempotency-key") === event.id;
} catch { return false; }
}