OXIRANKER DEVELOPER DOCS

OxiRanker 문서

웹사이트를 OxiRanker 서비스에 연결하기 위한 깔끔한 문서 센터입니다. WordPress 연동 또는 API 연동으로 시작한 뒤, 가이드를 step by step으로 따라가세요.

공식 문서Full-feature V1사이트 소유자, 개발자 및 연동 팀을 위한 문서
문서 메뉴
메뉴에서 가이드를 선택하세요. 이 페이지는 OxiRanker의 메인 문서 센터입니다.
API 서비스

API 서비스

커스텀 웹사이트, 커스텀 CMS 플랫폼, Next.js, Laravel, Node.js, PHP 또는 Python backend를 위해 Export API와 Webhook을 사용하세요.

Domain
확인됨
콘텐츠
준비됨
API
Webhook
중요한 저장 안내

OxiRanker는 destination website를 위한 permanent hosting이 아닙니다

OxiRanker는 destination website의 content, images 또는 exported files에 대해 permanent hosting 보장을 제공하지 않습니다. OxiRanker는 user-content hosting service가 아니며 generated content, cover images, Open Graph images 또는 output files는 OxiRanker infrastructure에 90일 넘게 남아 있지 않을 수 있습니다.

따라서 output을 받은 후 destination website는 final article, images, SEO data, Open Graph data 및 Schema data를 자체 database, server, storage 또는 CDN에 저장해야 하며 public display를 위해 OxiRanker URLs에 의존해서는 안 됩니다.

교육 영상

연결 교육 영상

WordPress를 연결하거나 API 연동을 구현하기 전에 이 영상을 시각적 walkthrough로 사용하세요.

API Output

output flow를 step by step으로 구현하세요

먼저 Export Token과 Webhook Secret을 backend에 저장한 다음 Webhook receiver, database, UPSERT logic, Image Mirroring 및 daily sync를 완료하세요.

STEP 01

OxiRanker에 가입하고 domain 만들기

먼저 OxiRanker에 가입하고 account에 sign in한 다음 Domains 섹션에서 Add Domain을 클릭하세요.

올바른 domain 형식

Domain name 필드에는 root domain만 입력하세요. 주소에는 http, https 또는 www가 포함되면 안 됩니다.

example.com

잘못된 형식

domain 필드에 이러한 형식을 입력하지 마세요.

https://example.com
http://example.com
www.example.com
domain 생성 후
domain 형식이 valid이면 OxiRanker가 domain을 만들고 domain ownership verification step으로 이동합니다.
STEP 02

domain 소유권 확인

이 step에서는 domain이 본인 소유이거나 관리 access가 있음을 증명해야 합니다.

DNS TXT

권장

domain DNS에 TXT record를 추가하세요. Cloudflare의 경우 일반적으로 가장 간단한 방법입니다.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

OxiRanker가 확인할 수 있도록 웹사이트의 .well-known path에 text file을 배치하세요.

File URL:
https://example.com/.well-known/yourdomain-verification.txt

File Content:
220daaa37****42730f27

Meta Tag

웹사이트 homepage의 head section 안에 meta tag를 배치하세요.

<meta name="yourdomain-verification" content="190208d****d275b65" />
중요한 DNS note
DNS record를 추가한 뒤, 특히 Cloudflare에서는 2~3분 기다려야 할 수 있습니다. 그런 다음 같은 OxiRanker page에서 Confirm을 클릭하세요.
Verification token lifetime
Domain verification values에는 time limit이 있습니다. 현재 verification values는 일반적으로 1440 minutes 동안 valid입니다.
STEP 03

첫 domain 확인 후 무료 token 받기

첫 domain을 verify한 후 OxiRanker는 initial testing을 위해 account에 1000 free tokens를 추가합니다.

첫 domain에만 적용
이 gift는 첫 번째 verified domain에만 추가되며 이후 domains에는 다시 추가되지 않습니다.
STEP 04

언어와 Blog URL 설정

Blog URLs 섹션에서 웹사이트가 single-language인지 multilingual인지, generated articles에 사용할 languages를 정의하세요.

언어 선택

OxiRanker는 24 languages를 지원합니다. 웹사이트 구조에 따라 하나 또는 여러 languages를 선택할 수 있습니다.

Blog URL 입력

각 language에 대해 해당 language의 blog URL을 입력하세요. 웹사이트가 multilingual이면 각 language path를 별도로 입력하세요.

https://example.com/fa/blog
https://example.com/en/blog
URL structure
웹사이트 구조가 다른 경우 실제 blog URL을 입력하세요. 중요한 점은 final URL이 articles가 publish될 path여야 한다는 것입니다.
STEP 05

각 language에 keywords 추가

Keywords tab에서 각 language에 최소 3개, 최대 50개의 keywords를 추가하세요.

주요 규칙

각 language의 keywords는 동일한 language로 입력해야 합니다. article language가 English이면 keywords도 English여야 합니다.

keywords 추가 방법

각 keyword를 입력한 뒤 Enter를 눌러 추가하세요. 모든 keywords를 완료한 뒤 Save를 클릭하세요.

일반적인 실수
English language에 Turkish keywords를 입력하면 English content에 Turkish phrases가 나타날 수 있습니다. Keyword language는 article language와 일치해야 합니다.
STEP 06

domain Article Profile 완료

Article Profile은 OxiRanker에게 brand가 무엇인지, 누구를 위해 작성하는지, 어떤 tone을 사용해야 하는지, 어떤 topics를 피해야 하는지 알려줍니다.

