Documentation API
Intégrez Linksgarden dans vos outils (n8n, Make, scripts, ERP…).Sommaire
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 :
| HTTP | code | Quand |
|---|---|---|
| 401 | missing_api_key | Header X-API-Key absent |
| 401 | invalid_api_key | Clé inconnue ou format invalide |
| 401 | key_expired | Clé expirée |
| 403 | key_revoked | Clé révoquée |
| 403 | user_inactive | Compte désactivé |
| 403 | insufficient_scope | Scope 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.
| Espace | Scope | Autorisation |
|---|---|---|
| Général | account:read | Consulter le compte et le solde annonceur |
| Annonceur | catalog:read | Consulter le catalogue et les tarifs |
orders:read | Consulter les commandes annonceur | |
orders:write | Passer des commandes et dépenser les crédits | |
| Éditeur | publisher_sites:read | Consulter les sites éditeur |
publisher_missions:read | Consulter les missions et leur messagerie | |
publisher_earnings:read | Consulter les soldes, l’historique des gains et l’affiliation | |
publisher_invoices:read | Consulter les factures éditeur | |
publisher_profile:read | Consulter le profil et les coordonnées de paiement | |
publisher_sites:write | Gérer les sites et leur validation de propriété | |
publisher_missions:write | Gérer les missions et leur messagerie | |
publisher_profile:write | Modifier le profil et les coordonnées de paiement | |
payouts:write | Demander 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
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": "..." }
}
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
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.
Liste paginée des sites. Tous les filtres sont optionnels.
Filtres acceptés en query string
| Paramètre | Description | Valeurs |
|---|---|---|
category | ID de catégorie | ID retourné par /categories, ou all, favoris, media |
lang | Langue cible | fr_FR (défaut), en_US, en_GB, de_DE, es_ES, it_IT, pt_PT |
tf_min | Majestic Trust Flow ≥ | 0, 5, 10, 15, 20 |
cf_min | Majestic Citation Flow ≥ | 0, 5, 10, 15, 20 |
rd_min | Referring Domains ≥ | 0, 10, 20, …, 100 |
dr_min | Ahrefs Domain Rating ≥ | 0, 5, 10, 20, 30 |
da_min | Moz Domain Authority ≥ | 0, 5, 10, 20, 30 |
price_max | Prix max en € (filtre prix_eur ≤ price_max) | 0 (tous) ou tout entier > 0, p.ex. 39, 45 |
exclusive | Sites exclusifs uniquement | 0, 1 |
eclair | Sites éclair uniquement | 0, 1 |
source_geo | Présence comme source sur ChatGPT dans l'échantillon Semnaut (lang=fr_FR uniquement) | 1 (observé), 0 (non observé), omis (tous) |
search | Recherche sur la valeur de domaine visible | texte libre |
target_site | Domaine ou sous-domaine cible nu, sans protocole, port ni chemin | exemple.fr, blog.exemple.fr |
include_used | Conserver les sites déjà utilisés pour cette cible ; nécessite target_site | true / 1, false / 0 (défaut) |
page | Numéro de page | défaut 1 |
per_page | Items par page | dé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.
Détail d'un site. Renvoie 404 si l'ID n'existe pas.
Commandes — Lecture
Liste paginée. status peut valoir :
all(défaut) — toutes les commandes payéesin_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).
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
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
| type | Description | Champs requis | Champs 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-types | Format 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_30 | idem 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.
| Champ | Types concernés | Bornes |
|---|---|---|
items | — | 1 ≤ N ≤ 200 |
| Taille du body | — | ≤ 1 MB |
ancre | spon, kw, home, sitewide | chaîne JSON Unicode non vide après trim, sans caractère de catégorie Unicode Cc ; UTF-8 invalide refusé, ≤ 200 chars |
url_cible | tous (sauf bulk) | doit commencer par http:// ou https://, ≤ 500 chars |
commentaire | tous (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_mots | spon, kw | 100 ≤ x ≤ 5000 |
urgent, fournir | spon, kw | 0 ou 1 |
date_souhaite | spon, kw | chaî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 |
duration | home, sitewide | 1, 2, 3, 6, 12 ou 24 mois |
renewal | home, sitewide | 0 ou 1 |
nombre_liens_total | bulk | 1 ≤ x ≤ 500 |
sous_type | bulk | valeur dans la liste plus haut |
quantite_par_periode | bulk | 1 ≤ x ≤ 2 000 |
frequence | bulk | day, week ou month |
localisation | bulk | fr_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_uniquedoit exister et le site doit être disponible (sinon item rejeté silencieusement, listé dansitems_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.
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
}
}
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.
Liste paginée des commandes reçues par l'éditeur (suivi).
| Paramètre | Valeurs |
|---|---|
status | all (défaut), in_progress, action_required, completed, cancelled |
page | défaut 1 |
per_page | dé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 }
}
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ètre | Valeurs |
|---|---|
page | défaut 1 |
per_page | dé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 }
}
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
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
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
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.
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
Historique des mouvements du solde partenaire (montants en euros).
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
Liste des factures de paiement : ref_facture, montant_eur, status, methode, date.
Parrainage : code_affiliation, nb_filleuls, historique des gains d'affiliation.
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
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.
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
Fils de messagerie (un par commande avec message) : id_commande_unique, root_ndd, url_cible, ancre, archive, dernier_message.
Archive un fil (id = id_commande_unique).
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
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.
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
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
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
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
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
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
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
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 }
}
}
| HTTP | code | Description |
|---|---|---|
| 400 | invalid_payload | JSON malformé |
| 400 | missing_field | Champ obligatoire absent |
| 400 | invalid_field | Valeur invalide |
| 400 | unknown_type | items[i].type inconnu |
| 400 | panier_vide | items=[] vide |
| 401 | missing_api_key | Header X-API-Key manquant |
| 401 | invalid_api_key | Clé inconnue / mauvais format |
| 401 | key_expired | Clé expirée |
| 403 | key_revoked | Clé révoquée |
| 403 | user_inactive | Compte désactivé |
| 403 | insufficient_scope | Scope requis absent |
| 404 | resource_not_found | Route inconnue, journalisée |
| 404 | site_not_found | ID site inexistant |
| 404 | order_not_found | Numéro de commande inconnu |
| 405 | method_not_allowed | Méthode HTTP non supportée, journalisée, avec header Allow |
| 409 | duplicate_request | Identifiant d’idempotence réutilisé avec un autre payload |
| 409 | request_in_progress | Requête idempotente identique encore en cours |
| 409 | invalid_state | Transition éditeur incompatible avec l'état courant |
| 413 | payload_too_large | Body > 1 MB ou > 200 items |
| 422 | credits_insuffisants | Solde < total panier |
| 422 | site_unavailable | Au moins un site n'est plus disponible |
| 422 | order_failed | Aucun item n'a pu être ajouté |
| 422 | payout_billing_invalid | Profil ou informations légales incohérents |
| 429 | payout_too_soon | Délai de 25 jours non écoulé |
| 429 | rate_limited | Quota partagé de 60 requêtes par fenêtre de 60 s dépassé |
| 500 | internal_error | Erreur 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 uniqueIdempotency-KeypourPOST /api/v1/orders - Body (POST) : JSON conforme aux exemples ci-dessus