DOCUMENTACIÓN PARA DESARROLLADORES DE OXIRANKER

Documentación de OxiRanker

Un centro de documentación claro para conectar tu sitio web con los servicios de OxiRanker. Empieza con la integración de WordPress o la integración API y luego sigue la guía paso a paso.

Documentación oficialVersión V1 completa con todas las funcionesPara propietarios de sitios, desarrolladores y equipos de integración
Menú de documentación
Elige una guía desde el menú. Esta página es el centro principal de documentación de OxiRanker.
Servicio API

Servicio API

Usa Export API y Webhook para sitios web personalizados, plataformas CMS personalizadas, Next.js, Laravel, Node.js, PHP o backends de Python.

Dominio
Verificado
Contenido
Listo
API
Webhook
Aviso importante de almacenamiento

OxiRanker no es hosting permanente para el sitio web de destino

OxiRanker no ofrece una garantía de hosting permanente para el contenido, las imágenes o los archivos exportados del sitio web de destino. OxiRanker no es un servicio de hosting de contenido de usuarios, y el contenido generado, las imágenes de portada, las imágenes Open Graph o los archivos de salida pueden no permanecer en la infraestructura de OxiRanker durante más de 90 días.

Por lo tanto, después de recibir la salida, el sitio web de destino debe almacenar el artículo final, las imágenes, los datos SEO, los datos Open Graph y los datos Schema en su propia base de datos, servidor, almacenamiento o CDN, y no debe depender de las URL de OxiRanker para la visualización pública.

Video de formación

Video de formación de conexión

Usa este video como recorrido visual antes de conectar WordPress o implementar la integración API.

Salida API

Implementa el flujo de salida paso a paso

Guarda primero el Export Token y el Webhook Secret en tu backend, luego completa el Webhook receiver, la base de datos, la lógica UPSERT, Image Mirroring y el sync diario.

PASO 01

Registrarse y crear un dominio en OxiRanker

Primero, regístrate en OxiRanker, inicia sesión en tu cuenta y haz clic en Add Domain desde la sección Domains.

Formato correcto del dominio

En el campo Domain name, introduce solo el dominio raíz. La dirección no debe incluir http, https ni www.

example.com

Formatos incorrectos

No introduzcas estos formatos en el campo del dominio.

https://example.com
http://example.com
www.example.com
Después de crear el dominio
Si el formato del dominio es válido, OxiRanker crea el dominio y te lleva al paso de verificación de propiedad del dominio.
PASO 02

Verificar la propiedad del dominio

En este paso, debes demostrar que el dominio te pertenece o que tienes acceso para administrarlo.

DNS TXT

Recomendado

Añade un TXT record al DNS del dominio. Para Cloudflare, este suele ser el método más simple.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

Coloca un archivo de texto en la ruta .well-known de tu sitio web para que OxiRanker pueda verificarlo.

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

File Content:
220daaa37****42730f27

Meta Tag

Coloca un meta tag dentro de la sección head de la página de inicio de tu sitio web.

<meta name="yourdomain-verification" content="190208d****d275b65" />
Nota importante sobre DNS
Después de añadir el DNS record, especialmente en Cloudflare, puede que tengas que esperar de 2 a 3 minutos. Luego haz clic en Confirm en la misma página de OxiRanker.
Duración del token de verificación
Los valores de verificación del dominio tienen un límite de tiempo. Actualmente, los valores de verificación suelen ser válidos durante 1440 minutos.
PASO 03

Recibir tokens gratuitos después de verificar el primer dominio

Después de verificar el primer dominio, OxiRanker añade 1000 tokens gratuitos a tu cuenta para pruebas iniciales.

Solo para el primer dominio
Este regalo solo se añade para el primer dominio verificado y no se vuelve a añadir para dominios posteriores.
PASO 04

Configurar idiomas y Blog URL

En la sección Blog URLs, define si tu sitio web es de un solo idioma o multilingüe y qué idiomas deben usarse para los artículos generados.

Elegir idiomas

OxiRanker admite 24 idiomas. Puedes elegir uno o varios idiomas según la estructura de tu sitio web.

Introducir Blog URL

Para cada idioma, introduce la URL del blog de ese idioma. Si tu sitio web es multilingüe, introduce cada ruta de idioma por separado.

