OROVA.VN — BIZ AI AGENT

/ Ghid pentru dezvoltatori

Publicare prin API pe orice site.

Cum să construiți propriul endpoint pentru ca Orova să poată publica articole finalizate pe un site care nu rulează WordPress.

Orova publică articolele finalizate în WordPress direct. Dacă site-ul dvs. nu este WordPress — un CMS propriu, o configurare headless, un site static sau backend-ul dvs. — puteți totuși lăsa Orova să publice automat, acceptând articolele printr-un mic API. Scrieți un singur endpoint HTTP; Orova îl apelează de fiecare dată când finalizează un articol. Acest ghid se adresează unui dezvoltator și descrie contractul complet al cererii.

/ Prezentare generală

Cum funcționează

Când Orova termină de scris un articol pentru un cuvânt cheie, trimite o singură cerere HTTP POST către URL-ul endpoint-ului pe care l-ați înregistrat. Corpul cererii este JSON și conține întregul articol. Endpoint-ul dvs. creează articolul la dvs. și răspunde cu URL-ul final, public, al articolului. Orova salvează acel URL ca link publicat, la fel ca pentru un articol WordPress. Folosiți această variantă când vreți publicare fără intervenție pe un site care nu este WordPress sau când preferați să păstrați controlul complet asupra modului în care sunt stocate articolele.

/ Configurare

Configurare — șase pași

  • 1

    Activați „Connect via API” în proiect. Deschideți proiectul (Projects → proiectul dvs. → Connections). Lângă cardul WordPress există un card Connect via API. Introduceți acolo URL-ul endpoint-ului. Câmpul cheii secrete este opțional: lăsați-l gol și Orova generează una sau lipiți o cheie pe care site-ul o folosește deja. După salvare, cheia apare pe card cu un buton Copy. Un proiect publică într-o singură destinație — conectarea prin API blochează butonul WordPress și invers.

  • 2

    Construiți un endpoint care acceptă POST. Pe site-ul sau serverul dvs., creați o rută care ascultă HTTP POST cu corp JSON. Acesta este URL-ul pe care îl lipiți înapoi în ecranul Connections al proiectului. Orova îl apelează o dată pentru fiecare articol finalizat.

  • 3

    Verificați token-ul Bearer la fiecare cerere. Fiecare cerere de la Orova poartă un antet Authorization de forma „Bearer <secret>”, cu exact cheia secretă afișată în proiect. Comparați-o cu copia stocată și respingeți tot ce nu se potrivește — asta împiedică pe oricine altcineva să publice pe site-ul dvs.

  • 4

    Creați articolul din payload. Citiți corpul JSON și creați un articol în CMS sau în baza de date: folosiți title, slug și content_html pentru articolul propriu-zis, excerpt ca meta description, featured_image_url ca imagine de copertă și keyword / lang / published_at ca metadate.

  • 5

    Returnați URL-ul live al articolului. Răspundeți cu HTTP 200 sau 201 și un corp JSON { "url": "https://yoursite.com/the-new-article" }. Orova stochează acel URL ca link publicat al articolului. Dacă returnați un status non-2xx sau omiteți câmpul url, Orova consideră publicarea eșuată.

  • 6

    Gestionați cererile de actualizare de la motorul Optimize. Când corpul conține action = "update", găsiți articolul existent după target_url (sau slug) și suprascrieți pe loc title, content_html și excerpt — păstrați URL-ul neschimbat. Răspundeți 200 cu { "url": ... } indicând același articol. Endpoint-urile care nu gestionează încă acest lucru vor face doar ca Orova să raporteze optimizarea drept eșuată; publicarea articolelor noi funcționează în continuare ca înainte.

/ Contract

Cererea trimisă de Orova

Fiecare articol finalizat sosește ca o singură cerere de această formă:

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.

/ Referință

Câmpurile payload

Câmpurile unei cereri de publicare (articol nou). Cererile de ștergere și actualizare folosesc același endpoint, cu corpurile mai mici prezentate în secțiunile proprii de mai jos.

CâmpTipDescriere
titlestringTitlul articolului.
slugstringURL-friendly identifier suggested for the post.
content_htmlstringCorpul complet al articolului, ca HTML gata de publicat.
excerptstringUn rezumat scurt, destinat meta description.
featured_image_urlstring | nullURL-ul imaginii de copertă sau null când nu există.
keywordstringCuvântul cheie SEO țintă pentru care a fost scris articolul.
langstringLanguage as an ISO code, e.g. "en" or "vi".
published_atstringMomentul în care Orova a trimis articolul, ISO 8601 în UTC, fără sufix de fus orar.
statusstring | undefinedOpțional. "draft" păstrează articolul nepublicat; "publish" sau nicio valoare îl publică.
actionstring | undefinedAbsent pentru articole noi. "delete" vă cere să eliminați un articol; "update" (trimis de motorul Optimize) vă cere să suprascrieți pe loc un articol existent.
target_urlstringDoar cu action = "update": URL-ul live al articolului de suprascris. Păstrați URL-ul neschimbat.
idnumber | undefinedOnly sent when Orova knows your post id (from `list` or `get`): match on it before `target_url`.

Câmpurile opționale sunt omise complet din corp când nu e nimic de trimis — Orova nu trimite niciodată null, deci citiți-le cu prudență. Și status este opțional: "draft" creează articolul ca ciornă, iar "publish" sau lipsa status îl publică imediat.

/ Contract

Răspunsul pe care trebuie să-l returnați

După ce ați creat articolul, răspundeți cu status HTTP 200 sau 201 și acest corp JSON:

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

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

