Aller au contenu
Linksgarden Documentation API

Documentation API

Intégrez Linksgarden dans vos outils (n8n, Make, scripts, ERP…).
Sommaire
Documentation complète Le contrat OpenAPI 3.1 est la référence canonique ; le guide Markdown fournit les exemples d’utilisation.
OpenAPI (.yaml) Télécharger (.md)

Prise en main

L'API Linksgarden est une API HTTP/JSON qui réplique les actions principales de la plateforme. Vous pouvez :

  • Lister les catégories et les sites avec tous les filtres (TF, CF, DR, DA, prix, langue, exclusivité, recherche)
  • Commander un ou plusieurs liens en une seule requête (spon, kw, home, sitewide, bulk)
  • Suivre vos commandes en cours et terminées
  • Consulter votre solde de crédits

Base URL : https://app.linksgarden.com/api/v1

Le préfixe /api/v1 est canonique ; l'ancien préfixe /api reste un alias de compatibilité. GET /api/v1/ping vérifie uniquement la liveness HTTP/PHP : il ne teste ni la base ni les dépendances et ne constitue pas une readiness.

Étape 1 : Connectez-vous pour générer une clé API dans vos informations de compte. Vous pouvez créer jusqu’à cinq clés utilisables, choisir librement leurs autorisations, puis les modifier, les remplacer ou les supprimer sans intervention de Linksgarden.

Étape 2 : Testez avec curl :

curl -H "X-API-Key: lg_xxxxxxxx_yyyyyyyy" \
  https://app.linksgarden.com/api/v1/me

Authentification

Toutes les requêtes (sauf /api/v1/ping) doivent inclure le header :

X-API-Key: lg_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

Erreurs possibles :

HTTPcodeQuand
401missing_api_keyHeader X-API-Key absent
401invalid_api_keyClé inconnue ou format invalide
401key_expiredClé expirée
403key_revokedClé révoquée
403user_inactiveCompte désactivé
403insufficient_scopeScope requis absent

Chaque clé est liée au user_id immuable du compte et porte des scopes. Le contrat OpenAPI indique le scope requis par opération ; l’email et l’état du compte sont relus depuis ce user_id à chaque requête.

EspaceScopeAutorisation
Généralaccount:readConsulter le compte et le solde annonceur
Annonceurcatalog:readConsulter le catalogue et les tarifs
orders:readConsulter les commandes annonceur
orders:writePasser des commandes et dépenser les crédits
Éditeurpublisher_sites:readConsulter les sites éditeur
publisher_missions:readConsulter les missions et leur messagerie
publisher_earnings:readConsulter les soldes, l’historique des gains et l’affiliation
publisher_invoices:readConsulter les factures éditeur
publisher_profile:readConsulter le profil et les coordonnées de paiement
publisher_sites:writeGérer les sites et leur validation de propriété
publisher_missions:writeGérer les missions et leur messagerie
publisher_profile:writeModifier le profil et les coordonnées de paiement
payouts:writeDemander un paiement éditeur

Les anciens scopes globaux publisher:read, publisher:write et * restent reconnus pour les clés existantes, mais ne sont pas proposés par la gestion en libre-service.

La page Mes informations > Clés API permet de choisir un ou plusieurs scopes à la création et de les modifier ensuite. La lecture seule est présélectionnée ; l’utilisateur peut choisir explicitement l’accès complet ou une combinaison personnalisée. Toutes les autorisations sont disponibles en libre-service : aucun « droit étendu » ne doit être demandé. Une modification s’applique dès l’appel suivant. « Remplacer la clé » génère une nouvelle valeur avec le même libellé et les mêmes autorisations, puis révoque immédiatement l’ancienne ; la nouvelle valeur n’est affichée qu’une seule fois. La suppression et le remplacement conservent l’ancienne clé et son historique d’activité, sans exposer les secrets ni les contenus sensibles.

Rate limit

Le quota est fixé à 60 requêtes par fenêtre fixe de 60 secondes et par compte. Toutes les clés d’un même utilisateur partagent ce compteur : la 60e requête est acceptée et la 61e reçoit une erreur 429.

Exemple de réponse lorsque 42 secondes restent avant la prochaine fenêtre :

HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1786726500

{"error":{"code":"rate_limited","message":"Limite dépassée : 60 requêtes/minute pour ce compte."}}

Après authentification réussie, les réponses qui traversent le rate limiter contiennent X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset. Ils décrivent respectivement le quota total du compte, le nombre de requêtes encore disponibles et le timestamp Unix de fin de la fenêtre. Sur un 429 rate_limited, Retry-After est dynamique et compris entre 1 et 60 secondes. Une erreur d’authentification produite avant le limiteur peut ne pas porter ces headers.

Endpoints

Compte

GET /api/v1/me

Retourne l'email, l'identifiant utilisateur et le solde de crédits.

