DOCUMENTATION DÉVELOPPEUR OXIRANKER

Documentation OxiRanker

Un centre de documentation clair pour connecter votre site web aux services OxiRanker. Commencez par l’intégration WordPress ou l’intégration API, puis suivez le guide étape par étape.

Documentation officielleVersion V1 complètePour les propriétaires de sites, les développeurs et les équipes d’intégration
Menu de documentation
Choisissez un guide dans le menu. Cette page est le centre principal de documentation d’OxiRanker.
Service API

Service API

Utilisez Export API et Webhook pour les sites personnalisés, les plateformes CMS personnalisées, Next.js, Laravel, Node.js, PHP ou les backends Python.

Domaine
Vérifié
Contenu
Prêt
API
Webhook
Avis important sur le stockage

OxiRanker n’est pas un hébergement permanent pour le site de destination

OxiRanker ne fournit aucune garantie d’hébergement permanent pour le contenu, les images ou les fichiers exportés du site de destination. OxiRanker n’est pas un service d’hébergement de contenu utilisateur, et le contenu généré, les images de couverture, les images Open Graph ou les fichiers de sortie peuvent ne pas rester sur l’infrastructure OxiRanker plus de 90 jours.

Par conséquent, après avoir reçu la sortie, le site de destination doit stocker l’article final, les images, les données SEO, les données Open Graph et les données Schema dans sa propre base de données, son propre serveur, son propre stockage ou CDN, et ne doit pas dépendre des URL OxiRanker pour l’affichage public.

Vidéo de formation

Vidéo de formation à la connexion

Utilisez cette vidéo comme guide visuel avant de connecter WordPress ou d’implémenter l’intégration API.

Sortie API

Implémentez le flux de sortie étape par étape

Stockez d’abord l’Export Token et le Webhook Secret dans votre backend, puis complétez le Webhook receiver, la base de données, la logique UPSERT, l’Image Mirroring et la sync quotidienne.

ÉTAPE 01

S’inscrire et créer un domaine dans OxiRanker

Tout d’abord, inscrivez-vous sur OxiRanker, connectez-vous à votre compte et cliquez sur Add Domain depuis la section Domains.

Format correct du domaine

Dans le champ Domain name, saisissez uniquement le domaine racine. L’adresse ne doit pas inclure http, https ou www.

example.com

Formats incorrects

N’entrez pas ces formats dans le champ du domaine.

https://example.com
http://example.com
www.example.com
Après la création du domaine
Si le format du domaine est valide, OxiRanker crée le domaine et vous dirige vers l’étape de vérification de la propriété du domaine.
ÉTAPE 02

Vérifier la propriété du domaine

Dans cette étape, vous devez prouver que le domaine vous appartient ou que vous avez accès à sa gestion.

DNS TXT

Recommandé

Ajoutez un TXT record au DNS du domaine. Pour Cloudflare, c’est généralement la méthode la plus simple.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

Placez un fichier texte dans le chemin .well-known de votre site web afin qu’OxiRanker puisse le vérifier.

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

File Content:
220daaa37****42730f27

Meta Tag

Placez une balise meta dans la section head de la page d’accueil de votre site web.

<meta name="yourdomain-verification" content="190208d****d275b65" />
Remarque DNS importante
Après avoir ajouté le DNS record, surtout dans Cloudflare, vous devrez peut-être attendre 2 à 3 minutes. Cliquez ensuite sur Confirm sur la même page OxiRanker.
Durée de validité du token de vérification
Les valeurs de vérification de domaine ont une limite de temps. Actuellement, les valeurs de vérification sont généralement valides pendant 1440 minutes.
ÉTAPE 03

Recevoir des tokens gratuits après la vérification du premier domaine

Après la vérification du premier domaine, OxiRanker ajoute 1000 tokens gratuits à votre compte pour les tests initiaux.