Industry

market 또는 business category를 정의합니다. 예: Dubai의 real estate consulting 및 property buying and selling.

Business description

business가 무엇을 판매하는지, 누구에게 서비스를 제공하는지, 어떻게 작동하는지, 무엇이 다른지 설명합니다.

Audience

content가 누구에게 말해야 하는지 정의합니다. 각 audience는 별도로 저장할 수 있습니다.

Tone

formal, educational, simple, technical 또는 sales-oriented 등 generated content의 style과 voice를 제어합니다.

Content goal

articles가 달성해야 할 목표를 정의합니다. 예: customers 유치, traffic 증가, users 교육.

금지된 주제

brand가 이야기해서는 안 되거나 연관되어서는 안 되는 모든 항목을 여기에 입력합니다.

Optional CTA

CTA는 article을 읽은 후 reader가 취하기를 원하는 action을 의미합니다. 어떤 text가 suitable한지 확실하지 않다면 이 field를 empty로 두는 것이 좋습니다.

Multilingual profile
웹사이트가 multilingual인 경우 profile은 하나의 language로만 완료하면 됩니다. OxiRanker는 동일한 profile을 기반으로 다른 languages에 필요한 versions를 준비할 수 있습니다.
STEP 07

내부 및 외부 링크

OxiRanker는 articles 안에 internal links와 external links를 intelligent하게 적용할 수 있습니다.

Internal links

이 option은 default로 enabled되어 있으며 enabled 상태를 유지하는 것이 좋습니다. Internal linking은 search engines가 웹사이트의 content structure를 더 잘 이해하도록 돕습니다.

첫 번째 article에서는 previous article이 아직 없기 때문에 internal links가 일반적으로 생성되지 않습니다. 두 번째 article부터 system은 internal linking을 수행할 수 있습니다.

External links

External links는 official 및 trusted sources에 추가되며 SEO authority가 의도치 않게 전달되지 않도록 nofollow로 marked됩니다.

STEP 08

AI로 profile quality 확인

Article Profile을 완료한 후 OxiRanker는 profile quality를 review합니다.

85+
저장에 필요한 minimum score
95
개선 후 일반적으로 도달하는 excellent score
AI
각 field에 대한 professional suggestions
score가 85 미만인 경우
system은 final saving을 허용하지 않으며 각 problematic field에 대한 explanations와 improvement suggestions를 표시합니다. suggestions를 적용한 뒤 다시 save할 수 있습니다.
STEP 09

article summaries를 위해 Telegram 연결

profile을 성공적으로 save한 후 Telegram channel로 article summaries 전송을 enable할 수 있습니다.

1

bot 추가

@Oxiranker_Bot을 Telegram channel에 추가하고 Admin으로 설정하세요.

2

channel username 입력

channel name을 @와 함께 입력하세요. 예: @my_channel

3

summary delivery enable

valid channel을 등록한 후 article summary delivery를 enable하세요.

Telegram benefit
enabled 상태이면 content generation 후 article summary가 image와 link와 함께 Telegram channel로 전송됩니다. 이는 웹사이트로 향하는 traffic paths를 강화할 수 있습니다.
STEP 10

automatic article engine 설정

Automatic article generation tab에서 automatic article generation engine을 관리할 수 있습니다.

Smart topic selection

system은 keywords와 Article Profile을 사용하여 suitable, unique, publishable한 topics를 선택합니다.

Brand-aware generation

article generation 중 tone, audience, content goal 및 brand restrictions가 반영됩니다.

Scheduled publishing

Monthly capacity는 한 달 전체에 분산되어 content publishing이 더 natural하고 consistent하게 보이도록 합니다.

Monthly capacity

7 articles / month
10 articles / month
15 articles / month
30 articles / month

30 articles는 거의 하루에 한 article을 의미합니다. 15 articles는 거의 이틀에 한 번을 의미합니다. 10 articles는 약 2~3일마다를 의미합니다. 7 articles는 약 3~4일마다를 의미합니다.

Article length 및 image

Article length는 Short, Standard 또는 Long일 수 있습니다. Standard는 대부분의 웹사이트에 balanced choice입니다.

Article images는 enabled 또는 disabled로 설정할 수 있으며 image theme은 Light 또는 Dark로 선택할 수 있습니다.

API websites에 대한 note
automatic engine은 OxiRanker 내부에서 articles를 generate합니다. generation 후 각 article을 custom website로 이동하려면 Export API, Webhook, database storage 및 Image Mirroring을 구현해야 합니다.
STEP 11

Generate에서 첫 번째 article 만들기

domain을 준비한 뒤 Generate menu를 열고 domain을 선택한 다음 New Content 또는 New Article을 클릭하세요.

1

Language

각 content request에는 하나의 language만 선택됩니다.

2

Topic

명확한 topic을 작성하세요. Source URL은 optional입니다.

3

Image

image를 enable 또는 disable하고 Light 또는 Dark theme을 선택하세요.

4

Payment 및 generation

cost가 wallet에서 deducted되고 article generation이 시작됩니다.

Generation time
Article 및 image generation은 일반적으로 약 2~3분이 걸립니다. 완료 후 article은 OxiRanker 내부에서 visible합니다.
Source URL
Source URL이 입력되면 OxiRanker는 topic을 더 잘 이해하기 위해 이를 사용하지만 copied content를 generate하지 않습니다. system은 unique content를 만들도록 설계되었습니다.
STEP 12

