OROVA.VN — BIZ AI AGENT

/ Guida per sviluppatori

Pubblicazione via API su qualsiasi sito web.

Come creare il tuo endpoint perché Orova possa pubblicare gli articoli finiti su un sito che non usa WordPress.

Orova pubblica gli articoli finiti su WordPress senza configurazioni aggiuntive. Se il tuo sito non è WordPress — un CMS personalizzato, un'architettura headless, un sito statico o un tuo backend — puoi comunque lasciare che Orova pubblichi automaticamente accettando gli articoli tramite una piccola API. Scrivi un solo endpoint HTTP; Orova lo chiama ogni volta che completa un articolo. Questa guida è rivolta a uno sviluppatore e descrive l'intero contratto della richiesta.

/ Panoramica

Come funziona

Quando Orova finisce di scrivere un articolo per una parola chiave, invia una singola richiesta HTTP POST all'URL dell'endpoint che hai registrato. Il corpo della richiesta è JSON e contiene l'intero articolo. Il tuo endpoint crea il post dalla tua parte e risponde con l'URL finale e pubblico dell'articolo. Orova salva quell'URL come link pubblicato, proprio come farebbe per un post WordPress. Usalo quando vuoi una pubblicazione automatica su un sito che non è WordPress, o quando preferisci mantenere il pieno controllo su come vengono archiviati i post.

/ Configurazione

Configurazione — sei passaggi

  • 1

    Attiva “Collega tramite API” nel tuo progetto. Apri il tuo progetto (Progetti → il tuo progetto → Connessioni). Accanto alla scheda WordPress c'è una scheda Collega via API. Inserisci lì l'URL del tuo endpoint. Il campo della chiave segreta è facoltativo: lascialo vuoto e Orova ne genera una, oppure incolla una chiave che il tuo sito usa già. Una volta salvata, la chiave appare sulla scheda con un pulsante Copia. Un progetto pubblica su una sola destinazione: collegarsi via API blocca il pulsante WordPress, e viceversa.

  • 2

    Crea un endpoint che accetta POST. Sul tuo sito o server, crea una route che ascolti richieste HTTP POST con corpo JSON. È l'URL da incollare nella schermata Connessioni del progetto. Orova lo chiama una volta per ogni articolo completato.

  • 3

    Verifica il token Bearer a ogni richiesta. Ogni richiesta di Orova include un header Authorization nel formato “Bearer <secret>”, con la chiave segreta esatta mostrata nel tuo progetto. Confrontala con la copia salvata e rifiuta tutto ciò che non corrisponde: è questo che impedisce ad altri di pubblicare sul tuo sito.

  • 4

    Crea l'articolo dal payload. Leggi il corpo JSON e crea un post nel tuo CMS o database: usa title, slug e content_html per l'articolo vero e proprio, excerpt come meta description, featured_image_url come immagine di copertina, e keyword / lang / published_at come metadati.

  • 5

    Restituisci l'URL pubblico dell'articolo. Rispondi con HTTP 200 o 201 e un corpo JSON { "url": "https://yoursite.com/the-new-article" }. Orova salva quell'URL come link pubblicato dell'articolo. Se restituisci uno stato diverso da 2xx, o ometti il campo url, Orova considera fallita la pubblicazione.

  • 6

    Gestisci le richieste di aggiornamento dal motore Optimize. Quando il corpo contiene action = "update", trova il post esistente tramite target_url (o slug) e sovrascrivi title, content_html ed excerpt sul posto, mantenendo l'URL invariato. Rispondi 200 con { "url": ... } che punta allo stesso post. Gli endpoint che non lo gestiscono ancora faranno solo segnalare a Orova l'ottimizzazione come fallita; la pubblicazione di nuovi articoli continua a funzionare come prima.

/ Contratto

La richiesta inviata da Orova

Ogni articolo completato arriva come una singola richiesta di questo tipo:

POST <your endpoint URL>

Headers:
  Content-Type: application/json
  Authorization: Bearer <secret>     # the secret key shown in Project -> Connections
  User-Agent: Orova-SEO

Body (JSON):
  {
    "title": "...",                  # article headline
    "slug": "...",                   # URL-friendly identifier
    "content_html": "...",           # full article HTML
    "excerpt": "...",                # meta description
    "featured_image_url": "..." | null,
    "keyword": "...",                # target keyword
    "lang": "en",                    # ISO language code
    "published_at": "2026-05-17T09:00:00.482913",  # ISO 8601, UTC, no offset suffix
    "status": "draft"                # optional: "draft" keeps it unpublished
  }
  # Optional fields are left out of the body when empty — never sent as null.

/ Riferimento