Uniquement pour le premier domaine
Ce cadeau est ajouté uniquement pour le premier domaine vérifié et n’est pas ajouté de nouveau pour les domaines suivants.
ÉTAPE 04

Configurer les langues et le Blog URL

Dans la section Blog URLs, définissez si votre site web est monolingue ou multilingue et quelles langues doivent être utilisées pour les articles générés.

Choisir les langues

OxiRanker prend en charge 24 langues. Vous pouvez choisir une ou plusieurs langues selon la structure de votre site web.

Saisir le Blog URL

Pour chaque langue, saisissez le Blog URL correspondant à cette langue. Si votre site web est multilingue, saisissez chaque chemin de langue séparément.

https://example.com/fa/blog
https://example.com/en/blog
Structure d’URL
Si votre site web a une structure différente, saisissez le véritable Blog URL de votre site. Le point important est que l’URL finale doit être le chemin où les articles doivent être publiés.
ÉTAPE 05

Ajouter des mots-clés pour chaque langue

Dans l’onglet Keywords, ajoutez au moins 3 et jusqu’à 50 mots-clés pour chaque langue.

Règle principale

Les mots-clés de chaque langue doivent être saisis dans la même langue. Si la langue de l’article est l’anglais, les mots-clés doivent également être en anglais.

Comment ajouter des mots-clés

Après avoir saisi chaque mot-clé, appuyez sur Enter pour l’ajouter. Après avoir complété tous les mots-clés, cliquez sur Save.

Erreur courante
Si vous saisissez des mots-clés turcs pour la langue anglaise, des expressions turques peuvent apparaître dans le contenu anglais. La langue des mots-clés doit correspondre à la langue de l’article.
ÉTAPE 06

Compléter l’Article Profile du domaine

L’Article Profile indique à OxiRanker ce qu’est votre marque, pour qui elle écrit, quel ton elle doit utiliser et quels sujets elle doit éviter.

Secteur

Définit le marché ou la catégorie d’activité. Exemple : conseil immobilier et achat-vente de biens immobiliers à Dubaï.

Description de l’activité

Explique ce que vend l’entreprise, qui elle sert, comment elle fonctionne et ce qui la rend différente.

Audience

Définit à qui le contenu doit s’adresser. Chaque audience peut être enregistrée séparément.

Ton

Contrôle le style et la voix du contenu généré, comme formel, éducatif, simple, technique ou orienté vente.

Objectif du contenu

Définit ce que les articles doivent accomplir, comme attirer des clients, augmenter le trafic ou éduquer les utilisateurs.

Sujets interdits

Tout ce dont la marque ne doit pas parler ou à quoi elle ne doit pas être associée est saisi ici.

CTA optionnel

CTA signifie l’action que vous voulez que le lecteur effectue après avoir lu l’article. Si vous n’êtes pas sûr du texte approprié, il est préférable de laisser ce champ vide.

Profil multilingue
Si votre site web est multilingue, vous n’avez besoin de compléter le profil que dans une seule langue. OxiRanker peut préparer les versions requises pour les autres langues à partir du même profil.
ÉTAPE 07

Liens internes et externes

OxiRanker peut appliquer intelligemment des liens internes et externes dans les articles.

Liens internes

Cette option est activée par défaut et il est préférable de la garder activée. Les liens internes aident les moteurs de recherche à mieux comprendre la structure de contenu de votre site web.

Dans le premier article, les liens internes ne sont généralement pas créés car il n’y a pas encore d’article précédent. À partir du deuxième article, le système peut effectuer le maillage interne.

Liens externes

Les liens externes sont ajoutés vers des sources officielles et fiables et sont marqués comme nofollow afin que l’autorité SEO ne soit pas transférée involontairement.

ÉTAPE 08

Vérifier la qualité du profil avec l’AI

Après avoir complété l’Article Profile, OxiRanker examine la qualité du profil.