{
  "data": {
    "email": "vous@example.com",
    "user_id": 8563,
    "role": "client",
    "id_agency": 0,
    "revendeur": false,
    "credits": {
      "solde_cents": 7150,
      "solde_eur": 71.50,
      "exclu_cents": 0,
      "exclu_eur": 0
    },
    "rate_limit": {
      "limit": 60,
      "window_seconds": 60,
      "scope": "account"
    },
    "scopes": ["account:read", "catalog:read", "orders:read"]
  },
  "meta": { "request_id": "..." }
}
GET /api/v1/credits

Solde uniquement (plus léger que /me).

{
  "data": {
    "solde_cents": 7150,
    "solde_eur": 71.50,
    "exclu_cents": 0,
    "exclu_eur": 0,
    "total_cents": 7150,
    "total_eur": 71.50
  }
}

Catalogue

GET /api/v1/categories?lang=fr_FR

Liste toutes les catégories (groupes). Langues supportées : fr_FR, en_US, en_GB, de_DE, es_ES, it_IT, pt_PT.

{
  "data": [
    {
      "id_groupe_unique": "abc123",
      "id_groupe": 12,
      "nom": "Mode & beauté",
      "description": "...",
      "nb_sites": 234
    },
    ...
  ],
  "meta": { "total": 69, "lang": "fr_FR" }
}

Le champ data[].id_groupe est l'identifiant à envoyer comme thematique d'un site éditeur (création ou modification) — et non id_groupe_unique.

GET /api/v1/sites?filtres

Liste paginée des sites. Tous les filtres sont optionnels.

Filtres acceptés en query string

ParamètreDescriptionValeurs
categoryID de catégorieID retourné par /categories, ou all, favoris, media
langLangue ciblefr_FR (défaut), en_US, en_GB, de_DE, es_ES, it_IT, pt_PT
tf_minMajestic Trust Flow ≥0, 5, 10, 15, 20
cf_minMajestic Citation Flow ≥0, 5, 10, 15, 20
rd_minReferring Domains ≥0, 10, 20, …, 100
dr_minAhrefs Domain Rating ≥0, 5, 10, 20, 30
da_minMoz Domain Authority ≥0, 5, 10, 20, 30
price_maxPrix max en € (filtre prix_eur ≤ price_max)0 (tous) ou tout entier > 0, p.ex. 39, 45
exclusiveSites exclusifs uniquement0, 1
eclairSites éclair uniquement0, 1
source_geoPrésence comme source sur ChatGPT dans l'échantillon Semnaut (lang=fr_FR uniquement)1 (observé), 0 (non observé), omis (tous)
searchRecherche sur la valeur de domaine visibletexte libre
target_siteDomaine ou sous-domaine cible nu, sans protocole, port ni cheminexemple.fr, blog.exemple.fr
include_usedConserver les sites déjà utilisés pour cette cible ; nécessite target_sitetrue / 1, false / 0 (défaut)
pageNuméro de pagedéfaut 1
per_pageItems par pagedéfaut 50, max 200

Avec target_site=blog.exemple.fr, toutes les offres des domaines éditeurs déjà utilisés pour cet hôte cible dans l'historique du compte authentifié sont retirées avant le calcul du total et la pagination. Les commandes en cours qui figurent dans cet historique comptent également ; le panier n'est pas concerné. L'ordre relatif des résultats est conservé. include_used=true permet de les inclure en conservant les autres filtres. Sans cible, le catalogue reste inchangé.

La cible est normalisée en minuscules et en ASCII pour les domaines internationaux (IDN), sans point final ni préfixe www.. www.exemple.fr et exemple.fr sont équivalents, tandis que blog.exemple.fr reste distinct. Le domaine cible lui-même n'est pas exclu par ce paramètre. Une cible vide, nulle, invalide ou fournie comme URL renvoie 400 invalid_field. include_used accepte strictement true, false, 1 ou 0 et sa présence sans target_site renvoie également 400 invalid_field, même avec false. Lorsqu'une cible est fournie, meta.filters contient target_site normalisé et include_used booléen.

GET /api/v1/sites?target_site=blog.exemple.fr&include_used=false&per_page=20

Exemple

curl -H "X-API-Key: $LG_KEY" \
  "https://app.linksgarden.com/api/v1/sites?lang=fr_FR&tf_min=10&price_max=50&source_geo=1&per_page=20"
{
  "data": [
    {
      "id_site_unique": "2ba80e0c6acdf",
      "root_ndd": "mo••••••.com",
      "domain_masked": true,
      "prix_eur": 30.00,
      "tf": 15, "cf": 12, "rd": 42, "dr": 18, "da": 22,
      "exclusif": false,
      "source_geo": true,
      "categorie": "Tech",
      "localisation": "fr_FR",
      "actif": true,
      "indexe": true
    }
  ],
  "meta": {
    "page": 1, "per_page": 20, "total": 312, "total_pages": 16,
    "filters": { "lang": "fr_FR", "tf_min": 10, "price_max": 50, "source_geo": 1 }
  }
}

La liste et le détail appliquent la même éligibilité. Les domaines réels sont affichés en clair, sauf lorsque l'éditeur a choisi de masquer son URL avant achat : le domaine est alors masqué (domain_masked: true) — sauf pour les comptes possédant des crédits ou ayant déjà effectué une recharge, qui voient tout en clair. Sans masque disponible, root_ndd vaut null. La recherche porte uniquement sur la valeur visible et les sites du compte appelant sont exclus.