https://example.com/fa/blog
https://example.com/en/blog
Estructura de URL
Si tu sitio web tiene una estructura diferente, introduce la URL real del blog de tu sitio web. El punto importante es que la URL final debe ser la ruta donde se deben publicar los artículos.
PASO 05

Añadir palabras clave para cada idioma

En la pestaña Keywords, añade al menos 3 y hasta 50 palabras clave para cada idioma.

Regla principal

Las palabras clave de cada idioma deben introducirse en el mismo idioma. Si el idioma del artículo es inglés, las palabras clave también deben estar en inglés.

Cómo añadir palabras clave

Después de escribir cada palabra clave, pulsa Enter para añadirla. Después de completar todas las palabras clave, haz clic en Save.

Error común
Si introduces palabras clave en turco para el idioma inglés, pueden aparecer frases turcas en el contenido en inglés. El idioma de las palabras clave debe coincidir con el idioma del artículo.
PASO 06

Completar el Article Profile del dominio

Article Profile indica a OxiRanker qué es tu marca, para quién escribe, qué tono debe usar y qué temas debe evitar.

Industria

Define el mercado o la categoría del negocio. Ejemplo: consultoría inmobiliaria y compra y venta de propiedades en Dubái.

Descripción del negocio

Explica qué vende el negocio, a quién sirve, cómo funciona y qué lo hace diferente.

Audiencia

Define a quién debe dirigirse el contenido. Cada audiencia puede guardarse por separado.

Tono

Controla el estilo y la voz del contenido generado, como formal, educativo, simple, técnico u orientado a ventas.

Objetivo del contenido

Define qué deben lograr los artículos, como atraer clientes, aumentar el tráfico o educar a los usuarios.

Temas prohibidos

Cualquier cosa de la que la marca no deba hablar o con la que no deba asociarse se introduce aquí.

CTA opcional

CTA significa la acción que quieres que el lector realice después de leer el artículo. Si no estás seguro de qué texto es adecuado, es mejor dejar este campo vacío.

Perfil multilingüe
Si tu sitio web es multilingüe, solo necesitas completar el perfil en un idioma. OxiRanker puede preparar las versiones requeridas para otros idiomas basándose en el mismo perfil.
PASO 07

Enlaces internos y externos

OxiRanker puede aplicar inteligentemente enlaces internos y externos dentro de los artículos.

Enlaces internos

Esta opción está activada por defecto y es mejor mantenerla activada. Los enlaces internos ayudan a los motores de búsqueda a entender mejor la estructura de contenido de tu sitio web.

En el primer artículo, normalmente no se crean enlaces internos porque todavía no hay ningún artículo anterior. A partir del segundo artículo, el sistema puede realizar enlaces internos.

Enlaces externos

Los enlaces externos se añaden a fuentes oficiales y confiables y se marcan como nofollow para que la autoridad SEO no se transfiera involuntariamente.

PASO 08

Comprobar la calidad del perfil con AI

Después de completar el Article Profile, OxiRanker revisa la calidad del perfil.

85+
Puntuación mínima requerida para guardar
95
Puntuación excelente que suele alcanzarse después de mejoras
AI
Sugerencias profesionales para cada campo
Si la puntuación está por debajo de 85
El sistema no permite el guardado final y muestra explicaciones y sugerencias de mejora para cada campo problemático. Puedes aplicar las sugerencias y guardar de nuevo.
PASO 09

Conectar Telegram para resúmenes de artículos

Después de guardar correctamente el perfil, puedes activar el envío de resúmenes de artículos a un canal de Telegram.

1

Añadir el bot

Añade @Oxiranker_Bot a tu canal de Telegram y hazlo Admin.

2

Introducir el nombre de usuario del canal

Introduce el nombre del canal con @. Ejemplo: @my_channel

3

Activar envío de resumen

Después de registrar un canal válido, activa el envío de resúmenes de artículos.

Beneficio de Telegram
Si está activado, después de la generación del contenido, el resumen del artículo se envía al canal de Telegram con la imagen y el enlace. Esto puede fortalecer las rutas de tráfico hacia tu sitio web.
PASO 10

Configurar el motor automático de artículos

En la pestaña Automatic article generation, puedes gestionar el motor automático de generación de artículos.

Selección inteligente de temas

El sistema usa palabras clave y Article Profile para seleccionar temas adecuados, únicos y publicables.

Generación consciente de la marca