85+
Score minimum requis pour enregistrer
95
Excellent score généralement atteint après améliorations
AI
Suggestions professionnelles pour chaque champ
Si le score est inférieur à 85
Le système ne permet pas l’enregistrement final et affiche des explications et des suggestions d’amélioration pour chaque champ problématique. Vous pouvez appliquer les suggestions et enregistrer à nouveau.
ÉTAPE 09

Connecter Telegram pour les résumés d’articles

Après avoir enregistré le profil avec succès, vous pouvez activer l’envoi de résumés d’articles vers un canal Telegram.

1

Ajouter le bot

Ajoutez @Oxiranker_Bot à votre canal Telegram et faites-en un Admin.

2

Saisir le nom d’utilisateur du canal

Saisissez le nom du canal avec @. Exemple : @my_channel

3

Activer l’envoi du résumé

Après avoir enregistré un canal valide, activez l’envoi des résumés d’articles.

Avantage Telegram
Si l’option est activée, après la génération du contenu, le résumé de l’article est envoyé au canal Telegram avec l’image et le lien. Cela peut renforcer les chemins de trafic vers votre site web.
ÉTAPE 10

Configurer le moteur automatique d’articles

Dans l’onglet Automatic article generation, vous pouvez gérer le moteur de génération automatique d’articles.

Sélection intelligente du sujet

Le système utilise les mots-clés et l’Article Profile pour sélectionner des sujets adaptés, uniques et publiables.

Génération adaptée à la marque

Le ton, l’audience, l’objectif du contenu et les restrictions de la marque sont respectés pendant la génération de l’article.

Publication planifiée

La capacité mensuelle est répartie tout au long du mois afin que la publication de contenu paraisse plus naturelle et régulière.

Capacité mensuelle

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

30 articles signifie presque un article par jour. 15 articles signifie presque un jour sur deux. 10 articles signifie environ tous les deux à trois jours. 7 articles signifie environ tous les trois à quatre jours.

Longueur de l’article et image

La longueur de l’article peut être Short, Standard ou Long. Standard est un choix équilibré pour la plupart des sites web.

Les images d’articles peuvent être activées ou désactivées, et le thème de l’image peut être Light ou Dark.

Remarque pour les sites API
Le moteur automatique génère les articles dans OxiRanker. Pour déplacer chaque article vers votre site personnalisé après génération, vous devez implémenter Export API, Webhook, le stockage en base de données et Image Mirroring.
ÉTAPE 11

Créer le premier article depuis Generate

Après avoir préparé le domaine, ouvrez le menu Generate, choisissez le domaine et cliquez sur New Content ou New Article.

1

Langue

Une seule langue est sélectionnée pour chaque demande de contenu.

2

Sujet

Écrivez un sujet clair. Source URL est optionnel.

3

Image

Activez ou désactivez l’image et choisissez le thème Light ou Dark.

4

Paiement et génération

Le coût est déduit du wallet et la génération de l’article démarre.

Temps de génération
La génération de l’article et de l’image prend généralement environ 2 à 3 minutes. Une fois terminée, l’article est visible dans OxiRanker.
Source URL
Si Source URL est saisi, OxiRanker l’utilise pour mieux comprendre le sujet, mais ne génère pas de contenu copié. Le système est conçu pour créer du contenu unique.
ÉTAPE 12

Examiner Preview, SEO Report et JSON Output

Après la génération de l’article, plusieurs sorties importantes sont disponibles pour le contrôle qualité et la préparation de la publication.

Preview

Affiche le texte de l’article et l’image générée. Cette section est utile pour examiner la qualité finale avant publication.

SEO Report

Affiche un rapport SEO compréhensible incluant SEO Score, title, meta description, canonical, keywords, schema, headings, links, images et OpenGraph.

JSON Output

Affiche la sortie complète de l’article pour les utilisateurs techniques, incluant slug, html_body, schema, URL des images, tags, category, temps de lecture et timestamps de création/mise à jour.