Pour lang=fr_FR, source_geo: true signifie que semnaut.fr a observé le site au moins une fois comme source sur ChatGPT le mois dernier, parmi un échantillon d'un million de résultats issus de prompts en français. false signifie seulement que le site n'a pas été observé dans cet échantillon, pas qu'il n'est jamais cité. Conformément au catalogue web, le filtre source_geo est disponible uniquement avec lang=fr_FR (400 invalid_field sinon) ; le champ est omis des réponses des autres localisations.

GET /api/v1/sites/{id_site_unique}

Détail d'un site. Renvoie 404 si l'ID n'existe pas.

Commandes — Lecture

GET /api/v1/orders?status=filter

Liste paginée. status peut valoir :

  • all (défaut) — toutes les commandes payées
  • in_progress — commandes encore en cours (mail de fin non reçu)
  • completed — commandes terminées

Paramètres page et per_page (défaut 50, max 200).

GET /api/v1/orders/{num_commande_client}

Détail d'une commande payée appartenant au compte, avec la liste complète des liens commandés dans items. num_commande_client doit être un entier décimal strictement positif ; toute autre forme, par exemple 12abc ou 0, renvoie 404 order_not_found.

Commandes — Création

POST /api/v1/orders

Crée et paie immédiatement une commande. Échoue (422) si vos crédits sont insuffisants. Peut contenir plusieurs types de liens dans le même appel.

Body JSON

{
  "items": [
    {
      "type": "spon",
      "id_site_unique": "abc12345",
      "url_cible": "https://exemple.com/page",
      "ancre": "ma super ancre",
      "nb_mots": 500,
      "fournir": 0,
      "urgent": 0,
      "commentaire": ""
    }
  ],
  "client_request_id": "uuid-pour-idempotence-obligatoire"
}

Types d'items supportés

typeDescriptionChamps requisChamps optionnels
spon Article sponsorisé classique id_site_unique, url_cible, ancre nb_mots, urgent, fournir, date_souhaite, commentaire
kw Lien avec keyword cible id_site_unique, id_url_unique, url_cible, ancre comme spon
home Lien en homepage id_site_unique, url_cible, ancre, duration (1, 2, 3, 6, 12 ou 24 mois) renewal, commentaire
sitewide Lien sitewide (présent sur toutes les pages) idem home idem home
bulk Commande en masse (déléguée) sous_type, nombre_liens_total, thematiques[], quantite_par_periode, frequence, links_data[] localisation, with_backlinks, extra_data selon le sous-type

Sous-types bulk autorisés

articles_10, articles_20, articles_30, articles_50, forums_5, forums_7.5, nap_5, nap_10, classement_10, classement_30, contenu_client_5, contenu_client_10, branding_10, branding_30

Pour un item bulk, nombre_liens_total vaut de 1 à 500, thematiques est une liste non vide (100 thèmes maximum, 100 caractères chacun), quantite_par_periode vaut de 1 à 2 000 et frequence vaut exactement day, week ou month. localisation vaut fr_FR (défaut) ou en_GB. links_data doit être non vide, compter exactement nombre_liens_total lignes et commencer chaque ligne par son entier séquentiel, à partir de 1.

Sous-typesFormat exact d'une ligne links_data
articles_*[n, url, ancre, titre, commentaire, type_affiché, date, prix_affiché] (8)
forums_*[n, url, ancre, titre, type_affiché, date, prix_affiché] (7)
nap_*[n, titre, brief, type_affiché, date, prix_affiché] ; avec backlinks, insérer url_backlink, ancre_backlink après n (6/8)
classement_10[n, produit_1…produit_5, brief, type_affiché, date, prix_affiché] ; avec backlinks, insérer les deux colonnes après n (10/12)
classement_30idem avec produit_1…produit_10 (15/17)
contenu_client_*[n, texte_à_insérer, titre, brief, type_affiché, date, prix_affiché] (7)
branding_*[n, marque, titre, brief, type_affiché, date, prix_affiché] ; avec backlinks, insérer les deux colonnes après n (7/9)

with_backlinks accepte un booléen ou 0/1 ; la valeur vraie est réservée à NAP, classement et branding. Un classement exige son premier produit et au moins trois produits non vides. La date est vide/null ou au format JJ/MM/AAAA, sans être passée. Les URL article/forum sont obligatoires ; une ancre backlink exige son URL. Limites : URL 500, ancre/produit/marque 150, titre 200, brief/commentaire 10 000 et texte à insérer 3 500 caractères. type_affiché est limité à 50 caractères et prix_affiché est numérique entre 0 et 1 000 000.

extra_data est obligatoire pour NAP avec nap_nom, nap_adresse, nap_tel et un iframe HTTPS Google Maps complet dans nap_url_iframe. Il est obligatoire pour classement avec mot_cle et éventuellement ordre_aleatoire. Le champ type optionnel vaut respectivement nap ou classement. Pour les autres familles, extra_data doit être absent ou vide. Les cellules d'affichage et les éventuels parametres/prix_total clients ne font pas autorité : le serveur reconstruit type, tarif et total.

