API REST

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

  1. Crei una chiave nella dashboard (menu API) e imposti l'indirizzo del webhook.
  2. Chiami POST /articles: Akendor risponde subito (202) con un job e scrive l'articolo in bozza.
  3. Quando la bozza è pronta, il tuo server riceve l'evento article.generated con il JSON.
  4. Dopo la tua revisione in Akendor e la pubblicazione, ricevi article.published.
RichiestaA cosa serveRisposta
POST /articlesMette in coda la scrittura di un articolo (in bozza)202
GET /jobs/{id}Stato di un job di scrittura200
PATCH /articles/{id}Modifica i campi indicati di un articolo200
DELETE /articles/{id}Elimina un articolo da Akendor204

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

Creare un articolo

POST /articles

Mette 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 caratteri
  • language — facoltativo. it, en, es, fr, de, pt, nl, pl, sv, zh o ja (predefinito: it)
  • category — facoltativo. Una categoria del tuo sito
  • instructions — facoltativo. Indicazioni editoriali per l'IA, fino a 2000 caratteri
  • secondaryKeywords — facoltativo. Fino a 10 keyword secondarie
  • externalId — 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

La modifica vale per l'articolo in Akendor. Se il sito usa WordPress o Firebase, la copia già pubblicata si aggiorna alla prossima pubblicazione da Akendor.

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 Akendor
  • article.updated — l'articolo è stato modificato con l'API
  • article.deleted — l'articolo è stato eliminato
  • article.failed — la scrittura non è riuscita, con il motivo
  • webhook.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

I nomi dei campi sono in inglese. Le date (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 evento
  • Akendor-Event-Type — il tipo di evento
  • Akendor-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

Il tuo server può girare ovunque, su una tua macchina o su qualsiasi servizio cloud. Akendor invia i webhook a un indirizzo https pubblico: non funzionano localhost né le reti interne. In sviluppo usa un tunnel verso la tua macchina.

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 webhook

Ricevere 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

Funzioni come 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 late

NoSQL (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 revocata
  • 403 — limite mensile del piano raggiunto (plan_limit_reached): le quote si rinnovano il primo del mese
  • 404 — articolo o job non trovato
  • 429 — troppe richieste: aspetta i secondi indicati nell'header Retry-After
  • 500 — 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

Conserva la chiave e il segreto come password, solo sul server. Rispondi subito al webhook e salva il JSON usando article.id come chiave, come spiegato sopra.