Signification des sorties
Ces sorties montrent que l’article n’est pas seulement du texte brut. C’est un contenu structuré, prêt pour le SEO, préparé pour WordPress, CMS, API et les systèmes de publication.
ÉTAPE 13

Que font exactement Export API et Webhook ?

Après la génération d’un article dans OxiRanker, le site de destination peut le lire avec Export API ou être notifié via Webhook lorsque l’article est prêt.

Méthode 1 : Export API

Le site de destination peut lire les articles depuis OxiRanker avec l’Export Token chaque fois que nécessaire. Cette méthode est utilisée pour la sync manuelle, la sync quotidienne, les pages de liste d’articles et la récupération du JSON complet de l’article.

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éthode 2 : Webhook

Lorsqu’un nouvel article est créé ou qu’un article est mis à jour, OxiRanker notifie l’endpoint du site de destination. Webhook n’envoie pas l’article complet. Il envoie seulement l’identifiant de l’article afin que le site de destination puisse récupérer l’article complet depuis l’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"
}
Remarque très importante
Webhook est seulement un message léger. Le site de destination ne doit pas s’attendre à recevoir l’article complet dans le Webhook. Après avoir reçu domain_id et article_id, il doit récupérer l’article complet depuis l’Export JSON API et le stocker dans sa propre base de données.
ÉTAPE 14

Responsabilité du stockage de contenu et Image Mirroring

Le site de destination doit stocker l’article final et les images sur sa propre infrastructure.

Ce que le site de destination doit stocker

Texte complet de l’article dans la base de données du site de destination
Image principale de l’article sur le serveur, le stockage ou le CDN de destination
Image Open Graph sur le serveur, le stockage ou le CDN de destination
Données SEO, OpenGraph et Article Schema
raw_detail_payload et raw_list_item pour préserver la sortie originale

Exemple correct de chemin d’image

Utiliser directement et durablement les URL d’images OxiRanker n’est pas correct. L’image doit être téléchargée et les URL de l’article doivent être remplacées par les URL internes du site de destination.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker n’est pas un hébergement permanent pour le site de destination
OxiRanker n’est pas responsable du stockage permanent du contenu de sortie et des images du site de destination. OxiRanker n’est pas un service d’hébergement de contenu utilisateur, et le contenu généré, les images de couverture, les images Open Graph ou les fichiers de sortie peuvent ne pas rester sur l’infrastructure OxiRanker plus de 90 jours. Par conséquent, le site de destination doit stocker l’article final, les images, les données SEO, OpenGraph et Schema dans sa propre base de données, son propre serveur, son propre stockage ou CDN.
ÉTAPE 15

Obtenir l’Export Token et les endpoints prêts

L’Export Token et les endpoints prêts à l’emploi sont disponibles depuis la page de l’article, l’onglet Outputs et la section API Output.

Où obtenir l’Export Token

Créez le premier article dans OxiRanker.
Ouvrez cette page d’article.
Ouvrez l’onglet Outputs.
Ouvrez la section API Output.
Créez un token depuis Token management.
Le token complet n’est affiché qu’une seule fois.
Stockez le token dans le backend du site de destination ou dans .env.
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}

Endpoints prêts à l’emploi

Sur la même page d’article, dans la section API Output, Ready-to-use endpoints affiche les URL préparées.

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
Rotation du Token
Si l’Export Token est renouvelé dans le panneau OxiRanker, le token précédent expire. La nouvelle valeur doit immédiatement remplacer l’ancienne valeur dans le .env ou l’environnement backend du site de destination, et le backend de destination doit être redémarré ou redéployé.
ÉTAPE 16

Créer Webhook Secret et l’endpoint de destination

Webhook Secret est également créé depuis la page de l’article, l’onglet Outputs et la section API Output.

Où Webhook Secret est-il créé ?