Exemple bulk forum minimal valide

{
  "type": "bulk",
  "sous_type": "forums_5",
  "nombre_liens_total": 1,
  "thematiques": ["tech"],
  "quantite_par_periode": 1,
  "frequence": "month",
  "links_data": [[1, "https://example.com/forum-test", "ancre forum", "", "forums_5", "", 5]],
  "localisation": "fr_FR"
}

Limites & garde-fous

Toute valeur hors borne renvoie une 400 invalid_field avec le détail du champ en cause.

ChampTypes concernésBornes
items—1 ≤ N ≤ 200
Taille du body—≤ 1 MB
ancrespon, kw, home, sitewidechaîne JSON Unicode non vide après trim, sans caractère de catégorie Unicode Cc ; UTF-8 invalide refusé, ≤ 200 chars
url_cibletous (sauf bulk)doit commencer par http:// ou https://, ≤ 500 chars
commentairetous (sauf bulk)chaîne JSON Unicode, ≤ 2000 chars ; tabulations et retours à la ligne CR/LF acceptés, autres caractères de catégorie Unicode Cc et UTF-8 invalide refusés
nb_motsspon, kw100 ≤ x ≤ 5000
urgent, fournirspon, kw0 ou 1
date_souhaitespon, kwchaîne vide ou date valide au format strict JJ-MM-AAAA ; J+7 minimum hors urgence (l'urgence ne s'applique pas si le texte est fourni). Les dates ISO et timestamps sont refusés
durationhome, sitewide1, 2, 3, 6, 12 ou 24 mois
renewalhome, sitewide0 ou 1
nombre_liens_totalbulk1 ≤ x ≤ 500
sous_typebulkvaleur dans la liste plus haut
quantite_par_periodebulk1 ≤ x ≤ 2 000
frequencebulkday, week ou month
localisationbulkfr_FR ou en_GB

En plus de ces validations syntaxiques :

  • Le total du panier doit être ≤ à votre solde de crédits (sinon 422 credits_insuffisants)
  • L'id_site_unique doit exister et le site doit être disponible (sinon item rejeté silencieusement, listé dans items_rejected)
  • La réservation d’idempotence et les écritures de commande sont atomiques dans une transaction de base
  • Le panier technique de l’API est isolé du panier web et des listes sauvegardées

Réponse en cas de succès (HTTP 201)

{
  "data": {
    "num_commande_client": "12345",
    "id_status_commande": 1234,
    "items_accepted": 1,
    "items_rejected": [],
    "total_ht_eur": 25.00,
    "total_ht_cents": 2500,
    "credits_restants_cents": 4650,
    "credits_restants_eur": 46.50,
    "nb_liens": 1
  }
}

Idempotence

Chaque création exige soit client_request_id dans le JSON, soit le header Idempotency-Key (1 à 80 caractères, sans caractère de contrôle). Si les deux sont présents, ils doivent être identiques. Pendant 24 h, le même payload est rejoué avec X-Idempotent-Replay: true; un payload différent renvoie 409 duplicate_request, et une exécution encore en cours 409 request_in_progress avec Retry-After.

Les notifications et synchronisations nécessaires sont enregistrées durablement avec la commande, puis traitées après validation de la transaction. Le succès HTTP 201 confirme la commande et le débit, pas encore l'achèvement de tous les traitements asynchrones.

Pour une commande KW, id_url_unique est réservé de façon unique dans la transaction principale. Une URL déjà attribuée renvoie 422 site_unavailable, même si les traitements asynchrones ne sont pas encore terminés.

Éditeur — Suivi & solde partenaire

Pour les comptes éditeurs : suivre les commandes reçues et consulter le solde partenaire (gains). Un compte non-éditeur reçoit un solde à 0 et une liste vide.

GET /api/v1/editeur/credits

Solde éditeur (old) et Solde éditeur (new), disponibles et en attente (centimes et euros). Les champs historiques et total restent limités au solde old ; les champs bis décrivent le solde new séparément.

curl -H "X-API-Key: $LG_KEY" https://app.linksgarden.com/api/v1/editeur/credits
{
  "data": {
    "solde_partenaire_cents": 12500,
    "solde_partenaire_eur": 125.00,
    "en_attente_cents": 4500,
    "en_attente_eur": 45.00,
    "total_cents": 17000,
    "total_eur": 170.00,
    "solde_partenaire_bis_cents": 2500,
    "solde_partenaire_bis_eur": 25.00,
    "en_attente_bis_cents": 1000,
    "en_attente_bis_eur": 10.00
  }
}
GET /api/v1/editeur/balance/history

Le paramètre compteur accepte old (défaut) ou new. Chaque requête retourne l'historique du seul compteur demandé ; une autre valeur renvoie 400 invalid_field.

curl -H "X-API-Key: $LG_KEY" "https://app.linksgarden.com/api/v1/editeur/balance/history?compteur=new"

