/ 개발자 가이드
API를 통한 발행 모든 웹사이트에.
WordPress를 사용하지 않는 사이트에 Orova가 완성된 글을 발행할 수 있도록 자체 엔드포인트를 구축하는 방법.
Orova는 완성된 글을 기본적으로 WordPress에 바로 발행합니다. 웹사이트가 WordPress가 아닌 경우(맞춤형 CMS, 헤드리스 환경, 정적 사이트 또는 자체 백엔드)에도 간단한 API를 통해 글을 수신하여 Orova가 자동으로 발행하도록 할 수 있습니다. 개발자가 HTTP 엔드포인트 하나만 작성하면 Orova가 글 작성을 마칠 때마다 이를 호출합니다. 이 가이드는 개발자를 위한 것이며 전체 요청 규약을 설명합니다.
/ 개요
작동 방식
Orova는 키워드에 대한 글 작성을 마치면 등록된 엔드포인트 URL로 단일 HTTP POST 요청을 보냅니다. 요청 본문은 JSON이며 전체 글을 포함합니다. 엔드포인트에서 해당 글을 생성하고 글의 최종 공개 URL로 응답합니다. Orova는 WordPress 게시글과 마찬가지로 해당 URL을 발행된 링크로 저장합니다. WordPress가 아닌 사이트에서 자동 발행을 원하거나 글 저장 방식을 직접 완전히 제어하고자 할 때 이 방식을 사용하세요.
/ 설정
설정 방법 — 6단계
- 1
프로젝트에서 “API로 연결” 활성화. 프로젝트를 엽니다(프로젝트 → 내 프로젝트 → 연결). WordPress 카드 옆에 API로 연결 카드가 있습니다. 거기에 엔드포인트 URL을 입력하세요. 비밀 키 필드는 선택사항입니다. 비워 두면 Orova가 자동으로 생성하며, 사이트에서 이미 사용하는 키를 붙여넣을 수도 있습니다. 저장하면 카드에 키와 함께 복사 버튼이 표시됩니다. 프로젝트는 하나의 대상에만 글을 발행합니다. API로 연결하면 WordPress 버튼이 비활성화되며, 그 반대의 경우도 마찬가지입니다.
- 2
POST를 수신하는 엔드포인트 구축. 자체 사이트 또는 서버에서 JSON 본문이 포함된 HTTP POST를 수신 대기하는 라우트를 만드세요. 이 URL을 프로젝트의 '연결' 화면에 다시 붙여넣습니다. Orova는 글을 작성할 때마다 이 엔드포인트를 한 번씩 호출합니다.
- 3
모든 요청에서 Bearer 토큰 검증. Orova에서 보내는 모든 요청에는 프로젝트에 표시된 정확한 비밀 키를 사용하는 “Bearer <secret>” 형식의 Authorization 헤더가 포함되어 있습니다. 저장된 키와 비교하여 일치하지 않는 요청은 모두 거부하세요. 이렇게 해야 다른 사람이 사이트에 글을 게시하는 것을 방지할 수 있습니다.
- 4
페이로드로 글 생성. JSON 본문을 읽고 CMS 또는 데이터베이스에 글을 생성하세요. 글 본문에는 title, slug, content_html을 사용하고, 메타 설명에는 excerpt를, 대표 이미지에는 featured_image_url을, 메타데이터에는 keyword / lang / published_at을 사용합니다.
- 5
글의 실제 URL 반환. HTTP 200 또는 201과 함께 { "url": "https://yoursite.com/the-new-article" } 형태의 JSON 본문으로 응답하세요. Orova는 해당 URL을 글의 발행 링크로 저장합니다. 2xx가 아닌 상태 코드를 반환하거나 url 필드를 생략하면 Orova는 발행을 실패로 처리합니다.
- 6
최적화 엔진의 업데이트 요청 처리. 본문에 action = "update"가 포함된 경우 target_url(또는 slug)로 기존 글을 찾아 title, content_html, excerpt를 해당 위치에 그대로 덮어쓰고 URL은 변경하지 마세요. 동일한 글을 가리키는 { "url": ... }와 함께 200으로 응답합니다. 이를 아직 처리하지 않는 엔드포인트는 단순히 Orova에서 최적화 실패로 보고될 뿐이며, 새 글 발행은 이전과 같이 계속 작동합니다.
/ 규약
Orova가 보내는 요청
완성된 각 글은 다음과 같은 형태의 단일 요청으로 전송됩니다:
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./ 참고 자료
페이로드 필드
발행(새 글) 요청의 필드입니다. 삭제 및 업데이트 요청은 아래 해당 섹션에 표시된 더 작은 본문으로 동일한 엔드포인트를 재사용합니다.
| 필드 | 유형 | 설명 |
|---|---|---|
| title | string | 글 제목입니다. |
| slug | string | URL-friendly identifier suggested for the post. |
| content_html | string | 즉시 발행 가능한 HTML 형태의 전체 글 본문입니다. |
| excerpt | string | 메타 설명(meta description)으로 사용할 짧은 요약입니다. |
| featured_image_url | string | null | 대표 이미지 URL이며, 이미지가 없는 경우 null입니다. |
| keyword | string | 글 작성을 위해 타겟팅한 SEO 키워드입니다. |
| lang | string | Language as an ISO code, e.g. "en" or "vi". |
| published_at | string | Orova가 글을 전송한 시점(시간대 접미사가 없는 UTC 기준 ISO 8601 형식)입니다. |
| status | string | undefined | 선택사항. "draft"는 글을 미발행 상태로 유지하며, "publish" 또는 값을 입력하지 않으면 즉시 발행합니다. |
| action | string | undefined | 새 글의 경우 생략됩니다. "delete"는 글 삭제를 요청하며, "update"(최적화 엔진에서 전송)는 기존 글을 해당 위치에서 덮어쓰도록 요청합니다. |
| target_url | string | action = "update"인 경우에만 해당: 덮어쓸 글의 실제 URL입니다. URL은 변경하지 마세요. |
| id | number | undefined | Only sent when Orova knows your post id (from `list` or `get`): match on it before `target_url`. |
선택사항 필드는 전송할 내용이 없을 때 본문에서 완전히 제외됩니다. Orova는 null을 전송하지 않으므로 방어적으로 읽으세요. status 또한 선택사항입니다. "draft"는 글을 임시저장 상태로 생성하며, "publish" 또는 status가 없으면 즉시 발행합니다.
/ 규약
반환해야 하는 응답
글을 생성한 후 HTTP 200 또는 201 상태 코드와 함께 다음 JSON 본문으로 응답하세요:
HTTP 200 (or 201)
Content-Type: application/json
{
"url": "https://yoursite.com/published-article"
}Orova는 해당 url을 글의 발행 링크로 저장합니다. 엔드포인트가 2xx 이외의 상태 코드를 반환하거나 본문이 url 필드를 가진 JSON 객체가 아닌 경우, Orova는 발행을 실패로 처리하고 다시 시도할 수 있도록 해당 글을 표시합니다. 60 seconds 이내에 응답하세요. 그 이후에는 Orova에서 요청 시간 초과로 처리됩니다(이는 발행, 업데이트, 삭제 모두에 동일하게 적용됩니다).
/ 예제
코드 예제
Node.js와 Express를 사용한 최소한의 수신 엔드포인트입니다. 토큰 검증, action(update / delete)에 따른 분기, 글 생성 또는 수정, URL 반환 등 동일한 단계가 모든 언어나 프레임워크에 적용됩니다.
// 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);/ 규약 — 업데이트
글 업데이트 (최적화)
Orova의 최적화 엔진은 이미 게시된 글을 다시 작성(최신 정보 반영, 제목 개선, 콘텐츠 병합)한 다음, URL을 변경하지 않고 해당 위치에 그대로 기존 글을 덮어씁니다. 요청은 동일한 Bearer 토큰과 함께 POST 방식으로 동일한 엔드포인트 URL로 전송됩니다:
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.target_url(없는 경우 slug로 대체)로 글을 찾아 제목, 본문 및 메타 설명을 교체하고, URL은 그대로 유지한 후 { "url": "<same post URL>" }와 함께 200으로 응답하세요. 2xx가 아닌 상태 코드나 url이 없는 본문은 Orova에서 해당 최적화를 실패로 표시하게 만듭니다(중복 글을 생성하지는 않습니다). 이를 아직 구현하지 않은 엔드포인트도 새 글 발행은 이전과 같이 계속 작동하며, 업데이트 구현은 최적화 화면을 사용할 때만 필요합니다.
8월 29일부터 업데이트 본문에는 id(target_url보다 먼저 매칭), 새 대표 이미지를 위한 featured_image_url, "publish" 또는 "draft" 값을 갖는 status도 포함될 수 있습니다. 세 가지 모두 선택사항이며 비어 있는 경우 본문에서 제외되고 결코 null로 전송되지 않습니다.
/ 규약 — Visual Editor
글 조회 (필수)
Orova는 주제 중복을 방지하고 내부 링크를 추가하며 이전 글을 최적화하기 위해 사이트에 이미 있는 글을 읽어야 합니다. 동일한 Bearer 토큰을 사용하여 동일한 엔드포인트 URL에 두 가지 동작(action)을 추가하세요. list는 글 목록을 반환하고 get은 글 하나의 전체 내용을 반환합니다.
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는 최대 500개로 제한되며 최신 글이 먼저 표시됩니다. get의 경우 Orova는 id, target_url, slug를 함께 전송하므로 해당 순서대로 인식되는 값과 매칭하세요. 일치하는 항목이 없으면 { "ok": false, "err": "not_found" }와 함께 404로 응답합니다. list의 excerpt는 선택사항입니다(최대 300자). 필수: Orova는 어떤 글이 이미 존재하는지 확인하기 위해(중복 방지, 내부 링크 추가, 전체 사이트 분석) list를 호출하고, 글을 최적화할 때 해당 글을 읽기 위해 get을 호출합니다. 사이트에서 이 두 동작(action)에 응답하지 않으면 새 글에 내부 링크가 추가되지 않으며 이전 글을 최적화할 수 없습니다.
/ 규약 — Visual Editor
이미지 업로드 (Visual Editor)
Visual Editor에 이미지를 끌어다 놓으면 Orova는 별도의 multipart나 보안을 유지해야 할 두 번째 URL 없이 JSON 본문 내 base64 형식으로 동일한 엔드포인트 URL에 이미지를 전송합니다.
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
}image/*만 허용하고 15 MB를 초과하는 파일은 거부하세요. 파일을 저장한 다음 이미지의 절대 URL로 응답해야 합니다. 상대 경로는 다른 곳에서 렌더링될 때 글이 깨질 수 있습니다. 이 동작은 Visual Editor에만 필요합니다.
/ 규약 — 삭제
글 삭제
사용자가 Orova 내에서(작성 또는 보고서 화면에서) 발행된 글을 삭제하면 Orova는 사이트에도 해당 글을 삭제하도록 요청합니다. 요청은 동일한 Bearer 토큰과 함께 POST 방식으로 동일한 엔드포인트 URL로 전송되며, 본문만 다릅니다:
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
}url(없는 경우 slug로 대체)로 글을 조회하여 삭제하거나 발행을 취소한 후 200 및 { "deleted": true }로 응답하세요(본문 없는 204도 가능합니다). 글이 더 이상 존재하지 않으면 404로 응답하며 Orova는 이미 삭제된 것으로 처리합니다. { "deleted": false }를 반환하면 글이 삭제되지 않았음을 Orova에 알립니다. 일반 발행 요청에는 action 필드가 포함되지 않으므로 기존 엔드포인트는 변경 없이 계속 작동합니다. 삭제 구현은 선택사항이지만 권장됩니다.
/ 보안
보안 유의 사항
- 항상 Bearer 토큰 검증. 엔드포인트는 공개 URL이므로 누구나 호출할 수 있습니다. Authorization 헤더는 요청이 실제로 Orova에서 전송되었음을 증명하는 유일한 수단이므로, 저장된 비밀 키와 정확히 일치하지 않는 토큰이 포함된 모든 요청은 거부하세요.
- HTTPS를 통해 엔드포인트 제공. 비밀 키는 헤더로 전송됩니다. HTTPS는 전송 중에 키가 유출되지 않도록 보호합니다.
- 코드에 비밀 키를 포함하지 마세요. 커밋하는 파일에 절대 하드코딩하지 말고 환경 변수나 시크릿 관리자에 저장하세요. 노출된 경우 프로젝트 → 연결에서 새 키를 생성하세요.
- content_html을 신뢰할 수 있는 마크업이 아닌 콘텐츠로 처리하세요. CMS의 다른 게시물 본문과 마찬가지로 주의해서 저장하고 렌더링하세요.
/ 마무리
이것이 규약의 전부입니다
엔드포인트가 활성화되고 프로젝트에 등록되면 Orova는 WordPress 사용자와 마찬가지로 사이트에 자동으로 글을 게시합니다. 완결된 각 글은 서버로 전송되어 게시물이 되며, 공개 URL이 보고서 및 분석으로 다시 전달되어 성과를 추적할 수 있습니다.
/ 도움이 필요하신가요?
엔드포인트 연결에 어려움을 겪고 계신가요? 워크스페이스 내 지원 섹션을 열거나 메시지를 보내주세요.