Durante la generación del artículo se respetan el tono, la audiencia, el objetivo del contenido y las restricciones de la marca.

Publicación programada

La capacidad mensual se distribuye a lo largo del mes para que la publicación de contenido parezca más natural y consistente.

Capacidad mensual

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

30 artículos significa casi un artículo al día. 15 artículos significa casi día por medio. 10 artículos significa aproximadamente cada dos o tres días. 7 artículos significa aproximadamente cada tres o cuatro días.

Longitud del artículo e imagen

La longitud del artículo puede ser Short, Standard o Long. Standard es una opción equilibrada para la mayoría de los sitios web.

Las imágenes de artículos pueden activarse o desactivarse, y el tema de la imagen puede ser Light o Dark.

Nota para sitios API
El motor automático genera artículos dentro de OxiRanker. Para mover cada artículo a tu sitio web personalizado después de la generación, debes implementar Export API, Webhook, almacenamiento en base de datos e Image Mirroring.
PASO 11

Crear el primer artículo desde Generate

Después de preparar el dominio, abre el menú Generate, elige el dominio y haz clic en New Content o New Article.

1

Idioma

Solo se selecciona un idioma para cada solicitud de contenido.

2

Tema

Escribe un tema claro. Source URL es opcional.

3

Imagen

Activa o desactiva la imagen y elige el tema Light o Dark.

4

Pago y generación

El coste se descuenta del wallet y comienza la generación del artículo.

Tiempo de generación
La generación del artículo y la imagen suele tardar unos 2 a 3 minutos. Después de completarse, el artículo es visible dentro de OxiRanker.
Source URL
Si se introduce Source URL, OxiRanker la usa para entender mejor el tema, pero no genera contenido copiado. El sistema está diseñado para crear contenido único.
PASO 12

Revisar Preview, SEO Report y JSON Output

Después de generar el artículo, hay varias salidas importantes disponibles para revisar la calidad y preparar la publicación.

Preview

Muestra el texto del artículo y la imagen generada. Esta sección es útil para revisar la calidad final antes de publicar.

SEO Report

Muestra un informe SEO comprensible que incluye SEO Score, title, meta description, canonical, keywords, schema, headings, links, images y OpenGraph.

JSON Output

Muestra la salida completa del artículo para usuarios técnicos, incluyendo slug, html_body, schema, URLs de imágenes, tags, category, tiempo de lectura y timestamps de creación/actualización.

Significado de las salidas
Estas salidas muestran que el artículo no es solo texto plano. Es contenido estructurado, preparado para SEO y listo para WordPress, CMS, API y sistemas de publicación.
PASO 13

¿Qué hacen exactamente Export API y Webhook?

Después de que se genera un artículo en OxiRanker, el sitio web de destino puede leerlo con Export API o recibir una notificación mediante Webhook cuando el artículo esté listo.

Método 1: Export API

El sitio web de destino puede leer artículos desde OxiRanker con el Export Token cuando sea necesario. Este método se usa para sync manual, sync diario, páginas de lista de artículos y obtención del JSON completo del artículo.

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

Método 2: Webhook

Cuando se crea un nuevo artículo o se actualiza un artículo, OxiRanker notifica al endpoint del sitio web de destino. Webhook no envía el artículo completo. Solo envía el identificador del artículo para que el sitio web de destino pueda obtener el artículo completo desde la Export JSON API.

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"
}
Nota muy importante
Webhook es solo un mensaje ligero. El sitio web de destino no debe esperar el artículo completo dentro del Webhook. Después de recibir domain_id y article_id, debe obtener el artículo completo desde la Export JSON API y almacenarlo en su propia base de datos.
PASO 14

Responsabilidad de almacenamiento de contenido e Image Mirroring

El sitio web de destino debe almacenar el artículo final y las imágenes en su propia infraestructura.

Qué debe almacenar el sitio web de destino

Texto completo del artículo en la base de datos del sitio web de destino
Imagen principal del artículo en el servidor, almacenamiento o CDN de destino
Imagen Open Graph en el servidor, almacenamiento o CDN de destino
Datos SEO, OpenGraph y Article Schema
raw_detail_payload y raw_list_item para preservar la salida original

Ejemplo correcto de ruta de imagen