Le gain initial suit la date de la vente : avant le 12/10/2026 à 00:00 (Europe/Paris), old ; à partir de cette date, new. Une date de vente absente ou nulle dirige vers new. Cela comprend les ventes initiales home/sitewide. Les renouvellements, l'affiliation et les paiements restent dans le circuit old. Le gain en attente est libéré après le délai habituel de 30 jours dans son compteur d'origine.

GET /api/v1/editeur/orders

Liste paginée des commandes reçues par l'éditeur (suivi).

ParamètreValeurs
statusall (défaut), in_progress, action_required, completed, cancelled
pagedéfaut 1
per_pagedéfaut 50, max 200
curl -H "X-API-Key: $LG_KEY" "https://app.linksgarden.com/api/v1/editeur/orders?status=in_progress"
{
  "data": [
    {
      "id_commande_editeur": "...",
      "status": 7,
      "statut": "En cours",
      "etat": "in_progress",
      "gain_eur": 45,
      "type": "spon",
      "id_site_unique": "2ba80e0c6acdf",
      "root_ndd": "monsite.com",
      "ancre": "...",
      "url_cible": "https://...",
      "date_commande": 1746950000,
      "date_acceptation": 1746950500
    }
  ],
  "meta": { "status": "in_progress", "page": 1, "per_page": 50, "total": 1, "total_pages": 1 }
}
GET /api/v1/editeur/orders/{id}

Détail complet d'une mission. {id} = id_commande_editeur (cf. la liste ci-dessus) : brief annonceur, négociation, dates, infos home/sitewide et fil de messages.

curl -H "X-API-Key: $LG_KEY" https://app.linksgarden.com/api/v1/editeur/orders/12
{
  "data": {
    "id_commande_editeur": 12,
    "id_commande_unique": "...",
    "id_site_unique": "2ba80e0c6acdf",
    "root_ndd": "monsite.com",
    "type": "spon",
    "status": 8,
    "statut": "Veuillez publier le texte",
    "etat": "action_required",
    "gain_eur": 27,
    "redaction": "editeur",
    "lien": { "ancre": "...", "url_cible": "https://...", "url_kw": null, "publication": null },
    "brief": { "commentaire": "...", "nb_mots": 500, "date_souhaitee": null, "urgent": false, "annonceur_fournit_texte": false },
    "negociation": { "editeur": [], "annonceur": [] },
    "dates": { "date_commande": 1746950000, "date_publication": null, "date_fin_commande": null },
    "messages": [ { "auteur": "Annonceur", "type": "message", "message": "...", "date": 1746950100 } ]
  }
}

Inventaire des sites de l'éditeur (ses sites mis en vente). Pour chaque produit : prix payé par l'annonceur et gain net de l'éditeur.

ParamètreValeurs
pagedéfaut 1
per_pagedéfaut 50, max 200
curl -H "X-API-Key: $LG_KEY" "https://app.linksgarden.com/api/v1/editeur/sites?per_page=20"
{
  "data": [
    {
      "id_site_unique": "2ba80e0c6acdf",
      "root_ndd": "monsite.com",
      "categorie": "Tech",
      "localisation": "fr_FR",
      "etat": { "actif_client": true, "en_ligne": true, "propriete_validee": true, "visible_catalogue": true },
      "metrics": { "tf": 15, "cf": 12, "rd": 42, "dr": 18, "da": 22 },
      "redaction": "linksgarden",
      "produits": {
        "spon":     { "actif": true,  "prix_public_eur": 45, "gain_eur": 27, "nb_mots": 500 },
        "kw":       { "actif": true,  "prix_public_eur": 30, "gain_eur": 15, "nb_mots": 300 },
        "home":     { "actif": false, "prix_public_eur": 0, "gain_eur": 0 },
        "sitewide": { "actif": false, "prix_public_eur": 0, "gain_eur": 0 }
      },
      "gains_cumules_eur": 162
    }
  ],
  "meta": { "page": 1, "per_page": 20, "total": 8, "total_pages": 1 }
}
GET /api/v1/editeur/sites/{id}

Détail d'un site (id = id_site_unique) : exactement 13 caractères hexadécimaux minuscules. Tout autre format renvoie 404.

curl -H "X-API-Key: $LG_KEY" https://app.linksgarden.com/api/v1/editeur/sites/2ba80e0c6acdf
POST /api/v1/editeur/sites/{id}/activate
POST /api/v1/editeur/sites/{id}/deactivate

Met exactement le site demandé en ligne (activate) ou hors ligne (deactivate) — colonne actif_client. Les caractères % et _ ne sont pas des jokers. La désactivation le retire des paniers acheteurs en cours.

curl -X POST -H "X-API-Key: $LG_KEY" https://app.linksgarden.com/api/v1/editeur/sites/2ba80e0c6acdf/deactivate
POST /api/v1/editeur/sites/{id}/delete

Supprime physiquement le site. Renvoie 409 invalid_state si une mission est en cours ou si un lien home/sitewide récurrent est encore à poser, en vérification ou actif. Les liens terminés, inactifs ou annulés ne bloquent pas la suppression. En cas de succès, la fiche, son challenge DNS et son registre exclusif sont supprimés dans la même transaction afin de libérer le domaine ; le formulaire web applique le même parcours.

