API pubblica degli articoli
Con l'API crei, aggiorni ed elimini gli articoli da codice. L'API non legge articoli: il contenuto resta in Akendor, dove viene analizzato e coordinato, e quando è pronto Akendor ti invia il JSON con un webhook firmato. Tu lo salvi nel database che preferisci.
Come funziona
- Crei una chiave nella dashboard (menu API) e imposti l'indirizzo del webhook.
- Chiami
POST /articles: Akendor risponde subito (202) con un job e scrive l'articolo in bozza. - Quando la bozza è pronta, il tuo server riceve l'evento
article.generatedcon il JSON. - Dopo la tua revisione in Akendor e la pubblicazione, ricevi
article.published.
Riepilogo degli endpoint
| Richiesta | A cosa serve | Risposta |
|---|---|---|
| POST /articles | Mette in coda la scrittura di un articolo (in bozza) | 202 |
| GET /jobs/{id} | Stato di un job di scrittura | 200 |
| PATCH /articles/{id} | Modifica i campi indicati di un articolo | 200 |
| DELETE /articles/{id} | Elimina un articolo da Akendor | 204 |
Tutti gli indirizzi partono da /api/v1. Le risposte di errore hanno sempre la stessa forma (vedi «Errori»).
Autenticazione
Ogni chiave è legata a un sito e può lavorare solo sugli articoli di quel sito. Invia la chiave nell'header Authorization: Bearer ak_live_… (oppure in X-API-Key). La chiave si vede una sola volta, alla creazione: conservala come una password e usala solo dal server, mai nel browser.
Authorization: Bearer ak_live_...Indirizzo base
https://www.akendor.com/api/v1Creare un articolo
POST /articlesMette in coda la scrittura di un articolo e risponde subito. L'articolo nasce sempre in bozza: lo rileggi in Akendor prima di pubblicarlo.
Corpo della richiesta (JSON)
keyword— obbligatorio. L'argomento principale, da 2 a 200 caratterilanguage— facoltativo.it,en,es,fr,de,pt,nl,pl,sv,zhoja(predefinito:it)category— facoltativo. Una categoria del tuo sitoinstructions— facoltativo. Indicazioni editoriali per l'IA, fino a 2000 caratterisecondaryKeywords— facoltativo. Fino a 10 keyword secondarieexternalId— facoltativo. Il tuo identificativo: lo ritrovi in ogni evento
curl -X POST "https://www.akendor.com/api/v1/articles" \
-H "Authorization: Bearer ak_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"keyword": "generalized anxiety",
"language": "en",
"category": "Mental health",
"instructions": "Practical tone, aimed at adults",
"secondaryKeywords": ["anxiety symptoms", "breathing exercises"],
"externalId": "order-1042"
}'Aggiungi l'header Idempotency-Key per non creare articoli doppi se la richiesta viene ripetuta: la stessa chiave restituisce lo stesso job.
Risposta (202)
{
"id": "Qm4xT8wLz1NcVb7YrE2D",
"type": "create",
"status": "queued",
"articleId": null,
"externalId": "order-1042",
"error": null,
"createdAt": "2026-06-19T10:14:00.000Z",
"updatedAt": "2026-06-19T10:14:00.000Z"
}Stato di un job
GET /jobs/{id}Restituisce solo lo stato (queued, processing, completed, failed), mai il contenuto: il JSON arriva dal webhook. Serve come verifica se un webhook non arriva.
curl "https://www.akendor.com/api/v1/jobs/Qm4xT8wLz1NcVb7YrE2D" \
-H "Authorization: Bearer ak_live_..."Modificare un articolo
PATCH /articles/{id}Cambia i campi indicati in fields e restituisce l'articolo aggiornato dentro data.article, con il suo status (draft o published). L<code>id</code> è quello dellevento article.generated.
Campi modificabili: title, subtitle, summary, slug, category, seoTitle, metaDescription, keyword, image, keyPoints, takeaways, tableOfContents, paragraphs, faq, externalReferences.
curl -X PATCH "https://www.akendor.com/api/v1/articles/7Hq2xYb0n5LwEJc9TfRa" \
-H "Authorization: Bearer ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"fields": {
"title": "How to manage generalized anxiety: a practical guide",
"keyPoints": ["Anxiety is a natural mechanism", "Breathing helps reduce tension"]
}
}'Risposta (200)
{
"data": {
"status": "draft",
"article": {
"id": "7Hq2xYb0n5LwEJc9TfRa",
"slug": "managing-generalized-anxiety",
"title": "How to manage generalized anxiety: a practical guide",
"keyPoints": ["Anxiety is a natural mechanism", "Breathing helps reduce tension"],
"lastUpdate": "2026-06-19T10:31:12.000Z",
"...": "all the other fields of the article"
}
}
}Modificare un articolo
Eliminare un articolo
DELETE /articles/{id}Elimina l'articolo da Akendor e risponde 204 senza corpo. Non tocca le copie già scritte su WordPress o Firebase.
curl -X DELETE "https://www.akendor.com/api/v1/articles/7Hq2xYb0n5LwEJc9TfRa" \
-H "Authorization: Bearer ak_live_..."Webhook
Imposti l'indirizzo (https) nella dashboard, nella sezione API. Akendor invia una richiesta POST con il JSON per ogni evento.
Eventi
article.generated— la bozza è pronta (status: "draft")article.published— hai pubblicato l'articolo in Akendorarticle.updated— l'articolo è stato modificato con l'APIarticle.deleted— l'articolo è stato eliminatoarticle.failed— la scrittura non è riuscita, con il motivowebhook.test— evento di prova inviato dalla dashboard
Ogni richiesta ha questa forma. Il campo data cambia in base all'evento.
{
"id": "9fK2mQ7vL0aXc3PzRt1B",
"type": "article.generated",
"createdAt": "2026-06-19T10:20:31.000Z",
"data": {
"jobId": "Qm4xT8wLz1NcVb7YrE2D",
"externalId": "order-1042",
"status": "draft",
"article": {
"id": "7Hq2xYb0n5LwEJc9TfRa",
"slug": "managing-generalized-anxiety",
"title": "How to manage generalized anxiety",
"subtitle": "Practical strategies and clinical insights",
"summary": "Generalized anxiety calls for a multifactorial approach...",
"category": "Mental health",
"language": "en",
"articleDate": "2026-06-19T10:14:00.000Z",
"lastUpdate": "2026-06-19T10:20:30.000Z",
"image": "https://cdn.example.com/img/anxiety-cover.webp",
"readingTime": 7,
"seoTitle": "How to manage generalized anxiety in 2026",
"metaDescription": "A complete guide to managing generalized anxiety...",
"keyword": "generalized anxiety",
"wordCount": 1450,
"seoScore": 82,
"tableOfContents": ["What anxiety is", "Physical symptoms"],
"keyPoints": ["Anxiety is a natural mechanism", "Breathing helps reduce tension"],
"takeaways": ["Recognize the physical signals before they grow"],
"paragraphs": [
{
"id": "5074accf-3949-4945-8ae0-525e20c99de6",
"heading": "What anxiety is",
"content": "<p>Generalized anxiety disorder...</p>",
"image": null,
"practicalInsight": { "type": "attenzione", "boxTitle": "Watch for physical signals", "points": ["Sudden racing heart"] }
}
],
"faq": [{ "question": "How long does an anxiety attack last?", "answer": "Usually between 10 and 30 minutes..." }],
"externalReferences": [{ "title": "pubmed.ncbi.nlm.nih.gov", "url": "https://pubmed.ncbi.nlm.nih.gov/..." }]
}
}
}// article.published / article.updated
{ "id": "...", "type": "article.updated", "createdAt": "...",
"data": { "externalId": "order-1042", "status": "draft" | "published", "article": { ... } } }
// article.deleted
{ "id": "...", "type": "article.deleted", "createdAt": "...",
"data": { "id": "7Hq2xYb0n5LwEJc9TfRa", "externalId": "order-1042" } }
// article.failed
{ "id": "...", "type": "article.failed", "createdAt": "...",
"data": { "jobId": "Qm4xT8wLz1NcVb7YrE2D", "externalId": "order-1042", "error": "..." } }Da sapere sul JSON
articleDate, lastUpdate) sono testo ISO 8601. seoScore è il punteggio SEO da 0 a 100, calcolato da Akendor: vale null se l'articolo non è ancora stato analizzato. Il vettore usato per trovare articoli simili non compare mai nei webhook. I valori di status (draft, published) e practicalInsight.type (consiglio, mito, esempio, attenzione) sono codici fissi. Usa i campi di testo e l'array paragraphs per costruire la tua pagina.Intestazioni
Akendor-Event-Id— identificativo dell'evento: usalo per non elaborare due volte lo stesso eventoAkendor-Event-Type— il tipo di eventoAkendor-Signature— la firma, vedi sotto
Verificare la firma
La firma è un HMAC-SHA256 di «t.corpo» (t è la data in secondi, corpo è il testo grezzo della richiesta) calcolato con il segreto del webhook. Calcolala anche tu e confrontala: se non coincide, o se t è vecchia di oltre 5 minuti, scarta la richiesta.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (a string, not parsed JSON)
function verify(rawBody, signatureHeader, secret) {
const [t, v1] = signatureHeader.split(",").map((part) => part.split("=")[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutes
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}Rispondi con un codice 2xx entro 10 secondi: fai il lavoro pesante dopo aver risposto.
Ritentativi
In caso di errore Akendor ritenta dopo 1 minuto, 5 minuti, 30 minuti, 2, 6 e 12 ore (7 tentativi in circa 21 ore). Se tutti falliscono la consegna risulta fallita e dalla dashboard puoi reinviarla. Un evento può arrivare più di una volta o fuori ordine: usa l'id dell'evento e il campo lastUpdate dell'articolo.
Con quale tecnologia lo ricevo?
Dipende solo da come costruisci il tuo lato, non da Akendor. Il webhook è una semplice richiesta HTTP, quindi puoi riceverlo con qualunque linguaggio o framework: ad esempio Next.js, Laravel, Django, Spring Boot o un backend per un'app mobile (React Native, SwiftUI, Kotlin, Flutter).
Cloud e server
Esempi nei linguaggi più diffusi
Scegli il tuo linguaggio. Per ognuno trovi come chiedere un articolo e come ricevere il webhook verificando la firma. Il segreto (whsec_…) e la chiave (ak_live_…) si leggono dalle variabili d'ambiente, mai dal codice.
Node.js, Express
Creare un articolo
const res = await fetch("https://www.akendor.com/api/v1/articles", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AKENDOR_API_KEY}`, // ak_live_...
"Content-Type": "application/json",
"Idempotency-Key": "order-1042",
},
body: JSON.stringify({ keyword: "generalized anxiety", language: "en", externalId: "order-1042" }),
});
const job = await res.json(); // 202 { id, status: "queued", ... }: the article arrives later via webhookRicevere il webhook e verificare la firma
import express from "express";
import crypto from "node:crypto";
const SECRET = process.env.AKENDOR_WEBHOOK_SECRET; // whsec_...
// rawBody: the body exactly as received (a string, not parsed JSON)
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
if (!parts.v1 || parts.v1.length !== expected.length) return false;
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // older than 5 minutes
return crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
const app = express();
app.post("/akendor/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const rawBody = req.body.toString("utf8");
if (!verify(rawBody, req.get("Akendor-Signature") ?? "", SECRET)) return res.sendStatus(401);
res.sendStatus(200); // reply first, then do the work
const event = JSON.parse(rawBody);
if (await alreadyProcessed(event.id)) return; // events can arrive more than once
if (event.type === "article.deleted") await deleteArticle(event.data.id);
else if (event.data.article) await saveArticle(event.data.article); // generated, published, updated
});
app.listen(3000);Come usare gli esempi
saveArticle, deleteArticle e alreadyProcessed sono segnaposto: sostituiscile con il codice del tuo database (vedi sotto, «Salvare il JSON nel tuo database»). La firma si calcola sul testo grezzo della richiesta, così come arriva: non rileggere e riscrivere il JSON prima di verificarla. La verifica della firma negli esempi JavaScript, Python e Java è stata provata con firme valide, manomesse e scadute; gli altri linguaggi seguono lo stesso schema con le librerie standard.Salvare il JSON nel tuo database
Usa article.id come chiave: è stabile e identifica lo stesso articolo in tutti gli eventi. Così lo stesso codice gestisce creazione, modifica e pubblicazione con un «inserisci o aggiorna». Per ignorare gli eventi arrivati in ritardo confronta article.lastUpdate.
SQL (esempio con PostgreSQL)
CREATE TABLE articles (
id text PRIMARY KEY, -- article.id (stable across all events)
external_id text, -- your externalId, if you sent one
slug text NOT NULL,
language text,
title text NOT NULL,
status text NOT NULL, -- draft | published
updated_at timestamptz NOT NULL, -- article.lastUpdate
article jsonb NOT NULL -- the full JSON
);
INSERT INTO articles (id, external_id, slug, language, title, status, updated_at, article)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO UPDATE SET
external_id = EXCLUDED.external_id, slug = EXCLUDED.slug, language = EXCLUDED.language,
title = EXCLUDED.title, status = EXCLUDED.status, updated_at = EXCLUDED.updated_at, article = EXCLUDED.article
WHERE EXCLUDED.updated_at >= articles.updated_at; -- ignore events that arrive lateNoSQL (esempio con MongoDB)
// event = the webhook JSON already verified (see "Verify the signature")
const { article } = event.data;
await db.collection("articles").replaceOne(
{ _id: article.id },
{ _id: article.id, externalId: event.data.externalId, status: event.data.status, ...article },
{ upsert: true }
);Errori
Gli errori hanno sempre questa forma, con un codice stabile e un messaggio in inglese.
{ "error": { "code": "invalid_request", "message": "keyword is required (2 to 200 characters)." } }400— richiesta non valida (il messaggio dice quale campo)401— chiave mancante, non valida o revocata403— limite mensile del piano raggiunto (plan_limit_reached): le quote si rinnovano il primo del mese404— articolo o job non trovato429— troppe richieste: aspetta i secondi indicati nell'headerRetry-After500— errore del server: riprova più tardi
Limiti
60 richieste al minuto per chiave e 20 creazioni di articoli all'ora. In più valgono le quote mensili del tuo piano: ogni articolo creato con l'API conta come uno degli articoli del mese.
Buone pratiche
article.id come chiave, come spiegato sopra.