Usar las URL de imágenes de OxiRanker directamente y de forma permanente no es correcto. La imagen debe descargarse y las URL del artículo deben reemplazarse por URL internas del sitio web de destino.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker no es hosting permanente para el sitio web de destino
OxiRanker no es responsable de almacenar permanentemente el contenido de salida y las imágenes del sitio web de destino. OxiRanker no es un servicio de hosting de contenido de usuarios, y el contenido generado, las imágenes de portada, las imágenes Open Graph o los archivos de salida pueden no permanecer en la infraestructura de OxiRanker durante más de 90 días. Por lo tanto, el sitio web de destino debe almacenar el artículo final, las imágenes, los datos SEO, OpenGraph y Schema en su propia base de datos, servidor, almacenamiento o CDN.
PASO 15

Obtener Export Token y endpoints listos

Export Token y los endpoints listos para usar están disponibles desde la página del artículo, la pestaña Outputs y la sección API Output.

Dónde obtener el Export Token

Crea el primer artículo en OxiRanker.
Abre la página de ese artículo.
Abre la pestaña Outputs.
Abre la sección API Output.
Crea un token desde Token management.
El token completo se muestra solo una vez.
Guarda el token en el backend del sitio web de destino o en .env.
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}

Endpoints listos para usar

En la misma página del artículo, dentro de la sección API Output, Ready-to-use endpoints muestra URLs preparadas.

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
Rotar el Token
Si el Export Token se rota en el panel de OxiRanker, el token anterior expira. El nuevo valor debe reemplazar inmediatamente al valor anterior en el .env o entorno backend del sitio web de destino, y el backend de destino debe reiniciarse o redesplegarse.
PASO 16

Crear Webhook Secret y endpoint de destino

Webhook Secret también se crea desde la página del artículo, la pestaña Outputs y la sección API Output.

¿Dónde se crea Webhook Secret?

Abre la página del artículo.
Abre la pestaña Outputs.
Abre la sección API Output.
Crea un secret en la sección Webhook Secret.
El secret completo se muestra solo una vez.
Guarda el secret en el .env del sitio web de destino.

Endpoint fijo de destino

OxiRanker solo acepta la siguiente ruta fija para el Webhook de destino. El sitio web de destino debe crear este endpoint en su backend.

POST /api/oxiranker/webhook

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

Este endpoint solo debe aceptar POST, preservar el raw body, verificar timestamp y signature, y devolver una respuesta exitosa para webhook.test.

Eventos activos
En la versión actual, los eventos son fijos: article.created y article.updated. Para probar la conexión, se envía el valor de evento webhook.test.
PASO 17

Valores .env requeridos para el sitio web de destino

OxiRanker no tiene acceso a los archivos del sitio web de destino ni a su .env. El desarrollador del sitio web de destino debe almacenar estos valores en su propio backend.

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

Valores de seguridad

OXIRANKER_EXPORT_API_TOKEN
El token de Export API solo debe almacenarse en el backend.
OXIRANKER_WEBHOOK_SECRET
El Webhook Secret solo debe almacenarse en el backend.
OXIRANKER_WEBHOOK_MAX_SKEW_MS
La diferencia máxima de tiempo permitida para Webhook. 300000 significa 5 minutos.

Valores de almacenamiento

SITE_ARTICLE_IMAGE_UPLOAD_DIR
La ruta física para almacenar imágenes reflejadas en el servidor de destino.
SITE_ARTICLE_IMAGE_MAX_BYTES
El tamaño máximo permitido de imagen para descarga y almacenamiento.
PUBLIC_BASE_URL
La URL pública del sitio web de destino usada para crear las URL finales de imágenes y artículos.
Seguridad del frontend
Export Token y Webhook Secret no deben colocarse dentro del código frontend, JavaScript visible para el usuario, localStorage o el objeto window.
PASO 18

Export API: List, JSON, HTML y ZIP

El sitio web de destino puede usar Export API para recibir la lista de artículos o la salida completa de cada artículo.

Obtener la lista de artículos para un dominio

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

limit define el número de artículos y actualmente está limitado entre 1 y 50. offset se usa para paginación. lang es opcional y se usa para obtener artículos de un idioma específico como fa, en o tr.

Obtener el artículo completo como JSON

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

Esta es la salida más importante para el sitio web de destino. El sitio web de destino debe almacenar el JSON completo del artículo en su propia base de datos y leer desde su propia base de datos para la visualización pública.