Preview, SEO Report 및 JSON Output 검토

article이 generated된 후 quality review 및 publishing preparation을 위한 여러 important outputs를 사용할 수 있습니다.

Preview

article text와 generated image를 표시합니다. 이 section은 publishing 전 final quality를 review하는 데 유용합니다.

SEO Report

SEO Score, title, meta description, canonical, keywords, schema, headings, links, images 및 OpenGraph를 포함한 understandable SEO report를 표시합니다.

JSON Output

technical users를 위해 full article output을 표시합니다. 여기에는 slug, html_body, schema, image URLs, tags, category, reading time 및 created/updated timestamps가 포함됩니다.

outputs의 의미
이 outputs는 article이 단순한 plain text가 아님을 보여줍니다. 이는 WordPress, CMS, API 및 publishing systems를 위해 준비된 structured, SEO-ready content입니다.
STEP 13

Export API와 Webhook은 정확히 무엇을 하나요?

OxiRanker에서 article이 generated된 후 destination website는 Export API로 이를 read하거나 article이 ready되었을 때 Webhook을 통해 notified될 수 있습니다.

Method 1: Export API

destination website는 필요할 때 Export Token으로 OxiRanker에서 articles를 read할 수 있습니다. 이 method는 manual sync, daily sync, article list pages 및 full article JSON fetch에 사용됩니다.

GET https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Method 2: Webhook

new article이 created되거나 article이 updated되면 OxiRanker는 destination website endpoint에 notify합니다. Webhook은 full article을 보내지 않습니다. destination website가 Export JSON API에서 full article을 fetch할 수 있도록 article identifier만 보냅니다.

https://example.com/api/oxiranker/webhook
{
  "event": "article.created",
  "domain_id": "1",
  "article_id": "5",
  "request_id": "5",
  "lang": "fa",
  "slug": "smart-internal-links",
  "occurred_at": "2026-05-20T19:35:33.000Z"
}
매우 중요한 note
Webhook은 lightweight message일 뿐입니다. destination website는 Webhook 안에 full article이 있다고 기대하면 안 됩니다. domain_id와 article_id를 받은 후 Export JSON API에서 complete article을 fetch하고 자체 database에 store해야 합니다.
STEP 14

Content storage responsibility 및 Image Mirroring

destination website는 final article과 images를 자체 infrastructure에 store해야 합니다.

destination website가 store해야 하는 항목

destination website database의 full article text
destination server, storage 또는 CDN의 main article image
destination server, storage 또는 CDN의 Open Graph image
SEO, OpenGraph 및 Article Schema data
original output을 보존하기 위한 raw_detail_payload 및 raw_list_item

올바른 image path 예시

OxiRanker image URLs를 directly 및 permanently 사용하는 것은 올바르지 않습니다. image를 download하고 article URLs를 destination website의 internal URLs로 replace해야 합니다.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker는 destination website를 위한 permanent hosting이 아닙니다
OxiRanker는 destination website의 output content와 images를 permanently store할 책임이 없습니다. OxiRanker는 user-content hosting service가 아니며 generated content, cover images, Open Graph images 또는 output files는 OxiRanker infrastructure에 90 days 넘게 남아 있지 않을 수 있습니다. 따라서 destination website는 final article, images, SEO data, OpenGraph 및 Schema를 자체 database, server, storage 또는 CDN에 store해야 합니다.
STEP 15

Export Token 및 ready endpoints 받기

Export Token과 ready-to-use endpoints는 article page, Outputs tab 및 API Output section에서 사용할 수 있습니다.

Export Token을 받을 위치

OxiRanker에서 첫 article을 create하세요.
해당 article page를 여세요.
Outputs tab을 여세요.
API Output section을 여세요.
Token management에서 token을 create하세요.
full token은 한 번만 표시됩니다.
token을 destination website backend 또는 .env에 store하세요.
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}

Ready-to-use endpoints

같은 article page의 API Output section 안에서 Ready-to-use endpoints가 prepared URLs를 표시합니다.

https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0
https://oxiranker.com/api/v1/users/domains/1/articles/1/export/json
https://oxiranker.com/api/v1/users/domains/1/articles/1/export/html?canonical_mode=absolute&include_css=0
Token rotation
OxiRanker panel에서 Export Token이 rotated되면 previous token은 expires됩니다. new value는 destination website .env 또는 backend environment의 old value를 즉시 replace해야 하며 destination backend는 restart 또는 redeploy되어야 합니다.
STEP 16

Webhook Secret 및 destination endpoint 만들기

Webhook Secret도 article page, Outputs tab 및 API Output section에서 생성됩니다.

Webhook Secret은 어디에서 생성되나요?

article page를 여세요.
Outputs tab을 여세요.
API Output section을 여세요.
Webhook Secret section에서 secret을 create하세요.
full secret은 한 번만 표시됩니다.
secret을 destination website .env에 store하세요.

고정 destination endpoint

OxiRanker는 destination Webhook에 대해 다음 fixed path만 허용합니다. destination website는 이 endpoint를 backend에 만들어야 합니다.

POST /api/oxiranker/webhook

https://example.com/api/oxiranker/webhook

이 endpoint는 POST만 accept해야 하며 raw body를 preserve하고 timestamp와 signature를 verify하며 webhook.test에 대해 successful response를 return해야 합니다.