curl -X POST -H "X-API-Key: $LG_KEY" https://app.linksgarden.com/api/v1/editeur/sites/2ba80e0c6acdf/delete
POST /api/v1/editeur/sites

Ajoute un site à l'inventaire (créé hors ligne, en attente de validation de propriété). Body : url + thematique (obligatoires) + langue/pays + config commerciale. thematique = data[].id_groupe de /api/v1/categories (scope catalog:read pour les lister) ; absente → 400 missing_field, catégorie inconnue → 400 invalid_field.

POST /api/v1/editeur/sites/{id}

Modifie partiellement la config commerciale d'un site. Les champs absents, notamment visibilite, description, refus, brief et rédaction, restent inchangés. Sans produits, l'offre actuelle est conservée ; si cet objet est fourni, il remplace la liste active et un produit omis est désactivé. Les réglages omis d'un produit présent sont conservés. Un produit activé requiert un prix positif. thematique est optionnelle (data[].id_groupe ; vide = catégorie inchangée).

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -d '{"thematique":"12","redacteur":"lg","produits":{"spon":{"prix":45,"nb_mots":500},"home":{"prix":25}}}' \
  https://app.linksgarden.com/api/v1/editeur/sites/2ba80e0c6acdf
GET /api/v1/editeur/balance/history

Historique des mouvements du solde partenaire (montants en euros).

GET /api/v1/editeur/payment-info

Coordonnées de paiement : IBAN masqué, titulaire, email PayPal, assujettissement TVA, données de facturation.

curl -H "X-API-Key: $LG_KEY" https://app.linksgarden.com/api/v1/editeur/balance/history
GET /api/v1/editeur/invoices

Liste des factures de paiement : ref_facture, montant_eur, status, methode, date.

GET /api/v1/editeur/affiliation

Parrainage : code_affiliation, nb_filleuls, historique des gains d'affiliation.

POST /api/v1/editeur/payment-info

Renseigne les coordonnées de paiement (prérequis au virement). Body : iban, titulaire, email_paypal, facturation.

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -d '{"iban":"FR7630001007941234567890185","titulaire":"Jean Dupont"}' \
  https://app.linksgarden.com/api/v1/editeur/payment-info
POST /api/v1/editeur/payouts

Mutation financière réelle et asynchrone. Idempotency-Key ou client_request_id est obligatoire. Les contrôles, le débit et l'enregistrement de la facture partagent la même transaction ; le PDF et la notification sont générés ensuite.

  • Montant de 100 € à 83 886,07 €, deux décimales maximum ; délai de 25 jours entre deux demandes
  • Formes acceptées : EI, Micro, SARL, SAS, SASU, EURL, other ; SIRET ou numéro de société toujours requis
  • SARL/SAS/SASU/EURL exigent denominationSociale, capitalSocial et rcsRm ; other exige otherFormeJuridique
  • Hors France, TVA forcée à zéro avec mention légale ; en France, TVA=1 exige un numéro valide
curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: payout-$(date +%s)" \
  -d '{"amount":"150.00","payment_method":"sepa","iban":"FR7630001007941234567890185","account_owner":"Jean Dupont","nom":"Dupont","prenom":"Jean","adresse":"1 rue Exemple","code_postal":"75001","ville":"Paris","siret":"12345678900011","formeJuridique":"SASU","denominationSociale":"Exemple SASU","capitalSocial":"1000 EUR","rcsRm":"RCS Paris 123 456 789","tva_status":0}' \
  https://app.linksgarden.com/api/v1/editeur/payouts

Réponse 202 : montants HT/TTC et trois états pending (payout_status, invoice_generation_status, notification_status). Le 202 confirme le débit et l’enregistrement, pas encore le PDF ni le mail.

GET /api/v1/editeur/profile
POST /api/v1/editeur/profile

Lit / met à jour les coordonnées de l'éditeur (mise à jour partielle : les champs non fournis sont conservés). Body : nom, prenom, societe, num_tva, num_siret, adresse, code_postal, ville, pays, telephone, association, newsletter.

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -d '{"telephone":"+33612345678"}' https://app.linksgarden.com/api/v1/editeur/profile
GET /api/v1/editeur/messages

Fils de messagerie (un par commande avec message) : id_commande_unique, root_ndd, url_cible, ancre, archive, dernier_message.

POST /api/v1/editeur/messages/{id}/archive

Archive un fil (id = id_commande_unique).

POST /api/v1/editeur/sites/{id}/ownership-challenge

Crée un challenge DNS TXT valable 24 h. La réponse fournit le nom _linksgarden.<domaine> et la valeur linksgarden-site-verification=<token>. Le secret n’est renvoyé qu’ici et seul son hash est conservé. Body optionnel : {"replace":true}.

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -d '{}' \
  https://app.linksgarden.com/api/v1/editeur/sites/2ba80e0c6acdf/ownership-challenge
POST /api/v1/editeur/sites/{id}/verify-ownership