Campi del payload

Campi di una richiesta di pubblicazione (nuovo articolo). Le richieste di eliminazione e aggiornamento usano lo stesso endpoint con i corpi più piccoli mostrati nelle rispettive sezioni qui sotto.

CampoTipoDescrizione
titlestringIl titolo dell'articolo.
slugstringURL-friendly identifier suggested for the post.
content_htmlstringIl corpo completo dell'articolo in HTML pronto per la pubblicazione.
excerptstringUn breve riassunto, pensato per la meta description.
featured_image_urlstring | nullURL dell'immagine di copertina, o null se non c'è.
keywordstringLa parola chiave SEO per cui è stato scritto l'articolo.
langstringLanguage as an ISO code, e.g. "en" or "vi".
published_atstringIl momento in cui Orova ha inviato l'articolo, in ISO 8601 UTC, senza suffisso di fuso orario.
statusstring | undefinedFacoltativo. "draft" mantiene l'articolo non pubblicato; "publish", o nessun valore, lo pubblica.
actionstring | undefinedAssente per i nuovi articoli. "delete" ti chiede di rimuovere un post; "update" (inviato dal motore Optimize) ti chiede di sovrascrivere sul posto un post esistente.
target_urlstringSolo con action = "update": l'URL pubblico del post da sovrascrivere. Mantieni l'URL invariato.
idnumber | undefinedOnly sent when Orova knows your post id (from `list` or `get`): match on it before `target_url`.

I campi facoltativi vengono omessi del tutto dal corpo quando non c'è nulla da inviare — Orova non invia mai null, quindi leggili in modo difensivo. Anche status è facoltativo: "draft" crea l'articolo come bozza, mentre "publish" o nessuno status lo pubblica subito.

/ Contratto

La risposta da restituire

Dopo aver creato il post, rispondi con uno stato HTTP 200 o 201 e questo corpo JSON:

HTTP 200 (or 201)
Content-Type: application/json

{
  "url": "https://yoursite.com/published-article"
}

Orova salva quell'url come link pubblicato dell'articolo. Se il tuo endpoint restituisce uno stato diverso da 2xx, o un corpo che non è un oggetto JSON con un campo url, Orova considera fallita la pubblicazione e contrassegna l'articolo di conseguenza, così puoi riprovare. Rispondi entro 60 secondi: dopo, Orova interrompe la richiesta per timeout (vale per pubblicazione, aggiornamento ed eliminazione).

/ Esempio

Esempio di codice

Un endpoint di ricezione minimo in Node.js con Express. Gli stessi passaggi — verificare il token, gestire action (update / delete), creare o modificare il post, restituire l'URL — valgono in qualsiasi linguaggio o framework.

// Node.js / Express — a minimal receiving endpoint
import express from "express";

const app = express();
app.use(express.json({ limit: "5mb" }));

// The secret Orova generated for this project.
const OROVA_SECRET = process.env.OROVA_SECRET;

app.post("/orova/publish", async (req, res) => {
  // 1. Verify the Bearer token.
  const auth = req.get("authorization") || "";
  if (auth !== "Bearer " + OROVA_SECRET) {
    return res.status(401).json({ error: "unauthorized" });
  }

  // 2. Required: list your articles (duplicate check, internal links). action = "list".
  if (req.body.action === "list") {
    const posts = await listPosts(req.body.limit || 500); // newest first, 500 max
    // each post: { id, title, slug, link, date, modified, status, author, excerpt? }
    return res.status(200).json({ ok: true, posts });
  }

  // 3. Required: read one article in full (used when optimizing it). action = "get".
  if (req.body.action === "get") {
    const post = await findPost(req.body.id, req.body.target_url, req.body.slug);
    if (!post) return res.status(404).json({ ok: false, err: "not_found" });
    return res.status(200).json({ ok: true, post });
  }

  // 4. Visual Editor: store an image, answer with its absolute URL.
  if (req.body.action === "upload_image") {
    const { filename, mime, data_base64 } = req.body;
    if (!String(mime || "").startsWith("image/")) {
      return res.status(422).json({ error: "image files only" });
    }
    const bytes = Buffer.from(data_base64, "base64");
    if (bytes.length > 15 * 1024 * 1024) {
      return res.status(413).json({ error: "image too large" });
    }
    return res.status(200).json({ ok: true, url: await saveImage(filename, bytes) });
  }

  // 5. Delete requests: body carries action = "delete".
  if (req.body.action === "delete") {
    const removed = await deletePostByUrl(req.body.url, req.body.slug);
    if (!removed) return res.status(404).json({ deleted: true }); // already gone
    return res.status(200).json({ deleted: true });
  }

  // 6. Update requests: action = "update". Optional fields may be absent.
  if (req.body.action === "update") {
    const updated = await updatePost(req.body.id, req.body.target_url, {
      title: req.body.title,
      html: req.body.content_html,
      metaDescription: req.body.excerpt,
      coverImage: req.body.featured_image_url,  // optional
      status: req.body.status,                  // optional: "publish" | "draft"
    });
    if (!updated) return res.status(404).json({ error: "post not found" });
    return res.status(200).json({ url: updated.url });
  }

  // 7. New article: read the payload Orova sent.
  const {
    title, slug, content_html, excerpt,
    featured_image_url, keyword, lang, published_at, status,
  } = req.body;

  // 8. Create the article in your own CMS or database.
  const post = await createPost({
    title,
    slug,
    html: content_html,
    metaDescription: excerpt,
    coverImage: featured_image_url,
    keyword,
    lang,
    publishedAt: published_at,
    draft: status === "draft",   // status is optional; absent means publish now
  });

  // 9. Return 200/201 with the live URL.
  return res.status(201).json({
    url: "https://yoursite.com/" + post.slug,
  });
});