Active events
current version에서 events는 고정되어 있습니다: article.created 및 article.updated. connection test를 위해 event value webhook.test가 전송됩니다.
STEP 17

destination website에 필요한 .env values

OxiRanker는 destination website files 또는 .env에 access할 수 없습니다. destination website developer는 이 values를 자체 backend에 store해야 합니다.

OXIRANKER_EXPORT_API_BASE_URL=https://oxiranker.com
OXIRANKER_EXPORT_API_TOKEN=YOUR_EXPORT_TOKEN
OXIRANKER_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET
OXIRANKER_WEBHOOK_MAX_SKEW_MS=300000
OXIRANKER_SOURCE_PROVIDER=oxiranker
OXIRANKER_SOURCE_DOMAIN_IDS=1

SITE_ARTICLE_IMAGE_UPLOAD_DIR=/var/www/example.com/uploads/site-articles
SITE_ARTICLE_IMAGE_MAX_BYTES=10485760

PUBLIC_BASE_URL=https://example.com

OXIRANKER_SYNC_ENABLED=true
OXIRANKER_SYNC_HOUR=3
OXIRANKER_SYNC_MINUTE=10
OXIRANKER_SYNC_PAGE_LIMIT=20
OXIRANKER_SYNC_MAX_PAGES=50

Security values

OXIRANKER_EXPORT_API_TOKEN
Export API token은 backend에만 store해야 합니다.
OXIRANKER_WEBHOOK_SECRET
Webhook Secret은 backend에만 store해야 합니다.
OXIRANKER_WEBHOOK_MAX_SKEW_MS
Webhook에 허용되는 maximum time difference입니다. 300000은 5 minutes를 의미합니다.

Storage values

SITE_ARTICLE_IMAGE_UPLOAD_DIR
destination server에 mirrored images를 store하기 위한 physical path입니다.
SITE_ARTICLE_IMAGE_MAX_BYTES
download 및 storage에 허용되는 maximum image size입니다.
PUBLIC_BASE_URL
final image 및 article URLs를 만들기 위해 사용되는 public destination website URL입니다.
Frontend security
Export Token과 Webhook Secret은 frontend code, user-visible JavaScript, localStorage 또는 window object 안에 배치하면 안 됩니다.
STEP 18

Export API: List, JSON, HTML 및 ZIP

destination website는 Export API를 사용하여 article list 또는 각 article의 full output을 받을 수 있습니다.

domain의 articles list 받기

GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

limit는 articles 수를 정의하며 현재 1~50 사이로 제한됩니다. offset은 pagination에 사용됩니다. lang은 optional이며 fa, en 또는 tr과 같은 specific language의 articles를 fetch하는 데 사용됩니다.

full article을 JSON으로 받기

GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

이는 destination website에 가장 important한 output입니다. destination website는 full article JSON을 자체 database에 store하고 public display를 위해 자체 database에서 read해야 합니다.

ready HTML 받기

GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/html?canonical_mode=absolute&include_css=0
Authorization: Bearer YOUR_EXPORT_TOKEN

HTML은 ready output을 원하는 웹사이트에 적합하지만 더 professional한 approach는 JSON을 store하고 destination website 자체 template으로 render하는 것입니다.

ZIP output 받기

GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKEN

ZIP output에는 article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt 및 README.txt가 포함됩니다.

STEP 19

Webhook Signature 및 안전한 Webhook 수신

destination website는 original raw body를 사용하여 Webhook signature를 verify해야 합니다.

Webhook headers

content-type: application/json
user-agent: Oxiranker-Webhooks/1.0
x-oxiranker-event: article.created
x-oxiranker-domain-id: 1
x-oxiranker-timestamp: 2026-05-20T19:35:33.000Z
x-oxiranker-signature: sha256=...

Signature formula

payload_to_sign = timestamp + "." + rawBody
signature = "sha256=" + HMAC_SHA256(webhook_secret, payload_to_sign)

destination website가 JSON을 먼저 parse한 뒤 다시 stringify하면 order 또는 spacing이 변경되어 signature가 invalid가 될 수 있습니다. signature verification에는 original raw body를 사용해야 합니다.

Timing-safe comparison
signatures를 expected === received로 비교하지 마세요. Node.js에서는 crypto.timingSafeEqual을 사용하는 것이 더 좋습니다.
STEP 20

Webhook 수신을 위한 simple Node.js code sample

이 Express sample은 raw body, timestamp, signature, webhook.test 및 article events를 처리합니다.

import express from "express";
import crypto from "crypto";

const app = express();

app.use(
  express.json({
    verify: (req, _res, buf) => {
      (req as any).rawBody = Buffer.from(buf);
    },
  })
);

function getHeader(req: express.Request, name: string): string {
  const value = req.headers[name.toLowerCase()];
  if (Array.isArray(value)) return String(value[0] || "").trim();
  return String(value || "").trim();
}

function getRawBody(req: express.Request): string {
  const raw = (req as any).rawBody;
  if (Buffer.isBuffer(raw)) return raw.toString("utf8");
  return JSON.stringify(req.body || {});
}

function createSignature(secret: string, timestamp: string, rawBody: string): string {
  const payload = `${timestamp}.${rawBody}`;
  const digest = crypto
    .createHmac("sha256", secret)
    .update(payload, "utf8")
    .digest("hex");

  return `sha256=${digest}`;
}