Obtener HTML listo

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 es adecuado para sitios web que quieren una salida lista, pero el enfoque más profesional es almacenar JSON y renderizarlo con la propia plantilla del sitio web de destino.

Obtener salida ZIP

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

La salida ZIP incluye article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt y README.txt.

PASO 19

Firma de Webhook y recepción segura de Webhook

El sitio web de destino debe verificar la firma de Webhook usando el raw body original.

Headers de Webhook

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=...

Fórmula de firma

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

Si el sitio web de destino parsea JSON primero y luego lo vuelve a convertir en string, el orden o los espacios pueden cambiar y la firma puede volverse inválida. El raw body original debe usarse para la verificación de firma.

Comparación timing-safe
No compares firmas con expected === received. En Node.js, es mejor usar crypto.timingSafeEqual.
PASO 20

Ejemplo simple de código Node.js para recibir Webhook

Este ejemplo de Express maneja raw body, timestamp, signature, webhook.test y eventos de artículos.

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,
  });
});
PASO 21

Base de datos recomendada para el sitio web de destino

El sitio web de destino debe almacenar artículos en su propia base de datos y prevenir duplicados con una unique constraint.

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);
Nota de PostgreSQL
En PostgreSQL, se usan comillas simples para texto persa o Unicode. Algo como N'...' no es necesario y no debe usarse.
PASO 22

Prevenir duplicados con UPSERT

El artículo no debe insertarse de nuevo por cada Webhook. Si ya existe, debe actualizarse.

Clave principal para prevenir duplicados
La constraint más importante para prevenir duplicados es: 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();
PASO 23

Obtener el artículo completo y sincronizar después del Webhook

Cuando llega un Webhook, el sitio web de destino debe obtener el artículo completo desde Export API, reflejar las imágenes y hacer UPSERT del artículo.

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,
  });
}
PASO 24

Image Mirroring y reglas de almacenamiento de imágenes

El sitio web de destino debe descargar imágenes, almacenarlas en su propia infraestructura y reemplazar las URL del artículo.

Reglas de Image Mirroring

Solo deben aceptarse URL HTTPS.
El archivo debe ser realmente una imagen.
content-type debe comenzar con image/.
Los archivos vacíos no deben almacenarse.
El tamaño del archivo no debe superar el límite.
Si cover y og son iguales, descarga solo una vez.
Después del mirroring, reemplaza las URL dentro de html_body y article_schema.

Ruta de almacenamiento recomendada

/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
PASO 25

Sync diario para fiabilidad

Webhook por sí solo no es suficiente. El sitio web de destino también debería tener un sync diario para recuperar artículos perdidos.

¿Por qué se necesita sync diario?

El sitio web de destino puede estar temporalmente caído.
Webhook puede hacer timeout.
Pueden ocurrir problemas de despliegue o red.
Un artículo puede generarse, pero su Webhook puede perderse.

Algoritmo de sync diario

Inicia offset desde 0.
Usa limit 20 o 50.
Obtén la lista de artículos desde Export List API.
Para cada elemento, obtén el Export JSON de ese artículo.
Refleja las imágenes.
Guarda el artículo con UPSERT.
Aumenta offset y continúa hasta que total se complete.
PASO 26

Mostrar artículos en el sitio web de destino

Después de almacenar artículos en site_articles, el sitio web de destino no debe conectarse a OxiRanker para cada visualización pública. Debe leer desde su propia base de datos.

Página de lista del blog

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;

Página de detalle del artículo

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;
URL de artículo recomendada
Para sitios web multilingües, puedes usar el patrón /{lang}/blog/{slug}. Ejemplo: /fa/blog/smart-internal-links
PASO 27

SEO, Schema, RTL y renderizado HTML

El sitio web de destino debe leer los datos SEO desde su propia base de datos y renderizarlos en la página del artículo.

Campos SEO

title para el título de la página
meta_description para meta description
effective_canonical_url para canonical
og_title y og_description para Open Graph
og_image_url o cover_image_url para la imagen de vista previa social
article_schema para JSON-LD

Renderizado HTML

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

Si el sitio web de destino tiene una política de seguridad estricta, puede sanitizar HTML antes de guardarlo o antes de mostrarlo.

RTL y LTR

Cada artículo tiene campos lang, dir e is_rtl. Si dir es rtl, la página del artículo debe renderizarse de derecha a izquierda.

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

Errores comunes y sus significados