Orova stochează acel url ca link publicat al articolului. Dacă endpoint-ul returnează orice status non-2xx sau un corp care nu este un obiect JSON cu câmpul url, Orova consideră publicarea eșuată și marchează articolul ca atare, ca să o puteți reîncerca. Răspundeți în 60 de secunde — după acest timp Orova închide cererea (se aplică la fel publicării, actualizării și ștergerii).

/ Exemplu

Exemplu de cod

Un endpoint minim de primire în Node.js cu Express. Aceiași pași — verificați token-ul, ramificați după action (update / delete), creați sau modificați articolul, returnați URL-ul — se aplică în orice limbaj sau 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);

/ Contract — update

Actualizarea articolelor (Optimize)

Motorul Optimize al Orova rescrie articole deja live — fapte actualizate, titluri mai bune, conținut îmbinat — apoi suprascrie articolul existent pe loc, păstrând URL-ul neschimbat. Cererea merge la același URL de endpoint, ca POST cu același 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.

Găsiți articolul după target_url (alternativ slug), înlocuiți titlul, corpul și meta description, păstrați URL-ul cum este, apoi răspundeți 200 cu { "url": "<same post URL>" }. Orice status non-2xx sau un corp fără url face ca Orova să marcheze optimizarea drept eșuată (nu creează niciodată un articol duplicat). Endpoint-urile care nu au implementat încă acest lucru continuă să publice articole noi neschimbat — implementarea update este necesară doar când folosiți ecranul Optimize.

Din 29 august, corpul de actualizare poate conține și id (potriviți după el înainte de target_url), featured_image_url pentru o nouă imagine de copertă și status cu valoarea "publish" sau "draft". Toate trei sunt opționale; când sunt goale, sunt eliminate din corp, nu sunt trimise niciodată ca null.

/ Contract — Visual Editor

Citirea articolelor (obligatoriu)

Orova trebuie să citească articolele deja existente pe site — pentru a evita subiectele duplicate, a adăuga linkuri interne și a optimiza articolele mai vechi. Adăugați încă două acțiuni pe același URL de endpoint, cu același token Bearer: list returnează articolele dvs., get returnează unul dintre ele complet.

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 este plafonat la 500, iar cel mai nou articol vine primul. Pentru get, Orova trimite împreună id, target_url și slug — potriviți după oricare îl recunoașteți, în această ordine. Când nu se potrivește nimic, răspundeți 404 cu { "ok": false, "err": "not_found" }. excerpt din list este opțional (maximum 300 de caractere). Obligatoriu: Orova apelează list pentru a afla ce articole există deja (pentru a evita duplicatele, a adăuga linkuri interne și a analiza întregul site) și apelează get pentru a citi un articol când îl optimizează. Dacă site-ul nu răspunde la aceste două acțiuni, articolele noi nu primesc linkuri interne, iar cele vechi nu pot fi optimizate.

/ Contract — Visual Editor

Încărcarea imaginilor (Visual Editor)

Când plasați o imagine în Visual Editor, Orova o trimite la același URL de endpoint ca base64 în corpul JSON — fără multipart, fără un al doilea URL de securizat.

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
  }

Acceptați doar image/* și respingeți orice peste 15 MB. Stocați fișierul, apoi răspundeți cu URL-ul absolut al imaginii: o cale relativă strică articolul imediat ce este afișat în altă parte. Această acțiune este necesară doar pentru Visual Editor.

/ Contract — delete

Ștergerea articolelor

Când un utilizator elimină un articol publicat în Orova (din ecranul Write sau Reports), Orova îi cere site-ului dvs. să îl șteargă și el. Cererea merge la același URL de endpoint, ca POST cu același token Bearer — singura diferență este corpul:

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
  }

Căutați articolul după url (alternativ slug), eliminați-l sau anulați-i publicarea, apoi răspundeți cu 200 și { "deleted": true } — funcționează și un simplu 204. Dacă articolul nu mai există, răspundeți 404, iar Orova îl consideră deja șters. Returnarea { "deleted": false } îi spune lui Orova că articolul nu a fost eliminat. Cererile obișnuite de publicare nu conțin niciodată un câmp action, deci endpoint-urile existente funcționează în continuare neschimbat — implementarea delete este opțională, dar recomandată.

/ Securitate

Note de securitate

  • Verificați mereu token-ul Bearer. Endpoint-ul dvs. este un URL public — oricine l-ar putea apela. Antetul Authorization este singurul lucru care dovedește că o cerere vine cu adevărat de la Orova, deci respingeți orice cerere al cărei token nu se potrivește exact cu secretul stocat.
  • Serviți endpoint-ul prin HTTPS. Secretul circulă într-un antet. HTTPS împiedică citirea lui în tranzit.
  • Păstrați secretul în afara codului. Stocați-l într-o variabilă de mediu sau într-un manager de secrete, niciodată scris direct într-un fișier pe care îl încărcați în repository. Dacă este expus vreodată, generați unul nou din Project → Connections.
  • Tratați content_html drept conținut, nu drept markup de încredere. Stocați-l și afișați-l cu aceeași grijă ca orice corp de articol din CMS-ul dvs.

/ Încheiere

Acesta este întregul contract

Cu endpoint-ul activ și înregistrat în proiect, Orova publică automat pe site-ul dvs. — exact ca pentru utilizatorii WordPress. Fiecare articol finalizat ajunge pe serverul dvs., devine o postare, iar URL-ul său public revine în Reports și Analysis, ca să puteți urmări cum performează.

← Înapoi la biblioteca de ghiduri

/ Aveți nevoie de ajutor?

Vă blocați la conectarea endpoint-ului? Deschideți secțiunea Asistență din spațiul de lucru sau trimiteți-ne un mesaj.