function timingSafeEqualText(a: string, b: string): boolean {
  const ab = Buffer.from(a, "utf8");
  const bb = Buffer.from(b, "utf8");

  if (ab.length !== bb.length) return false;
  return crypto.timingSafeEqual(ab, bb);
}

function isTimestampValid(timestamp: string): boolean {
  const maxSkewMs = Number(process.env.OXIRANKER_WEBHOOK_MAX_SKEW_MS || 300000);
  const date = new Date(timestamp);

  if (Number.isNaN(date.getTime())) return false;

  const diff = Math.abs(Date.now() - date.getTime());
  return diff <= maxSkewMs;
}

app.post("/api/oxiranker/webhook", async (req, res) => {
  const secret = String(process.env.OXIRANKER_WEBHOOK_SECRET || "").trim();

  if (!secret) {
    return res.status(500).json({
      error: "webhook_secret_missing",
      message: "Webhook secret is not configured.",
    });
  }

  const eventHeader = getHeader(req, "x-oxiranker-event");
  const domainIdHeader = getHeader(req, "x-oxiranker-domain-id");
  const timestamp = getHeader(req, "x-oxiranker-timestamp");
  const receivedSignature = getHeader(req, "x-oxiranker-signature");
  const rawBody = getRawBody(req);

  if (!eventHeader) {
    return res.status(400).json({
      error: "missing_webhook_event",
      message: "Webhook event header is required.",
    });
  }

  if (!domainIdHeader) {
    return res.status(400).json({
      error: "missing_webhook_domain_id",
      message: "Webhook domain id header is required.",
    });
  }

  if (!timestamp) {
    return res.status(400).json({
      error: "missing_webhook_timestamp",
      message: "Webhook timestamp header is required.",
    });
  }

  if (!receivedSignature) {
    return res.status(400).json({
      error: "missing_webhook_signature",
      message: "Webhook signature header is required.",
    });
  }

  if (!isTimestampValid(timestamp)) {
    return res.status(401).json({
      error: "invalid_webhook_timestamp",
      message: "Webhook timestamp is expired or invalid.",
    });
  }

  const expectedSignature = createSignature(secret, timestamp, rawBody);

  if (!timingSafeEqualText(expectedSignature, receivedSignature)) {
    return res.status(401).json({
      error: "invalid_webhook_signature",
      message: "Invalid webhook signature.",
    });
  }

  const payload = req.body || {};
  const event = String(payload.event || eventHeader || "").trim();

  if (event === "webhook.test") {
    const sourceDomainId = Number(payload.domain_id || domainIdHeader);

    return res.status(200).json({
      ok: true,
      message: "Webhook test received successfully.",
      event,
      source_domain_id:
        Number.isFinite(sourceDomainId) && sourceDomainId > 0 ? sourceDomainId : null,
      test: true,
    });
  }

  if (event !== "article.created" && event !== "article.updated") {
    return res.status(400).json({
      error: "unsupported_webhook_event",
      message: "Webhook event is not supported.",
    });
  }

  const sourceDomainId = Number(payload.domain_id || domainIdHeader);
  const sourceArticleId = Number(payload.article_id);

  if (!Number.isFinite(sourceDomainId) || sourceDomainId <= 0) {
    return res.status(400).json({
      error: "invalid_domain_id",
      message: "Webhook domain id is invalid.",
    });
  }

  if (!Number.isFinite(sourceArticleId) || sourceArticleId <= 0) {
    return res.status(400).json({
      error: "invalid_article_id",
      message: "Webhook article id is invalid.",
    });
  }

  // Fetch the full article from Export API and store it in the database here.
  // await syncArticleFromOxiranker(sourceDomainId, sourceArticleId);

  return res.status(200).json({
    ok: true,
    message: "Webhook processed successfully.",
    event,
    source_domain_id: sourceDomainId,
    source_article_id: sourceArticleId,
  });
});
STEP 21

권장 destination website database

destination website는 articles를 자체 database에 store하고 unique constraint로 duplicates를 방지해야 합니다.