Après publication du TXT, déclenche sa résolution DNS, sans body ni secret. Les tentatives sont espacées de 30 s. Une nouvelle preuve peut reprendre un registre orphelin si l'ancienne fiche a disparu ou n'est plus validée ; une fiche encore validée sur un autre compte provoque 409 domain_already_verified. Expiration, preuve absente et DNS indisponible ont également des réponses distinctes.

curl -X POST -H "X-API-Key: $LG_KEY" \
  https://app.linksgarden.com/api/v1/editeur/sites/2ba80e0c6acdf/verify-ownership

Écritures — workflow des commandes

Ces mutations et les paiements sont sensibles. Leur description reflète l’état courant ; le contrat OpenAPI et les suites locales délimitent les garanties effectivement vérifiées.

Ces endpoints modifient l'état d'une commande ({id} = id_commande_editeur) et renvoient la mission à jour. Pour accept, refuse et publish, fournir obligatoirement Idempotency-Key ou client_request_id (1 à 80 caractères, sans caractère de contrôle) ; les deux valeurs doivent être identiques si elles coexistent. Pendant 24 h, un rejeu identique renvoie le statut et le résultat mémorisés avec X-Idempotent-Replay: true. Payload invalide : 400 ; clé réutilisée avec un autre payload ou état incompatible : 409 ; exécution encore en cours : 409 request_in_progress avec Retry-After.

POST /api/v1/editeur/orders/{id}/accept

Accepte une commande (statut 5 requis). Passe en statut 0 (Linksgarden rédige) ou 8 (home/sitewide ou l'éditeur fournit le texte). Sans body.

curl -X POST -H "X-API-Key: $LG_KEY" \
  -H "Idempotency-Key: publisher-accept-12-$(date +%s)" \
  https://app.linksgarden.com/api/v1/editeur/orders/12/accept
POST /api/v1/editeur/orders/{id}/refuse

Refuse une commande (statut 5 ou 16) et rembourse l'annonceur (meta.remboursement_eur). Body : message_refus (obligatoire, sauf motif=low_price).

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: publisher-refuse-12-$(date +%s)" \
  -d '{"message_refus":"Thématique hors sujet"}' \
  https://app.linksgarden.com/api/v1/editeur/orders/12/refuse
POST /api/v1/editeur/orders/{id}/publish

Déclare la mission en ligne (statut 8). Article (spon/kw) : body url → l'API scrape et vérifie le backlink (statut 2 + gain crédité, ou 12 = vérification manuelle). Home/sitewide : sans body, confirme la pose.

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: publisher-publish-12-$(date +%s)" \
  -d '{"url":"https://mon-blog.com/article"}' \
  https://app.linksgarden.com/api/v1/editeur/orders/12/publish
POST /api/v1/editeur/orders/{id}/text-accept

Valide le texte rédigé par Linksgarden (statut 6 requis) → statut 8 (à publier). Sans body.

curl -X POST -H "X-API-Key: $LG_KEY" https://app.linksgarden.com/api/v1/editeur/orders/12/text-accept
POST /api/v1/editeur/orders/{id}/text-refuse

Refuse le texte et le renvoie en rédaction (statut 6 requis) → statut 0. Body : message_refus (obligatoire).

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -d '{"message_refus":"Ton trop promotionnel"}' \
  https://app.linksgarden.com/api/v1/editeur/orders/12/text-refuse
POST /api/v1/editeur/orders/{id}/counter-offer

Contre-proposition de prix (statut 5 ou 16) → statut 15. Body : gain (€, ≥ 1) et message (facultatif). meta.prix_demande_eur = prix correspondant côté annonceur.

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -d '{"gain":30,"message":"Mon tarif pour ce lien"}' \
  https://app.linksgarden.com/api/v1/editeur/orders/12/counter-offer
POST /api/v1/editeur/orders/{id}/accept-offer

Accepte la contre-proposition de l'annonceur (statut 16) → statut 17, prix négocié figé. Body facultatif : offer_count (anti-rejeu). meta.prix_negocie_eur.

curl -X POST -H "X-API-Key: $LG_KEY" https://app.linksgarden.com/api/v1/editeur/orders/12/accept-offer
POST /api/v1/editeur/orders/{id}/messages

Envoie un message à l'annonceur sur le fil de la mission (aucun changement de statut). Body : message (obligatoire).

curl -X POST -H "X-API-Key: $LG_KEY" -H "Content-Type: application/json" \
  -d '{"message":"Bonjour, publication demain."}' \
  https://app.linksgarden.com/api/v1/editeur/orders/12/messages

Traitements asynchrones et documents privés

Certaines opérations déclenchent des traitements asynchrones, notamment les notifications, les synchronisations avec les services partenaires et la génération de documents. Une réponse 201 ou 202 confirme que l'opération métier a été enregistrée ; elle ne garantit pas que tous les effets secondaires sont déjà terminés.

Conservez les identifiants d'idempotence utilisés pour les mutations et consultez ensuite les endpoints de lecture afin de suivre l'état de la commande, de la mission ou du paiement. Les factures et coordonnées de paiement restent privées et ne sont accessibles qu'après authentification et contrôle du propriétaire.

Codes d'erreur

Format standard de réponse en cas d'erreur :

{
  "error": {
    "code": "credits_insuffisants",
    "message": "Solde insuffisant : 12.50€ disponibles, 45.00€ requis.",
    "status": 422,
    "request_id": "a1b2c3d4e5f6a7b8",
    "details": { "solde_cents": 1250, "requis_cents": 4500 }
  }
}
HTTPcodeDescription
400invalid_payloadJSON malformé
400missing_fieldChamp obligatoire absent
400invalid_fieldValeur invalide
400unknown_typeitems[i].type inconnu
400panier_videitems=[] vide
401missing_api_keyHeader X-API-Key manquant
401invalid_api_keyClé inconnue / mauvais format
401key_expiredClé expirée
403key_revokedClé révoquée
403user_inactiveCompte désactivé
403insufficient_scopeScope requis absent
404resource_not_foundRoute inconnue, journalisée
404site_not_foundID site inexistant
404order_not_foundNuméro de commande inconnu
405method_not_allowedMéthode HTTP non supportée, journalisée, avec header Allow
409duplicate_requestIdentifiant d’idempotence réutilisé avec un autre payload
409request_in_progressRequête idempotente identique encore en cours
409invalid_stateTransition éditeur incompatible avec l'état courant
413payload_too_largeBody > 1 MB ou > 200 items
422credits_insuffisantsSolde < total panier
422site_unavailableAu moins un site n'est plus disponible
422order_failedAucun item n'a pu être ajouté
422payout_billing_invalidProfil ou informations légales incohérents
429payout_too_soonDélai de 25 jours non écoulé
429rate_limitedQuota partagé de 60 requêtes par fenêtre de 60 s dépassé
500internal_errorErreur serveur

Recettes pratiques

Commander un spon en bash

API_KEY="lg_xxxxxxxx_yyyyyyyy"

curl -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "Idempotency-Key: order-$(date +%s)" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "type": "spon",
      "id_site_unique": "abc12345",
      "url_cible": "https://monsite.com/page",
      "ancre": "ma super ancre",
      "nb_mots": 500
    }]
  }' \
  https://app.linksgarden.com/api/v1/orders