Ouvrez la page de l’article.
Ouvrez l’onglet Outputs.
Ouvrez la section API Output.
Créez un secret dans la section Webhook Secret.
Le secret complet n’est affiché qu’une seule fois.
Stockez le secret dans le .env du site de destination.

Endpoint fixe de destination

OxiRanker accepte uniquement le chemin fixe suivant pour le Webhook de destination. Le site de destination doit créer cet endpoint dans son backend.

POST /api/oxiranker/webhook

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

Cet endpoint doit uniquement accepter POST, préserver le raw body, vérifier timestamp et signature, et retourner une réponse réussie pour webhook.test.

Événements actifs
Dans la version actuelle, les événements sont fixes : article.created et article.updated. Pour tester la connexion, la valeur d’événement webhook.test est envoyée.
ÉTAPE 17

Valeurs .env requises pour le site de destination

OxiRanker n’a pas accès aux fichiers du site de destination ni à son .env. Le développeur du site de destination doit stocker ces valeurs dans son propre 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

Valeurs de sécurité

OXIRANKER_EXPORT_API_TOKEN
Le token Export API doit être stocké uniquement dans le backend.
OXIRANKER_WEBHOOK_SECRET
Le Webhook Secret doit être stocké uniquement dans le backend.
OXIRANKER_WEBHOOK_MAX_SKEW_MS
La différence de temps maximale autorisée pour Webhook. 300000 signifie 5 minutes.

Valeurs de stockage

SITE_ARTICLE_IMAGE_UPLOAD_DIR
Le chemin physique pour stocker les images miroir sur le serveur de destination.
SITE_ARTICLE_IMAGE_MAX_BYTES
La taille maximale autorisée de l’image pour le téléchargement et le stockage.
PUBLIC_BASE_URL
L’URL publique du site de destination utilisée pour créer les URL finales des images et des articles.
Sécurité Frontend
Export Token et Webhook Secret ne doivent pas être placés dans le code frontend, le JavaScript visible par l’utilisateur, localStorage ou l’objet window.
ÉTAPE 18

Export API : List, JSON, HTML et ZIP

Le site de destination peut utiliser Export API pour recevoir la liste des articles ou la sortie complète de chaque article.

Obtenir la liste des articles pour un domaine

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

limit définit le nombre d’articles et est actuellement limité entre 1 et 50. offset est utilisé pour la pagination. lang est optionnel et sert à récupérer les articles pour une langue spécifique comme fa, en ou tr.

Obtenir l’article complet en JSON

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

C’est la sortie la plus importante pour le site de destination. Le site de destination doit stocker le JSON complet de l’article dans sa propre base de données et lire depuis sa propre base de données pour l’affichage public.

Obtenir le HTML prêt

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 convient aux sites qui veulent une sortie prête, mais l’approche la plus professionnelle consiste à stocker JSON et à le rendre avec le propre template du site de destination.

Obtenir la sortie ZIP

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

La sortie ZIP inclut article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt et README.txt.

ÉTAPE 19

Signature Webhook et réception sécurisée du Webhook

Le site de destination doit vérifier la signature Webhook avec le raw body original.

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

Formule de signature

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

Si le site de destination parse d’abord JSON puis le stringify à nouveau, l’ordre ou les espaces peuvent changer et la signature peut devenir invalide. Le raw body original doit être utilisé pour la vérification de la signature.

Comparaison timing-safe
Ne comparez pas les signatures avec expected === received. Dans Node.js, il est préférable d’utiliser crypto.timingSafeEqual.
ÉTAPE 20

Exemple simple de code Node.js pour recevoir Webhook

Cet exemple Express gère raw body, timestamp, signature, webhook.test et les événements d’article.

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

Base de données recommandée pour le site de destination

Le site de destination doit stocker les articles dans sa propre base de données et empêcher les doublons avec une 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);
Remarque PostgreSQL
Dans PostgreSQL, les guillemets simples sont utilisés pour le texte persan ou Unicode. Quelque chose comme N'...' n’est pas nécessaire et ne doit pas être utilisé.
ÉTAPE 22