CREATE TABLE public.site_articles (
  id BIGSERIAL PRIMARY KEY,

  source_provider VARCHAR NOT NULL DEFAULT 'oxiranker',
  source_domain_id BIGINT NOT NULL,
  source_article_id BIGINT NOT NULL,
  source_request_id BIGINT NULL,

  lang VARCHAR NOT NULL,
  lang_label VARCHAR NULL,
  dir VARCHAR NOT NULL,
  is_rtl BOOLEAN NULL,

  length VARCHAR NULL,
  topic_text TEXT NULL,

  slug VARCHAR NOT NULL,
  canonical_path VARCHAR NULL,
  effective_canonical_url TEXT NULL,

  title TEXT NOT NULL,
  meta_description TEXT NULL,
  html_body TEXT NULL,

  focus_keywords JSONB NOT NULL DEFAULT '[]'::jsonb,
  category VARCHAR NULL,
  tags JSONB NOT NULL DEFAULT '[]'::jsonb,

  reading_time_minutes INTEGER NULL,

  og_title TEXT NULL,
  og_description TEXT NULL,

  cover_image_url TEXT NULL,
  og_image_url TEXT NULL,

  article_schema JSONB NOT NULL DEFAULT '{}'::jsonb,

  raw_list_item JSONB NOT NULL DEFAULT '{}'::jsonb,
  raw_detail_payload JSONB NOT NULL DEFAULT '{}'::jsonb,

  source_created_at TIMESTAMPTZ NULL,
  source_updated_at TIMESTAMPTZ NULL,

  sync_status VARCHAR NOT NULL DEFAULT 'synced',
  last_synced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  last_sync_error TEXT NULL,

  publish_status VARCHAR NOT NULL DEFAULT 'draft',
  published_at TIMESTAMPTZ NULL,
  deleted_at TIMESTAMPTZ NULL,

  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

  source_cover_image_url TEXT NULL,
  source_og_image_url TEXT NULL,

  image_mirror_status VARCHAR NOT NULL DEFAULT 'pending',
  image_mirror_error TEXT NULL,
  image_mirrored_at TIMESTAMPTZ NULL,

  CONSTRAINT site_articles_dir_check
    CHECK (dir IN ('ltr', 'rtl')),

  CONSTRAINT site_articles_reading_time_check
    CHECK (reading_time_minutes IS NULL OR reading_time_minutes >= 0),

  CONSTRAINT site_articles_sync_status_check
    CHECK (sync_status IN ('pending', 'syncing', 'synced', 'failed')),

  CONSTRAINT site_articles_publish_status_check
    CHECK (publish_status IN ('draft', 'published', 'archived')),

  CONSTRAINT site_articles_image_mirror_status_check
    CHECK (image_mirror_status IN ('pending', 'not_needed', 'mirrored', 'failed')),

  CONSTRAINT site_articles_source_unique
    UNIQUE (source_provider, source_domain_id, source_article_id)
);
CREATE INDEX site_articles_source_lookup_idx
ON public.site_articles (source_provider, source_domain_id, source_article_id);

CREATE INDEX site_articles_source_updated_at_idx
ON public.site_articles (source_updated_at DESC);

CREATE INDEX site_articles_slug_idx
ON public.site_articles (slug)
WHERE deleted_at IS NULL;

CREATE UNIQUE INDEX site_articles_lang_slug_active_unique
ON public.site_articles (lang, slug)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_lang_publish_idx
ON public.site_articles (lang, publish_status)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_published_at_idx
ON public.site_articles (published_at DESC)
WHERE deleted_at IS NULL;

CREATE INDEX site_articles_focus_keywords_gin_idx
ON public.site_articles USING gin (focus_keywords);

CREATE INDEX site_articles_tags_gin_idx
ON public.site_articles USING gin (tags);

CREATE INDEX site_articles_article_schema_gin_idx
ON public.site_articles USING gin (article_schema);
PostgreSQL note
PostgreSQL에서는 Persian 또는 Unicode text에 simple quotes를 사용합니다. N'...' 같은 것은 필요하지 않으며 사용해서는 안 됩니다.
STEP 22

UPSERT로 duplicates 방지

article은 각 Webhook마다 다시 insert되면 안 됩니다. 이미 존재하는 경우 update되어야 합니다.

duplicates 방지를 위한 main key
duplicates를 방지하기 위한 가장 important한 constraint는 다음과 같습니다: UNIQUE (source_provider, source_domain_id, source_article_id)
INSERT INTO public.site_articles (
  source_provider,
  source_domain_id,
  source_article_id,
  source_request_id,
  lang,
  dir,
  slug,
  title,
  meta_description,
  html_body,
  focus_keywords,
  category,
  tags,
  reading_time_minutes,
  cover_image_url,
  og_image_url,
  article_schema,
  source_created_at,
  source_updated_at,
  sync_status,
  last_synced_at,
  publish_status,
  published_at,
  created_at,
  updated_at
)
VALUES (
  'oxiranker',
  1,
  5,
  5,
  'fa',
  'rtl',
  'smart-internal-links',
  'Article title',
  'Meta description',
  '<p>HTML body</p>',
  '[]'::jsonb,
  'SEO',
  '[]'::jsonb,
  7,
  'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
  'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
  '{}'::jsonb,
  '2026-05-20T19:33:23.618Z',
  '2026-05-20T19:34:09.629Z',
  'synced',
  now(),
  'published',
  now(),
  now(),
  now()
)
ON CONFLICT (source_provider, source_domain_id, source_article_id)
DO UPDATE SET
  source_request_id = EXCLUDED.source_request_id,
  lang = EXCLUDED.lang,
  dir = EXCLUDED.dir,
  slug = EXCLUDED.slug,
  title = EXCLUDED.title,
  meta_description = EXCLUDED.meta_description,
  html_body = EXCLUDED.html_body,
  focus_keywords = EXCLUDED.focus_keywords,
  category = EXCLUDED.category,
  tags = EXCLUDED.tags,
  reading_time_minutes = EXCLUDED.reading_time_minutes,
  cover_image_url = EXCLUDED.cover_image_url,
  og_image_url = EXCLUDED.og_image_url,
  article_schema = EXCLUDED.article_schema,
  source_created_at = EXCLUDED.source_created_at,
  source_updated_at = EXCLUDED.source_updated_at,
  sync_status = 'synced',
  last_synced_at = now(),
  last_sync_error = NULL,
  publish_status = CASE
    WHEN public.site_articles.publish_status = 'archived'
    THEN public.site_articles.publish_status
    ELSE 'published'
  END,
  published_at = COALESCE(public.site_articles.published_at, EXCLUDED.published_at, now()),
  deleted_at = NULL,
  updated_at = now();
STEP 23

Webhook 이후 full article fetch 및 sync

Webhook이 도착하면 destination website는 Export API에서 full article을 fetch하고 images를 mirror한 뒤 article을 UPSERT해야 합니다.