Commande mixte (spon + home + bulk) en une requête

curl -X POST -H "X-API-Key: $API_KEY" -H "Idempotency-Key: order-mixte-$(date +%s)" -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "type": "spon",  "id_site_unique": "...", "url_cible": "...", "ancre": "...", "nb_mots": 500 },
      { "type": "home",  "id_site_unique": "...", "url_cible": "...", "ancre": "...", "duration": 12 },
      {
        "type": "bulk",
        "sous_type": "articles_10",
        "nombre_liens_total": 1,
        "thematiques": ["tech"],
        "quantite_par_periode": 1,
        "frequence": "month",
        "links_data": [[1, "https://example.com/article-bulk", "ancre bulk", "", "", "articles_10", "", 10]],
        "localisation": "fr_FR"
      }
    ]
  }' \
  https://app.linksgarden.com/api/v1/orders

Python (avec requests)

import time
import requests

API = "https://app.linksgarden.com/api/v1"
KEY = "lg_xxxxxxxx_yyyyyyyy"
headers = {"X-API-Key": KEY, "Content-Type": "application/json"}

# Mon solde
r = requests.get(f"{API}/me", headers=headers)
print(r.json()["data"]["credits"])

# Lister les sites Tech avec TF >= 15
r = requests.get(f"{API}/sites",
    headers=headers,
    params={"lang": "fr_FR", "tf_min": 15, "per_page": 50})
for s in r.json()["data"]:
    print(s["root_ndd"], s["prix_eur"], "€")

# Commander
r = requests.post(f"{API}/orders", headers=headers, json={
    "items": [{
        "type": "spon",
        "id_site_unique": "abc12345",
        "url_cible": "https://monsite.com/page",
        "ancre": "ma super ancre",
        "nb_mots": 500
    }],
    "client_request_id": f"order-{int(time.time())}"
})
print(r.status_code, r.json())

Node.js (fetch natif)

const API = "https://app.linksgarden.com/api/v1";
const KEY = "lg_xxxxxxxx_yyyyyyyy";
const headers = { "X-API-Key": KEY, "Content-Type": "application/json" };

const sites = await fetch(`${API}/sites?lang=fr_FR&tf_min=15&per_page=50`,
    { headers }).then(r => r.json());

const order = await fetch(`${API}/orders`, {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": `order-${crypto.randomUUID()}` },
    body: JSON.stringify({
        items: [{
            type: "spon",
            id_site_unique: sites.data[0].id_site_unique,
            url_cible: "https://monsite.com/page",
            ancre: "ancre",
            nb_mots: 500,
        }],
    }),
}).then(r => r.json());
console.log(order);

Workflow n8n / Make

Utilisez un node HTTP Request standard avec :

  • Method : GET / POST
  • URL : https://app.linksgarden.com/api/v1/...
  • Headers : X-API-Key = {{ $env.LG_API_KEY }} + Content-Type: application/json; ajouter une valeur unique Idempotency-Key pour POST /api/v1/orders
  • Body (POST) : JSON conforme aux exemples ci-dessus