Empêcher les doublons avec UPSERT

L’article ne doit pas être inséré à nouveau pour chaque Webhook. S’il existe déjà, il doit être mis à jour.

Clé principale pour empêcher les doublons
La contrainte la plus importante pour empêcher les doublons est : 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();
ÉTAPE 23

Récupérer l’article complet et synchroniser après Webhook

Lorsqu’un Webhook arrive, le site de destination doit récupérer l’article complet depuis Export API, mettre les images en miroir et faire un UPSERT de l’article.

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

Image Mirroring et règles de stockage des images

Le site de destination doit télécharger les images, les stocker sur sa propre infrastructure et remplacer les URL de l’article.

Règles d’Image Mirroring

Seules les URL HTTPS doivent être acceptées.
Le fichier doit réellement être une image.
content-type doit commencer par image/.
Les fichiers vides ne doivent pas être stockés.
La taille du fichier ne doit pas dépasser la limite.
Si cover et og sont identiques, téléchargez une seule fois.
Après le mirroring, remplacez les URL dans html_body et article_schema.

Chemin de stockage recommandé

/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
ÉTAPE 25

Sync quotidienne pour la fiabilité

Webhook seul ne suffit pas. Le site de destination doit également avoir une sync quotidienne pour récupérer les articles manqués.

Pourquoi la sync quotidienne est-elle nécessaire ?

Le site de destination peut être temporairement indisponible.
Webhook peut expirer.
Des problèmes de déploiement ou de réseau peuvent survenir.
Un article peut être généré mais son Webhook peut être manqué.

Algorithme de sync quotidienne

Démarrez offset à 0.
Utilisez limit 20 ou 50.
Récupérez la liste des articles depuis Export List API.
Pour chaque élément, récupérez l’Export JSON de cet article.
Mettez les images en miroir.
Stockez l’article avec UPSERT.
Augmentez offset et continuez jusqu’à ce que total soit terminé.
ÉTAPE 26

Afficher les articles sur le site de destination

Après avoir stocké les articles dans site_articles, le site de destination ne doit pas se connecter à OxiRanker pour chaque affichage public. Il doit lire depuis sa propre base de données.

Page de liste du 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;

Page de détail de l’article

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 d’article recommandée
Pour les sites multilingues, vous pouvez utiliser le modèle /{lang}/blog/{slug}. Exemple : /fa/blog/smart-internal-links
ÉTAPE 27

SEO, Schema, RTL et rendu HTML

Le site de destination doit lire les données SEO depuis sa propre base de données et les afficher sur la page de l’article.

Champs SEO

title pour le titre de la page
meta_description pour la meta description
effective_canonical_url pour canonical
og_title et og_description pour Open Graph
og_image_url ou cover_image_url pour l’image de prévisualisation sociale
article_schema pour JSON-LD

Rendu HTML

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

Si le site de destination a une politique de sécurité stricte, il peut nettoyer le HTML avant l’enregistrement ou avant l’affichage.

RTL et LTR

Chaque article possède les champs lang, dir et is_rtl. Si dir est rtl, la page de l’article doit être rendue de droite à gauche.

<article dir="rtl">
  ...
</article>
ÉTAPE 28

Erreurs courantes et leurs significations

Les erreurs du site de destination doivent être en anglais, claires et compréhensibles afin que le dépannage puisse être effectué rapidement.

Erreurs Export API

Token d’export du domaine manquant.
Le token Export API n’a pas été envoyé. Authorization: Bearer YOUR_EXPORT_TOKEN doit être envoyé.
Token d’export du domaine invalide.
Le token est incorrect, a été renouvelé, n’est pas lié à ce domaine ou le domainId est incorrect.
Le token n’a pas le scope requis.
Le token n’a pas le scope requis. Les nouveaux tokens doivent avoir articles:read et exports:read.
lang invalide.
La valeur lang dans le query parameter a été envoyée incorrectement.