async function fetchOxirankerArticleDetail(args: {
  baseUrl: string;
  token: string;
  domainId: number;
  articleId: number;
}) {
  const url = `${args.baseUrl.replace(/\/+$/, "")}/api/v1/users/domains/${args.domainId}/articles/${args.articleId}/export/json`;

  const response = await fetch(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${args.token}`,
      Accept: "application/json",
      "User-Agent": "My-Site-Oxiranker-Sync/1.0",
    },
  });

  const bodyText = await response.text();

  if (response.status < 200 || response.status > 299) {
    throw new Error(
      `Export API request failed with HTTP status ${response.status}. Response: ${bodyText.slice(0, 1000)}`
    );
  }

  try {
    return JSON.parse(bodyText);
  } catch {
    throw new Error("Export API returned invalid JSON.");
  }
}
async function syncArticleFromWebhook(payload: any) {
  const sourceDomainId = Number(payload.domain_id);
  const sourceArticleId = Number(payload.article_id);

  if (!Number.isFinite(sourceDomainId) || sourceDomainId <= 0) {
    throw new Error("Webhook domain id is invalid.");
  }

  if (!Number.isFinite(sourceArticleId) || sourceArticleId <= 0) {
    throw new Error("Webhook article id is invalid.");
  }

  const article = await fetchOxirankerArticleDetail({
    baseUrl: process.env.OXIRANKER_EXPORT_API_BASE_URL!,
    token: process.env.OXIRANKER_EXPORT_API_TOKEN!,
    domainId: sourceDomainId,
    articleId: sourceArticleId,
  });

  const mirroredImages = await mirrorArticleImages(article);

  const finalHtmlBody = replaceImageUrls(article.html_body, mirroredImages);
  const finalArticleSchema = replaceImageUrlsInJson(article.article_schema, mirroredImages);

  await upsertSiteArticle({
    source_provider: process.env.OXIRANKER_SOURCE_PROVIDER || "oxiranker",
    source_domain_id: sourceDomainId,
    source_article_id: sourceArticleId,
    source_request_id: article.request_id,
    lang: article.lang,
    dir: article.dir,
    slug: article.slug,
    title: article.title,
    meta_description: article.meta_description,
    html_body: finalHtmlBody,
    focus_keywords: article.focus_keywords,
    category: article.category,
    tags: article.tags,
    reading_time_minutes: article.reading_time_minutes,
    cover_image_url: mirroredImages.cover_image_url,
    og_image_url: mirroredImages.og_image_url,
    article_schema: finalArticleSchema,
    raw_detail_payload: article,
    source_created_at: article.created_at,
    source_updated_at: article.updated_at,
  });
}
STEP 24

Image Mirroring 및 image storage rules

destination website는 images를 download하고 자체 infrastructure에 store하며 article URLs를 replace해야 합니다.

Image Mirroring rules

HTTPS URLs만 accept해야 합니다.
file은 실제로 image여야 합니다.
content-type은 image/로 시작해야 합니다.
empty files는 store하면 안 됩니다.
file size는 limit를 초과하면 안 됩니다.
cover와 og가 같으면 한 번만 download하세요.
mirroring 후 html_body 및 article_schema 안의 URLs를 replace하세요.

권장 storage path

/uploads/site-articles/oxiranker/1/5/cover.webp
/uploads/site-articles/oxiranker/1/5/og.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
STEP 25

신뢰성을 위한 daily sync

Webhook만으로는 충분하지 않습니다. destination website는 missed articles를 recover하기 위한 daily sync도 가져야 합니다.

daily sync가 필요한 이유

destination website가 일시적으로 down될 수 있습니다.
Webhook이 timeout될 수 있습니다.
deployment 또는 network issues가 발생할 수 있습니다.
article이 generated되었지만 해당 Webhook이 missed될 수 있습니다.

Daily sync algorithm

offset을 0부터 시작하세요.
limit 20 또는 50을 사용하세요.
Export List API에서 article list를 fetch하세요.
각 item에 대해 해당 article의 Export JSON을 fetch하세요.
images를 mirror하세요.
article을 UPSERT로 store하세요.
offset을 increase하고 total이 completed될 때까지 continue하세요.
STEP 26

destination website에 articles 표시

articles를 site_articles에 store한 후 destination website는 각 public display를 위해 OxiRanker에 연결하면 안 됩니다. 자체 database에서 read해야 합니다.

Blog list page

SELECT
  id,
  lang,
  lang_label,
  dir,
  is_rtl,
  slug,
  title,
  meta_description,
  category,
  tags,
  reading_time_minutes,
  cover_image_url,
  og_image_url,
  source_created_at,
  published_at,
  created_at
FROM public.site_articles
WHERE source_provider = 'oxiranker'
  AND deleted_at IS NULL
  AND sync_status = 'synced'
  AND publish_status = 'published'
ORDER BY COALESCE(source_created_at, published_at, created_at) DESC, id DESC
LIMIT 20 OFFSET 0;

Article detail page

SELECT *
FROM public.site_articles
WHERE source_provider = 'oxiranker'
  AND lang = 'fa'
  AND slug = 'smart-internal-links'
  AND deleted_at IS NULL
  AND sync_status = 'synced'
  AND publish_status = 'published'
LIMIT 1;
권장 article URL
Multilingual websites의 경우 /{lang}/blog/{slug} pattern을 사용할 수 있습니다. 예: /fa/blog/smart-internal-links
STEP 27

SEO, Schema, RTL 및 HTML rendering

destination website는 자체 database에서 SEO data를 read하고 article page에서 render해야 합니다.

SEO fields

page title용 title
meta description용 meta_description
canonical용 effective_canonical_url
Open Graph용 og_title 및 og_description
social preview image용 og_image_url 또는 cover_image_url
JSON-LD용 article_schema

HTML rendering

<div
  className="article-content"
  dangerouslySetInnerHTML={{ __html: article.html_body }}
/>

destination website에 strict security policy가 있는 경우 save 전 또는 display 전에 HTML을 sanitize할 수 있습니다.

RTL 및 LTR

각 article에는 lang, dir 및 is_rtl fields가 있습니다. dir이 rtl이면 article page는 right-to-left로 render되어야 합니다.

<article dir="rtl">
  ...
</article>
STEP 28

일반적인 errors와 의미

destination website errors는 troubleshooting을 빠르게 할 수 있도록 English, clear 및 understandable해야 합니다.

Export API errors

domain export token이 없습니다.
Export API token이 전송되지 않았습니다. Authorization: Bearer YOUR_EXPORT_TOKEN을 전송해야 합니다.
domain export token이 invalid입니다.
token이 wrong이거나, rotated되었거나, 이 domain과 관련이 없거나, domainId가 wrong입니다.
token에 required scope가 없습니다.
token에 required scope가 없습니다. new tokens에는 articles:read 및 exports:read가 있어야 합니다.
lang이 invalid입니다.
query parameter의 lang value가 incorrectly 전송되었습니다.

Webhook errors

Webhook URL은 HTTPS를 사용해야 합니다.
Webhook URL은 https://로 시작해야 합니다.
Webhook signature가 invalid입니다.
Webhook Secret이 wrong이거나, secret이 rotated되었거나, raw body가 changed되었거나, signature가 incorrectly calculated되었습니다.
Webhook timestamp가 expired되었거나 invalid입니다.
timestamp가 old이거나 future에 있거나 invalid입니다. server clock 및 NTP를 확인하세요.
Webhook test delivery가 실패했습니다.
destination website가 successful 200~299 status를 return하지 않았거나, timed out되었거나, webhook.test를 correctly handle하지 않았습니다.
STEP 29

최종 API 및 Webhook testing checklist

implementation 후 이 tests는 connection을 beginning부터 end까지 verify합니다.

Export Token 및 JSON test

GET https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=2&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Webhook test

destination Webhook을 Postman으로 signature 없이 call하면 security error를 받아야 합니다. 이는 endpoint가 active이고 security를 check한다는 의미입니다.

{
  "error": "missing_webhook_signature",
  "message": "Webhook signature header is required."
}

그런 다음 OxiRanker 안에서 Test Webhook을 클릭하세요. receiver가 webhook.test를 support하면 response_status는 200이어야 합니다.

Rotation 후
Webhook Secret 또는 Export Token이 rotated되면 new value를 즉시 copy하고 destination website .env에서 replace한 뒤 destination backend를 restart 또는 redeploy하세요.
STEP 30

올바른 implementation을 위한 important rules

system이 secure, stable 및 maintainable 상태를 유지하도록 API integration에서 이 rules를 따라야 합니다.

Export Token은 backend에만 store해야 합니다.
Webhook Secret은 backend에만 store해야 합니다.
destination Webhook endpoint는 정확히 /api/oxiranker/webhook이어야 합니다.
Webhook URL은 HTTPS를 사용해야 합니다.
Webhook signature와 timestamp는 항상 verify되어야 합니다.
Articles는 simple insert가 아니라 UPSERT로 store해야 합니다.
duplicates를 방지하려면 unique constraint가 required입니다.
Images는 OxiRanker에서 download되어 destination website에 store되어야 합니다.
destination website는 OxiRanker image URLs에 permanently depend하면 안 됩니다.
Image URLs는 html_body 및 article_schema 안에서 replace되어야 합니다.
Webhook이 fail하면 daily sync가 missed article을 recover해야 합니다.
users 또는 logs에 표시되는 errors는 English이고 understandable해야 합니다.
article이 archived된 경우 automatic sync는 이를 다시 publish하면 안 됩니다.
original output data가 손실되지 않도록 raw_detail_payload와 raw_list_item을 preserve해야 합니다.
Export Token 또는 Webhook Secret이 rotated되면 new value를 .env에서 replace하고 backend를 restart 또는 redeploy해야 합니다.
STEP 31

Complete API flow summary

이 steps를 완료하면 custom website는 OxiRanker에서 generated된 articles를 안전하게 receive, store, mirror 및 publish할 수 있습니다.

OxiRanker에 가입하기
domain 추가하기
domain ownership verify하기
languages 선택 및 Blog URL 추가
각 language에 keywords 추가
Article Profile 완료
필요한 경우 Telegram 설정
automatic article engine 설정
Generate에서 첫 article create
Preview 및 SEO Report review
API Output에서 Export Token 받기
ready Export API endpoints 받기
Webhook Secret 받기
destination backend에 fixed /api/oxiranker/webhook endpoint 만들기
Token 및 Secret을 destination website .env에 store
site_articles table 및 unique constraint 만들기
Export JSON에서 full article fetching 구현
Image Mirroring 구현
UPSERT 구현
daily sync 구현
destination database에서 article, SEO, OG 및 Schema render
final Export API 및 Webhook tests 실행