app.listen(3000);

/ Contratto — aggiornamento

Aggiornamento articoli (Optimize)

Il motore Optimize di Orova riscrive gli articoli già pubblicati — dati aggiornati, titoli migliori, contenuti uniti — e poi sovrascrive il post esistente sul posto, mantenendo l'URL invariato. La richiesta va allo stesso URL dell'endpoint, come POST con lo stesso token Bearer:

POST <your endpoint URL>

Headers:
  Content-Type: application/json
  Authorization: Bearer <secret>
  User-Agent: Orova-SEO

Body (JSON):
  {
    "action": "update",          # always the literal string "update"
    "id": 123,                   # optional: post id, match on this first
    "target_url": "...",         # public URL of the post to overwrite (match next)
    "title": "...",              # new headline
    "slug": "...",               # slug fallback for matching, may be empty
    "content_html": "...",       # full new article body as HTML
    "excerpt": "...",            # new meta description
    "featured_image_url": "...", # optional: new cover image
    "status": "publish"          # optional: "publish" or "draft"
  }
  # Optional fields are left out of the body when empty — never sent as null.

Trova il post tramite target_url (in alternativa slug), sostituisci titolo, corpo e meta description, mantieni l'URL com'è, poi rispondi 200 con { "url": "<same post URL>" }. Qualsiasi stato diverso da 2xx, o un corpo senza url, fa contrassegnare a Orova quell'ottimizzazione come fallita (non crea mai un post duplicato). Gli endpoint che non l'hanno ancora implementato continuano a pubblicare nuovi articoli senza cambiamenti: implementare l'aggiornamento serve solo quando usi la schermata Optimize.

Dal 29 agosto il corpo dell'aggiornamento può contenere anche id (da confrontare prima di target_url), featured_image_url per una nuova immagine di copertina e status con "publish" o "draft". Tutti e tre sono facoltativi; se vuoti vengono omessi dal corpo, mai inviati come null.

/ Contratto — Visual Editor

Lettura articoli (obbligatoria)

Orova deve leggere gli articoli già presenti sul tuo sito — per evitare argomenti duplicati, aggiungere link interni e ottimizzare i post più vecchi. Aggiungi altre due azioni sullo stesso URL dell'endpoint, con lo stesso token Bearer: list restituisce i tuoi articoli, get ne restituisce uno completo.

POST <your endpoint URL>     # same endpoint, same Bearer secret

Body (JSON):
  {
    "action": "list",            # always the literal string "list"
    "limit": 500                 # newest article first, 500 maximum
  }

Response — HTTP 200:
  {
    "ok": true,
    "posts": [
      {
        "id": 123,               # your own post id, reused by "get" and "update"
        "title": "...",
        "slug": "...",
        "link": "https://yoursite.com/the-article",
        "date": "2026-08-29",    # published date, YYYY-MM-DD
        "modified": "2026-08-29",
        "status": "publish",     # "publish" or "draft"
        "author": "...",
        "excerpt": "..."         # optional: short summary, 300 characters max
      }
    ]
  }
  # Required: Orova reads this list to avoid duplicate topics,
  # add internal links and analyze the whole site.
POST <your endpoint URL>

Body (JSON):
  {
    "action": "get",             # always the literal string "get"
    "id": 123,                   # match on this first
    "target_url": "...",         # then the public URL
    "slug": "..."                # then the slug
  }