Los errores del sitio web de destino deben estar en inglés, ser claros y comprensibles para que la resolución de problemas se pueda hacer rápidamente.

Errores de Export API

Falta el token de exportación del dominio.
No se envió el token de Export API. Debe enviarse Authorization: Bearer YOUR_EXPORT_TOKEN.
Token de exportación del dominio inválido.
El token es incorrecto, fue rotado, no está relacionado con este dominio o el domainId es incorrecto.
El token no tiene el scope requerido.
El token no tiene el scope requerido. Los nuevos tokens deben tener articles:read y exports:read.
lang inválido.
El valor lang en el query parameter se envió incorrectamente.

Errores de Webhook

La URL de Webhook debe usar HTTPS.
La URL de Webhook debe comenzar con https://.
Firma de Webhook inválida.
Webhook Secret es incorrecto, el secret fue rotado, el raw body cambió o la firma se calculó incorrectamente.
El timestamp de Webhook está expirado o es inválido.
El timestamp es antiguo, está en el futuro o es inválido. Verifica el reloj del servidor y NTP.
Falló la entrega de prueba de Webhook.
El sitio web de destino no devolvió un estado exitoso de 200 a 299, hizo timeout o no manejó webhook.test correctamente.
PASO 29

Checklist final de pruebas de API y Webhook

Después de la implementación, estas pruebas verifican la conexión de principio a fin.

Prueba de Export Token y JSON

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

Prueba de Webhook

Si llamas al Webhook de destino con Postman sin una firma, debes recibir un error de seguridad. Esto significa que el endpoint está activo y verifica la seguridad.

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

Luego haz clic en Test Webhook dentro de OxiRanker. Si el receiver admite webhook.test, response_status debe ser 200.

Después de la rotación
Si Webhook Secret o Export Token se rota, copia el nuevo valor inmediatamente, reemplázalo en el .env del sitio web de destino y reinicia o redespliega el backend de destino.
PASO 30

Reglas importantes para una implementación correcta

Estas reglas deben seguirse en la integración API para que el sistema permanezca seguro, estable y mantenible.

Export Token solo debe almacenarse en el backend.
Webhook Secret solo debe almacenarse en el backend.
El endpoint del Webhook de destino debe ser exactamente /api/oxiranker/webhook.
La URL de Webhook debe usar HTTPS.
La firma de Webhook y el timestamp siempre deben verificarse.
Los artículos deben almacenarse con UPSERT, no con un insert simple.
Se requiere una unique constraint para prevenir duplicados.
Las imágenes deben descargarse desde OxiRanker y almacenarse en el sitio web de destino.
El sitio web de destino no debe depender permanentemente de las URL de imágenes de OxiRanker.
Las URL de imágenes deben reemplazarse dentro de html_body y article_schema.
Si Webhook falla, el sync diario debe recuperar el artículo perdido.
Los errores mostrados a usuarios o logs deben estar en inglés y ser comprensibles.
Si un artículo está archivado, el sync automático no debe publicarlo de nuevo.
raw_detail_payload y raw_list_item deben preservarse para que no se pierdan datos de la salida original.
Si Export Token o Webhook Secret se rota, el nuevo valor debe reemplazarse en .env y el backend debe reiniciarse o redesplegarse.
PASO 31

Resumen completo del flujo API

Después de completar estos pasos, tu sitio web personalizado puede recibir, almacenar, reflejar y publicar de forma segura artículos generados en OxiRanker.

Registrarse en OxiRanker
Añadir un dominio
Verificar la propiedad del dominio
Elegir idiomas y añadir Blog URL
Añadir palabras clave para cada idioma
Completar Article Profile
Configurar Telegram si es necesario
Configurar el motor automático de artículos
Crear el primer artículo desde Generate
Revisar Preview y SEO Report
Obtener Export Token desde API Output
Obtener endpoints listos de Export API
Obtener Webhook Secret
Crear el endpoint fijo /api/oxiranker/webhook en el backend de destino
Guardar Token y Secret en el .env del sitio web de destino
Crear la tabla site_articles y unique constraint
Implementar la obtención del artículo completo desde Export JSON
Implementar Image Mirroring
Implementar UPSERT
Implementar sync diario
Renderizar artículo, SEO, OG y Schema desde la base de datos de destino
Ejecutar pruebas finales de Export API y Webhook