Erreurs Webhook

L’URL Webhook doit utiliser HTTPS.
L’URL Webhook doit commencer par https://.
Signature Webhook invalide.
Webhook Secret est incorrect, le secret a été renouvelé, le raw body a changé ou la signature a été calculée incorrectement.
Le timestamp Webhook a expiré ou est invalide.
Le timestamp est ancien, dans le futur ou invalide. Vérifiez l’horloge du serveur et NTP.
Échec de l’envoi du test Webhook.
Le site de destination n’a pas retourné un statut réussi de 200 à 299, a expiré ou n’a pas géré webhook.test correctement.
ÉTAPE 29

Checklist finale de test API et Webhook

Après l’implémentation, ces tests vérifient la connexion de bout en bout.

Test Export Token et 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

Test Webhook

Si vous appelez le Webhook de destination avec Postman sans signature, vous devez recevoir une erreur de sécurité. Cela signifie que l’endpoint est actif et vérifie la sécurité.

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

Cliquez ensuite sur Test Webhook dans OxiRanker. Si le receiver prend en charge webhook.test, response_status doit être 200.

Après rotation
Si Webhook Secret ou Export Token est renouvelé, copiez immédiatement la nouvelle valeur, remplacez-la dans le .env du site de destination et redémarrez ou redéployez le backend de destination.
ÉTAPE 30

Règles importantes pour une implémentation correcte

Ces règles doivent être suivies dans l’intégration API afin que le système reste sécurisé, stable et maintenable.

Export Token doit être stocké uniquement dans le backend.
Webhook Secret doit être stocké uniquement dans le backend.
L’endpoint Webhook de destination doit être exactement /api/oxiranker/webhook.
L’URL Webhook doit utiliser HTTPS.
La signature Webhook et le timestamp doivent toujours être vérifiés.
Les articles doivent être stockés avec UPSERT, pas avec un simple insert.
Une unique constraint est requise pour empêcher les doublons.
Les images doivent être téléchargées depuis OxiRanker et stockées sur le site de destination.
Le site de destination ne doit pas dépendre durablement des URL d’images OxiRanker.
Les URL d’images doivent être remplacées dans html_body et article_schema.
Si Webhook échoue, la sync quotidienne doit récupérer l’article manqué.
Les erreurs affichées aux utilisateurs ou dans les logs doivent être en anglais et compréhensibles.
Si un article est archivé, la sync automatique ne doit pas le republier.
raw_detail_payload et raw_list_item doivent être préservés afin qu’aucune donnée de sortie originale ne soit perdue.
Si Export Token ou Webhook Secret est renouvelé, la nouvelle valeur doit être remplacée dans .env et le backend doit être redémarré ou redéployé.
ÉTAPE 31

Résumé du flux API complet

Après avoir terminé ces étapes, votre site personnalisé peut recevoir, stocker, mettre en miroir et publier en toute sécurité les articles générés dans OxiRanker.

S’inscrire sur OxiRanker
Ajouter un domaine
Vérifier la propriété du domaine
Choisir les langues et ajouter le Blog URL
Ajouter des mots-clés pour chaque langue
Compléter Article Profile
Configurer Telegram si nécessaire
Configurer le moteur automatique d’articles
Créer le premier article depuis Generate
Examiner Preview et SEO Report
Obtenir Export Token depuis API Output
Obtenir les endpoints Export API prêts
Obtenir Webhook Secret
Créer l’endpoint fixe /api/oxiranker/webhook dans le backend de destination
Stocker Token et Secret dans le .env du site de destination
Créer la table site_articles et une unique constraint
Implémenter la récupération complète de l’article depuis Export JSON
Implémenter Image Mirroring
Implémenter UPSERT
Implémenter la sync quotidienne
Afficher l’article, SEO, OG et Schema depuis la base de données de destination
Exécuter les tests finaux Export API et Webhook