Response — HTTP 200:
  {
    "ok": true,
    "post": {
      "id": 123,
      "title": "...",
      "slug": "...",
      "link": "https://yoursite.com/the-article",
      "content_html": "...",     # full article body as HTML
      "excerpt": "...",
      "featured_image_url": "...",
      "status": "publish"        # "publish" or "draft"
    }
  }

No such post — HTTP 404:
  { "ok": false, "err": "not_found" }

limit ha un massimo di 500 e l'articolo più recente viene per primo. Per get, Orova invia insieme id, target_url e slug: usa quello che riconosci, in quest'ordine. Se non c'è corrispondenza, rispondi 404 con { "ok": false, "err": "not_found" }. excerpt in list è facoltativo (massimo 300 caratteri). Obbligatorio: Orova chiama list per sapere quali articoli esistono già (per evitare duplicati, aggiungere link interni e analizzare l'intero sito) e chiama get per leggere un articolo quando lo ottimizza. Se il tuo sito non risponde a queste due azioni, i nuovi articoli non ricevono link interni e quelli vecchi non possono essere ottimizzati.

/ Contratto — Visual Editor

Caricamento immagini (Visual Editor)

Quando trascini un'immagine nel Visual Editor, Orova la invia allo stesso URL dell'endpoint in base64 dentro il corpo JSON: niente multipart, nessun secondo URL da proteggere.

POST <your endpoint URL>

Body (JSON):
  {
    "action": "upload_image",    # always the literal string "upload_image"
    "filename": "cover.png",     # original file name, extension included
    "mime": "image/png",         # image/* only
    "data_base64": "iVBORw0KGgo..."   # file bytes, base64, 15 MB maximum
  }

Response — HTTP 200 (or 201):
  {
    "ok": true,
    "url": "https://yoursite.com/uploads/cover.png"   # absolute URL
  }

Accetta solo image/* e rifiuta tutto ciò che supera 15 MB. Salva il file, poi rispondi con l'URL assoluto dell'immagine: un percorso relativo rompe l'articolo appena viene visualizzato altrove. Questa azione serve solo per il Visual Editor.

/ Contratto — eliminazione

Eliminazione articoli

Quando un utente rimuove un articolo pubblicato in Orova (dalla schermata Scrivi o Report), Orova chiede al tuo sito di eliminarlo a sua volta. La richiesta va allo stesso URL dell'endpoint, come POST con lo stesso token Bearer: cambia solo il corpo:

POST <your endpoint URL>

Headers:
  Content-Type: application/json
  Authorization: Bearer <secret>
  User-Agent: Orova-SEO

Body (JSON):
  {
    "action": "delete",          # always the literal string "delete"
    "url": "...",                # public URL of the article to remove (match on this first)
    "slug": "...",               # slug fallback, may be empty
    "keyword": "..."             # the article's target keyword, for your logs
  }

Cerca il post tramite url (in alternativa slug), rimuovilo o annullane la pubblicazione, poi rispondi con 200 e { "deleted": true }: va bene anche un semplice 204. Se il post non esiste più, rispondi 404 e Orova lo considera già eliminato. Restituire { "deleted": false } indica a Orova che il post non è stato rimosso. Le normali richieste di pubblicazione non contengono mai un campo action, quindi gli endpoint esistenti continuano a funzionare: implementare l'eliminazione è facoltativo ma consigliato.

/ Sicurezza

Note sulla sicurezza

  • Verifica sempre il token Bearer. Il tuo endpoint è un URL pubblico: chiunque potrebbe chiamarlo. L'header Authorization è l'unica prova che una richiesta arrivi davvero da Orova, quindi rifiuta ogni richiesta il cui token non corrisponde esattamente al segreto salvato.
  • Servi l'endpoint su HTTPS. Il segreto viaggia in un header. HTTPS impedisce che venga letto durante il transito.
  • Tieni il segreto fuori dal codice. Conservalo in una variabile d'ambiente o in un gestore di segreti, mai scritto direttamente in un file che fai commit. Se viene esposto, generane uno nuovo da Progetto → Connessioni.
  • Tratta content_html come contenuto, non come markup affidabile. Salvalo e visualizzalo con la stessa attenzione che riservi al corpo di qualsiasi articolo nel tuo CMS.

/ Conclusione

Questo è l'intero contratto

Con l'endpoint attivo e registrato nel progetto, Orova pubblica automaticamente sul tuo sito, esattamente come fa per gli utenti WordPress. Ogni articolo completato arriva sul tuo server, diventa un post e il suo URL pubblico torna in Report e Analisi, così puoi monitorarne il rendimento.

← Torna alla raccolta di guide

/ Serve aiuto?

Bloccato nel collegare il tuo endpoint? Apri la sezione Supporto nel tuo workspace, oppure inviaci un messaggio.