Documentation de l'API
API REST de lcrs.fr pour créer et gérer les liens courts d'un workspace, sur le domaine du service ou sur un domaine personnalisé, obtenir leurs QR codes et lire leurs statistiques.
Créer une clé
Dans le dashboard, ouvrez le workspace, puis « Clés API » (réservé aux propriétaires et administrateurs, adresse e-mail confirmée). Donnez un nom à la clé, choisissez sa portée (lecture et écriture, ou lecture seule), puis copiez la clé affichée : elle ne sera plus jamais affichée. Une clé se révoque depuis le même écran.
Authentification
Clé API du workspace dans l'en-tête Authorization: Bearer <clé> uniquement. Une clé se crée dans le dashboard (« Clés API » du workspace) et ne s'affiche qu'une fois. Clé absente, invalide ou révoquée : 401 unauthenticated. Après 100 échecs d'authentification en 15 minutes depuis une même adresse IP : 429 rate_limited, même pour une clé valide, jusqu'à la fin de la fenêtre. Plusieurs intégrations qui partagent une adresse IP de sortie (plateforme d'automatisation, NAT d'entreprise, CI) partagent donc ce compteur : une intégration doit cesser ses appels après un 401 au lieu de réessayer, et une clé révoquée doit être retirée de toutes les intégrations qui l'utilisent. La clé ne doit jamais figurer dans l'URL : en paramètre de requête, elle est refusée (401 ; 422 unknown_field si une clé valide est aussi présentée dans l'en-tête) et peut rester dans les journaux d'accès des intermédiaires (proxy, serveur web).
Format des clés
lcrs_<identifiant>_<secret>. lcrs_ est le préfixe du service ; l'identifiant de clé (8 caractères, non secret) apparaît dans created_by.key_prefix et dans le dashboard sous la forme lcrs_<identifiant>…. Une règle gitleaks fournie par lcrs.fr, reproduite en entier dans la documentation publique /docs/api (section « Format des clés »), détecte les clés publiées par erreur.
Pour détecter une clé publiée par erreur, ajoutez cette règle à la configuration gitleaks de vos dépôts :
# Règle gitleaks de détection des clés API lcrs.fr (FR-003).
#
# Une clé a la forme lcrs_<identifiant>_<secret> : le préfixe du service « lcrs_ », un
# identifiant de clé de 8 caractères et un secret de 44 caractères, tous deux dans l'alphabet
# base58. Ajoutez ce bloc à la configuration gitleaks de votre projet (fichier .gitleaks.toml,
# avec [extend] useDefault = true pour garder les règles par défaut).
[[rules]]
id = "lcrs-api-key"
description = "Clé API lcrs.fr : révoquez-la dans « Clés API » du workspace, puis créez-en une autre."
regex = '''lcrs_[1-9A-HJ-NP-Za-km-z]{8}_[1-9A-HJ-NP-Za-km-z]{44}'''
keywords = ["lcrs_"]
tags = ["key", "lcrs"]
Portée
Une clé read_only ne peut que lire ; toute écriture rend 403 insufficient_scope. L'existence du lien est contrôlée avant la portée : une écriture visant un lien introuvable rend 404 quelle que soit la portée de la clé.
Format
JSON en UTF-8, champs en snake_case, dates ISO 8601 en UTC, identifiants publics préfixés (lnk_, dom_). Corps des requêtes POST et PATCH en Content-Type: application/json (paramètre charset=utf-8 accepté, casse indifférente ; type absent ou autre : 400 invalid_content_type), objet JSON de 16 Kio au plus (au-delà : 413 payload_too_large). Un corps envoyé avec GET ou DELETE est ignoré jusqu'à 1 Mio ; au-delà de 1 Mio, quelle que soit l'opération, le serveur web refuse la requête (413 payload_too_large, au même format, sans en-têtes X-RateLimit-* ni X-Request-Id).
Erreurs
{"error": {"code", "message", "details"?, "request_id"?}} ; code stable (anglais), message en français. Une erreur du client ne rend jamais de 5xx : une requête que le serveur ne peut pas interpréter (en-tête d'hôte invalide…) rend 400 bad_request.
| Code | Statut | Sens |
|---|---|---|
unauthenticated | 401 | Clé absente, invalide ou révoquée. |
insufficient_scope | 403 | Écriture avec une clé en lecture seule. |
workspace_pending_deletion | 403 | Workspace en attente de suppression. |
not_found | 404 | Lien ou chemin introuvable. |
invalid_json | 400 | Corps qui n'est pas un objet JSON valide. |
invalid_content_type | 400 | Content-Type absent ou autre que JSON. |
bad_request | 400 | Requête que le serveur ne peut pas interpréter. |
method_not_allowed | 405 | Méthode non prévue pour ce chemin. |
payload_too_large | 413 | Corps de plus de 16 Kio (1 Mio au serveur web). |
validation_failed | 422 | Valeurs invalides, un détail par champ. |
unknown_field | 422 | Champ ou paramètre non prévu. |
slug_taken | 409 | Slug déjà pris ; un slug libre est proposé. |
link_state_conflict | 409 | Changement impossible dans l'état actuel du lien. |
link_blocked | 409 | Lien bloqué pour destination dangereuse : pas de QR code (1.3.0). |
idempotency_key_reused | 409 | Même Idempotency-Key, corps différent. |
idempotency_in_progress | 409 | Première requête de même Idempotency-Key encore en cours. |
plan_feature_unavailable | 403 | Fonctionnalité non comprise dans l'offre du titulaire (statistiques) ; voir `details`. |
quota_exceeded | 429 | Limite de l'offre atteinte (liens, tags par lien, ou clics du mois pour les statistiques) ; voir `details.quota`. Ne pas réessayer avant `Retry-After` s'il est présent. |
rate_limited | 429 | Limite de débit dépassée ; voir Retry-After. |
creation_limit_reached | 429 | Plafond de créations du compte sur 24 heures atteint ; voir `details.retry_at` et Retry-After (1.5.0). |
account_suspended | 403 | Compte du créateur de la clé suspendu par l'équipe lcrs.fr (1.5.0). |
workspace_suspended | 403 | Workspace suspendu par l'équipe lcrs.fr (1.5.0). |
internal_error | 500 | Erreur du service, avec request_id. |
service_unavailable | 503 | Service momentanément indisponible. |
Pagination
page (≥ 1, défaut 1), per_page (1 à 100, défaut 20) ; hors bornes : 422.
Limite de débit
Par clé, selon l'offre du titulaire du workspace (Free 60 requêtes par minute, Starter 300, Pro et Business 1 200). En-têtes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset ; dépassement : 429 rate_limited avec Retry-After. Ces trois en-têtes sont présents sur toute réponse à une requête authentifiée, succès ou erreur (absents seulement en 401, sur le 429 des échecs d'authentification par IP et sur les 404 et 405 de chemin ou de méthode inconnus, rendus avant l'authentification) : la limite de débit est toujours bornée.
Chemins et méthodes
Un chemin inconnu sous /api/v1/, barre oblique finale comprise (/links/ : aucune redirection), rend 404 not_found ; une méthode non prévue pour un chemin existant (PUT, OPTIONS…) rend 405 method_not_allowed, au format d'erreur standard. Ces deux réponses sont rendues avant toute vérification de la clé, qu'une clé soit présentée ou non : sans en-têtes X-RateLimit-*, sans consommer de limite de débit ni compter comme une utilisation de la clé.
Paramètres de requête
Chaque opération n'accepte que les paramètres qu'elle documente (listLinks : page, per_page, q, status, slug, tag, has_tags, untagged, domain ; listTags : page, per_page, key ; getAnalytics : period, from, to, tag, link, country, device, referrer, group_by, sort, page, per_page ; listDomains : page, per_page ; getLinkQrCode : format, size ; les autres : aucun). Tout autre paramètre (per-page, limit…) rend 422 unknown_field, avec le nom du paramètre dans details, comme un champ inconnu du corps : une faute de frappe n'est jamais ignorée. Une pagination ou un filtre invalides, ou un lien introuvable, sont signalés en premier.
Création rejouable
En-tête facultatif Idempotency-Key sur la création (24 h).
Statistiques
getAnalytics, à partir de l'offre Starter (Free : 403 plan_feature_unavailable). Clics hors robots, jours et heures selon l'heure de Paris, tags pris à l'instant de chaque clic. Suspendues (429 quota_exceeded, details.quota à max_clicks_per_month) quand la consommation du mois du titulaire dépasse la limite de son offre, jusqu'au 1er du mois suivant.
Compteur des liens (1.2.0)
click_count et last_clicked_at d'un lien ne comptent plus les clics de robots, comme les statistiques. Recalculés une fois à la mise en service de la 1.2.0, ils ont pu baisser pour les liens qui avaient reçu des clics de robots (aperçus des messageries et réseaux sociaux) ; ils ne baissent plus ensuite, purge comprise.
Domaines (1.3.0)
Un lien est servi sur le domaine du service (lcrs.fr) ou sur un domaine personnalisé du titulaire du workspace (go.acme.fr). Les domaines s'ajoutent, se vérifient et se suppriment dans le dashboard (« Mon compte », réservé au titulaire) ; l'API les liste en lecture (listDomains). À la création, domain (identifiant dom_… ou nom) choisit le domaine ; sans domain, le domaine par défaut du workspace est utilisé. Seul un domaine active peut recevoir un lien. Un slug est unique sur son domaine : promo peut exister sur lcrs.fr et sur go.acme.fr. short_url est toujours l'adresse complète, sur le domaine du lien, en forme ASCII (https://liens.xn--caf-dma.fr/promo).
QR codes (1.3.0)
getLinkQrCode rend l'image (PNG ou SVG) du QR code de l'URL courte d'un lien ; chaque scan passe par la redirection et compte comme un clic.
Aperçus sociaux (1.4.0)
Collé sur un réseau social ou une messagerie (LinkedIn, Facebook, X, Slack, WhatsApp…), un lien actif est présenté par une carte que lcrs.fr construit à partir des informations publiées par sa destination (titre, description, image recadrée), avec la mention « via lcrs.fr » sur l'offre Free ; les offres payantes peuvent la retirer dans les réglages du workspace. Le champ preview d'un lien, en lecture seule, décrit cette carte. Elle ne se personnalise pas. Les passages des robots des réseaux ne comptent jamais comme des clics.
Anti-abus (1.5.0)
Un lien peut être checking (en vérification : sa destination est analysée avant qu'il ne redirige, quelques secondes, pour un compte récent) ou under_review (en examen par l'équipe lcrs.fr) ; ni l'un ni l'autre ne redirige. Une destination refusée par lcrs.fr est rejetée à la création (422, destination_refused) ; un compte récent a un plafond de créations sur 24 heures (429 creation_limit_reached, avec Retry-After) ; les clés d'un compte ou d'un workspace suspendu sont refusées (403 account_suspended, workspace_suspended). Les mesures sont expliquées et contestables dans le dashboard.
Opérations
Lister les liens du workspace
Liens non supprimés du workspace de la clé, du plus récent au plus ancien.
Cas limites : per_page=500 ou page=0 → 422 ; status inconnu → 422 ; paramètre non
prévu (per-page, sort) → 422 unknown_field ; slug d'un lien supprimé ou slug
retiré → liste vide.
Filtres de tags, combinés entre eux (le lien doit tous les satisfaire) et avec les autres
filtres : tag[<clé>]=<valeur> (clé et valeur exactes), has_tags=<clé>,<clé> (clé
présente, toute valeur), untagged=true (liens sans aucun tag). Plus de 5 clés au total
→ 422 too_many_tag_filters ; même clé deux fois (tag et has_tags confondus, ou
tag[<clé>] répété dans la requête), ou untagged avec un autre filtre de tag → 422
tag_filter_conflict ; clé ou valeur invalide → 422. La valeur d'un filtre est mise en
forme comme une valeur de tag enregistrée (forme Unicode composée, espaces de bord
retirés ; vide après ce retrait → 422).
Paramètres
page(requête) : Numéro de page, à partir de 1 ; hors bornes ou non numérique → 422. Exemple :1.per_page(requête) : Taille de page, de 1 à 100 ; hors bornes ou non numérique → 422. Exemple :20.q(requête) : Fragment cherché, sans distinction de casse, dans le titre, le slug et la destination lisible (100 caractères au plus ; au-delà → 422). Exemple :offre.status(requête) : Statut affiché. Exemple :active.slug(requête) : Slug exact, sans distinction de casse, slug actuel seulement. Un slug étant unique par domaine (1.3.0), rend au plus un lien par domaine ; combiner avecdomainpour viser un seul lien. Exemple :Black-Friday.tag(requête) : Filtre clé et valeur exactes, une entrée par clé :tag[campaign]=black-friday&tag[channel]=email. Clé normalisée en minuscules ; valeur mise en forme comme une valeur enregistrée (forme composée, espaces de bord retirés) puis comparée à la casse près ; une clé répétée → 422tag_filter_conflict. Exemple :{ "campaign": "black-friday" }.has_tags(requête) : Clés présentes, quelle que soit leur valeur, séparées par des virgules (5 au plus,tagcompris). Exemple :campaign,channel.untagged(requête) :true: seulement les liens sans aucun tag ; incompatible avectagethas_tags. Exemple :true.domain(requête) : Liens d'un domaine (dom_…) : le domaine du service, tout domaine rendu parlistDomains(même sans lien), ou tout domaine non retiré qui porte des liens du workspace (y compris un domainedisabledou celui d'un ancien titulaire du workspace). Mal formé → 422 ; autre domaine (inconnu, retiré, d'un autre titulaire sans lien du workspace) → 404. Exemple :dom_1CfhK2qKFyECr9Rx5oPk7j.
Exemple de requête
curl "https://lcrs.fr/api/v1/links?page=1&per_page=20" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…"
Réponse 200
Page de liens.
{
"data": [
{
"id": "lnk_1CfhK2qKFyECr9Rx5oPk7i",
"short_url": "https://go.acme.fr/black-friday",
"domain": {
"id": "dom_1CfhK2qKFyECr9Rx5oPk7j",
"name": "go.acme.fr"
},
"slug": "black-friday",
"title": "Black Friday 2026",
"destination_url": "https://exemple.fr/soldes/black-friday-2026",
"status": "active",
"expires_at": "2026-11-30T22:59:00Z",
"forward_query": false,
"click_count": 1284,
"last_clicked_at": "2026-10-04T12:02:11Z",
"created_at": "2026-09-28T08:15:00Z",
"updated_at": "2026-10-02T09:40:00Z",
"created_by": {
"type": "api_key",
"name": "Site e-commerce",
"key_prefix": "7Qk2Ab9x"
},
"tags": {
"campaign": "black-friday",
"channel": "email"
},
"preview": {
"status": "ready",
"banner": false,
"title": "Black Friday : -40 % sur toute la boutique",
"description": "Jusqu'au 1er décembre, livraison offerte.",
"image_url": "https://go.acme.fr/_preview/3f/3fa94c0e5b1d2a7f88e6c41b9d03a5f7e2c18b6d4a9f0e3c7b2d5a8e1f46b2c2.jpg",
"read_at": "2026-10-02T09:40:12Z",
"failure": null
}
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 38
}
}
Erreurs
- 400
-
Requête rejetée par le serveur avant son traitement, par exemple un en-tête d'hôte invalide (
bad_request, message générique).Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Workspace en attente de suppression (
workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0) ; une lecture ne reçoit jamaisinsufficient_scope.Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 404
-
Lien inconnu, supprimé ou d'un autre workspace ; ou domaine désigné (
domain,idd'un domaine) inconnu, d'un autre titulaire ou retiré.Lien inconnu, supprimé ou d'un autre workspace
{ "error": { "code": "not_found", "message": "Lien introuvable." } }Domaine inconnu ou d'un autre titulaire
{ "error": { "code": "not_found", "message": "Domaine introuvable." } } - 422
-
Pagination ou filtre invalides (
validation_failed, un détail par paramètre), paramètre non prévu (unknown_field).Pagination hors bornes (per_page=500)
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "per_page", "code": "out_of_range", "message": "La taille de page doit être comprise entre 1 et 100." } ] } }Statut de filtre inconnu
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "status", "code": "invalid_choice", "message": "Statut inconnu." } ] } }Filtre slug mal formé
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "slug", "code": "slug_invalid", "message": "Le slug compte de 3 à 64 caractères : lettres non accentuées, chiffres, tiret et underscore." } ] } }Paramètre de requête non prévu (per-page)
{ "error": { "code": "unknown_field", "message": "La requête contient un champ ou un paramètre inconnu.", "details": [ { "field": "per-page", "code": "unknown_field", "message": "Ce champ ou paramètre n'est pas prévu par cette opération." } ] } }Plus de 5 filtres de tags
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "tag", "code": "too_many_tag_filters", "message": "Filtrez sur 5 clés de tags au plus." } ] } }untagged avec un autre filtre de tag
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "untagged", "code": "tag_filter_conflict", "message": "Le filtre untagged ne se combine pas avec un autre filtre de tag." } ] } } - 429
-
Limite de débit (
rate_limited, en-têteRetry-After) : par clé, ou après 100 échecs d'authentification en 15 minutes depuis une même IP.Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Créer un lien court
Crée un lien sur le domaine choisi (domain) ou, à défaut, sur le domaine par défaut du
workspace (ou lcrs.fr si ce domaine n'est plus utilisable) ; les slugs réservés sont
contrôlés sur ce domaine effectif. Sans slug, un slug aléatoire de 7 caractères est attribué. Le lien est vérifié contre la liste des menaces juste après sa création :
il peut passer au statut blocked dans les minutes qui suivent. Pour un compte récent ou
surveillé, il est créé checking et redirige dès que l'analyse de sa destination est
faite (moins de 30 secondes en général) ; il peut aussi passer under_review (1.5.0).
Destination refusée par lcrs.fr → 422 (destination_url, destination_refused) ;
plafond de créations du compte sur 24 heures atteint → 429 creation_limit_reached
(details.retry_at, Retry-After) ; compte ou workspace suspendu → 403
account_suspended ou workspace_suspended.
Cas limites : slug réservé ou mal formé → 422 (sur lcrs.fr, toute la liste : login,
api… ; sur un domaine personnalisé, les seuls termes sensibles : abuse, lcrs…) ;
slug pris sur ce domaine, même en ignorant la casse → 409 avec un slug libre proposé ;
domain d'un domaine du titulaire qui n'est pas actif → 422 (domain,
domain_not_usable) ; domain inconnu, d'un autre titulaire ou d'un ancien titulaire du
workspace → 404 not_found ; destination vers lcrs.fr ou un domaine personnalisé, adresse
privée, schéma autre que http(s), plus de 2 048 caractères → 422 ; expiration passée ou
sans fuseau → 422 ; limite de liens de l'offre atteinte → 429 quota_exceeded
(details.quota à max_links) ; plus de tags que la limite de l'offre → 429
quota_exceeded (details.quota à max_tags_per_link) ; tags qui n'est pas un objet,
valeur de tag qui n'est pas une chaîne (null compris) → 422 invalid_type ; clé de tag
invalide ou en double après passage en minuscules, valeur vide, trop longue ou avec un
caractère refusé (contrôle, retour ou séparateur de ligne, contrôle du sens d'écriture)
→ 422, champ tags.<clé> ; champ inconnu → 422 unknown_field ;
écriture empêchée par une écriture concurrente (conflit momentané de la base, rare) →
503 service_unavailable, rien n'est modifié : renvoyer la requête.
Les tags font partie du corps comparé par Idempotency-Key, après mise en forme (clés en
minuscules, valeurs en forme composée sans espaces de bord) et à l'ordre des clés près :
{"Campaign": "x"} rejoué en {"campaign": "x"} est le même corps.
Paramètres
Idempotency-Key(en-tête) : Valeur choisie par le client (1 à 255 caractères ASCII imprimables, sans espace au début ni à la fin). Rejouée pendant 24 h avec la même clé API et le même corps, elle rend la même réponse sans créer de doublon (en-têteIdempotent-Replayed: true). La réponse d'origine est rendue telle quelle, sans nouvelle validation, même si le lien a été modifié ou supprimé depuis : relire le lien pour son état actuel. Même valeur et corps différent (comparé tel qu'envoyé, à l'ordre des champs près : une même date écrite dans un autre fuseau est un autre corps) : 409idempotency_key_reused; première requête encore en cours (moins de 5 minutes) : 409idempotency_in_progress. Une création refusée n'est pas mémorisée.
Corps de la requête
| Champ | Type | Description |
|---|---|---|
destination_url | texte, obligatoire | URL http(s) ; IDN et espaces acceptés (normalisés). 2 048 caractères au plus. |
title | texte ou null | 100 caractères au plus. |
slug | texte ou null | Slug personnalisé ; absent, null ou chaîne vide pour un slug aléatoire (comme le champ vide du dashboard). Refusé (422) s'il a la forme d'une clé API. |
expires_at | date ou null | Date future avec fuseau. |
forward_query | booléen | |
tags | objet | Tags du lien créé. Clés converties en minuscules (deux clés identiques à la casse près → 422) ; valeurs sans espaces de bord. |
domain | texte | Domaine du lien : identifiant dom_… ou nom (go.acme.fr, forme lisible ou ASCII) ; absent : domaine par défaut du workspace (1.3.0). 253 caractères au plus. |
Exemple de requête
curl -X POST "https://lcrs.fr/api/v1/links" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…" \
-H "Content-Type: application/json" \
-d '{"destination_url":"https://exemple.fr/offre","title":"Offre d'\''automne","slug":null,"expires_at":"2026-12-31T22:59:00Z","forward_query":true,"tags":{"campaign":"automne-2026","channel":"email"}}'
Réponse 201
Lien créé (ou création rejouée).
{
"id": "lnk_1CfhP3ZymREn7Ne4GA5LH3",
"short_url": "https://lcrs.fr/aB3dE9k",
"domain": {
"id": "dom_1CfhJmHgRwcnJGYzZgkkcf",
"name": "lcrs.fr"
},
"slug": "aB3dE9k",
"title": "Offre d'automne",
"destination_url": "https://exemple.fr/offre",
"status": "active",
"expires_at": "2026-12-31T22:59:00Z",
"forward_query": true,
"click_count": 0,
"last_clicked_at": null,
"created_at": "2026-10-04T15:02:00Z",
"updated_at": "2026-10-04T15:02:00Z",
"created_by": {
"type": "api_key",
"name": "Site e-commerce",
"key_prefix": "7Qk2Ab9x"
},
"tags": {
"campaign": "automne-2026",
"channel": "email"
},
"preview": {
"status": "pending",
"banner": null,
"title": null,
"description": null,
"image_url": null,
"read_at": null,
"failure": null
}
}
Erreurs
- 400
-
Requête mal formée : JSON invalide (
invalid_json), type de contenu absent ou autre queapplication/json(invalid_content_type), ou requête rejetée avant son traitement (bad_request).Corps qui n'est pas un objet JSON valide
{ "error": { "code": "invalid_json", "message": "Le corps de la requête n'est pas du JSON valide." } }Corps envoyé sans le type application/json
{ "error": { "code": "invalid_content_type", "message": "Envoyez le corps de la requête en Content-Type: application/json." } }Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Portée insuffisante (
insufficient_scope), workspace en attente de suppression (workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0).Écriture avec une clé en lecture seule
{ "error": { "code": "insufficient_scope", "message": "Cette clé est en lecture seule." } }Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 404
-
Lien inconnu, supprimé ou d'un autre workspace ; ou domaine désigné (
domain,idd'un domaine) inconnu, d'un autre titulaire ou retiré.Lien inconnu, supprimé ou d'un autre workspace
{ "error": { "code": "not_found", "message": "Lien introuvable." } }Domaine inconnu ou d'un autre titulaire
{ "error": { "code": "not_found", "message": "Domaine introuvable." } } - 409
-
Slug pris (
slug_taken, avecdetails.suggestion), clé d'idempotence réutilisée ou en cours.Slug déjà pris, même en ignorant la casse
{ "error": { "code": "slug_taken", "message": "Ce lien court est déjà pris.", "details": { "suggestion": "black-friday-2" } } }Même Idempotency-Key, autre corps
{ "error": { "code": "idempotency_key_reused", "message": "Cette clé d'idempotence a déjà servi pour une requête différente." } }Première requête de même Idempotency-Key encore en cours
{ "error": { "code": "idempotency_in_progress", "message": "Une requête avec cette clé d'idempotence est en cours : réessayez dans quelques instants." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Validation (
validation_failed, un détail par champ), champ inconnu (unknown_field), en-têteIdempotency-Keyinvalide.Domaine du titulaire qui n'est pas actif
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "domain", "code": "domain_not_usable", "message": "Ce domaine n'est pas disponible pour de nouveaux liens." } ] } }Destination refusée par lcrs.fr (1.5.0)
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "destination_url", "code": "destination_refused", "message": "Cette destination n'est pas acceptée par lcrs.fr." } ] } }Destination interdite et expiration passée
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "destination_url", "code": "destination_forbidden", "message": "Un lien court ne peut pas pointer vers lcrs.fr ni vers un autre lien court." }, { "field": "expires_at", "code": "expiration_past", "message": "La date d'expiration doit être dans le futur." } ] } }Slug réservé (login)
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "slug", "code": "slug_reserved", "message": "Ce slug est réservé." } ] } }Champ inconnu dans le corps (labels)
{ "error": { "code": "unknown_field", "message": "La requête contient un champ ou un paramètre inconnu.", "details": [ { "field": "labels", "code": "unknown_field", "message": "Ce champ ou paramètre n'est pas prévu par cette opération." } ] } }Clé de tag mal formée et valeur trop longue
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "tags.2026-campaign", "code": "tag_key_invalid", "message": "Une clé compte de 1 à 32 caractères : lettres non accentuées, chiffres, tiret et underscore, en commençant par une lettre." }, { "field": "tags.audience", "code": "tag_value_too_long", "message": "Une valeur compte de 1 à 100 caractères." } ] } }tags envoyé sous forme de tableau
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "tags", "code": "invalid_type", "message": "Ce champ doit être un objet JSON." } ] } }En-tête Idempotency-Key invalide
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "Idempotency-Key", "code": "invalid_idempotency_key", "message": "Indiquez de 1 à 255 caractères ASCII imprimables, sans espace au début ni à la fin." } ] } } - 429
-
Limite de l'offre atteinte (
quota_exceeded, avecdetails.quota—max_linksoumax_tags_per_link—,details.limitetdetails.upgrade_plan), plafond de créations du compte sur 24 heures (creation_limit_reached,details.retry_at, en-têteRetry-After, 1.5.0) ou limite de débit (rate_limited, en-têteRetry-After).Limite de liens de l'offre du titulaire atteinte
{ "error": { "code": "quota_exceeded", "message": "Limite de liens de l'offre Free atteinte.", "details": { "quota": "max_links", "limit": 100, "upgrade_plan": "Starter" } } }Plus de tags que la limite de l'offre du titulaire
{ "error": { "code": "quota_exceeded", "message": "Votre offre Free permet 1 tag par lien.", "details": { "quota": "max_tags_per_link", "limit": 1, "upgrade_plan": "Starter" } } }Plafond de créations sur 24 heures d'un compte récent (1.5.0)
{ "error": { "code": "creation_limit_reached", "message": "Votre compte a atteint son plafond de 50 liens créés sur 24 heures. Vous pourrez en créer de nouveau à partir de 10 h 14.", "details": { "retry_at": "2026-10-21T08:14:03Z" } } }Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Lire un lien
Paramètres
id(chemin, obligatoire) : Identifiant public du lien (lnk_…). Mal formé → 422 ; inconnu, supprimé ou d'un autre workspace → 404, contrôlé avant la portée de la clé (une cléread_onlyreçoit aussi 404 pour un lien introuvable). Exemple :lnk_1CfhK2qKFyECr9Rx5oPk7i.
Exemple de requête
curl "https://lcrs.fr/api/v1/links/lnk_1CfhK2qKFyECr9Rx5oPk7i" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…"
Réponse 200
Le lien.
{
"id": "lnk_1CfhK2qKFyECr9Rx5oPk7i",
"short_url": "https://lcrs.fr/black-friday",
"domain": {
"id": "dom_1CfhJmHgRwcnJGYzZgkkcf",
"name": "lcrs.fr"
},
"slug": "black-friday",
"title": "Black Friday 2026",
"destination_url": "https://exemple.fr/soldes/black-friday-2026",
"status": "blocked",
"expires_at": "2026-11-30T22:59:00Z",
"forward_query": false,
"click_count": 1284,
"last_clicked_at": "2026-10-04T12:02:11Z",
"created_at": "2026-09-28T08:15:00Z",
"updated_at": "2026-10-02T09:40:00Z",
"created_by": {
"type": "member",
"name": "Marie Dubois"
},
"tags": [],
"preview": {
"status": "pending",
"banner": null,
"title": null,
"description": null,
"image_url": null,
"read_at": null,
"failure": null
}
}
Erreurs
- 400
-
Requête rejetée par le serveur avant son traitement, par exemple un en-tête d'hôte invalide (
bad_request, message générique).Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Workspace en attente de suppression (
workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0) ; une lecture ne reçoit jamaisinsufficient_scope.Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 404
-
Lien inconnu, supprimé ou d'un autre workspace ; ou domaine désigné (
domain,idd'un domaine) inconnu, d'un autre titulaire ou retiré.Lien inconnu, supprimé ou d'un autre workspace
{ "error": { "code": "not_found", "message": "Lien introuvable." } }Domaine inconnu ou d'un autre titulaire
{ "error": { "code": "not_found", "message": "Domaine introuvable." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Identifiant mal formé (
validation_failed) ou paramètre de requête non prévu (unknown_field).Identifiant mal formé
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "id", "code": "invalid_id", "message": "L'identifiant doit être de la forme lnk_…." } ] } }Paramètre de requête non prévu (per-page)
{ "error": { "code": "unknown_field", "message": "La requête contient un champ ou un paramètre inconnu.", "details": [ { "field": "per-page", "code": "unknown_field", "message": "Ce champ ou paramètre n'est pas prévu par cette opération." } ] } } - 429
-
Limite de débit (
rate_limited, en-têteRetry-After) : par clé, ou après 100 échecs d'authentification en 15 minutes depuis une même IP.Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Modifier un lien
Modification partielle : seuls les champs fournis changent ; null efface title ou
expires_at et n'est accepté pour aucun autre champ (slug, destination_url,
forward_query ou status à null → 422 validation_failed). Changer slug retire définitivement l'ancien slug, qui cesse de rediriger
et ne sera jamais réattribué ; la réponse indique previous_slug. Une chaîne vide en
slug attribue un nouveau slug aléatoire. Changer la destination d'un lien bloqué
relance la vérification. Changer destination_url remet preview à pending, champs
vidés (1.4.0) : la carte de l'ancienne destination n'est plus servie.
Changer domain déplace le lien sur un autre domaine actif du titulaire (ou sur
lcrs.fr) : le slug est gardé s'il est libre et non réservé sur le nouveau domaine
(sinon, sauf si un nouveau slug est envoyé dans la même requête : 409 slug_taken avec
une suggestion pour un slug pris, 422 slug_reserved pour un slug réservé, par exemple
login déplacé vers lcrs.fr) ; l'ancienne adresse cesse de rediriger et n'est jamais réattribuée, et les QR
codes déjà diffusés ne fonctionnent plus ; la réponse indique previous_short_url.
Un domain égal au domaine actuel du lien n'est pas contrôlé : un lien sur un domaine qui
n'accepte plus de nouveaux liens (verification_lost, disabled, domaine d'un ancien
titulaire) reste modifiable.
Changer slug (chaîne vide comprise) crée un nouveau couple domaine et slug : c'est refusé
sur un domaine qui n'accepte plus de nouveaux liens → 422, champ slug, code
domain_not_usable.
Cas limites : nouveau domain non actif → 422 (domain_not_usable) ; nouveau domain
inconnu, d'un autre titulaire ou d'un ancien titulaire du workspace → 404 ; status: active sur un lien bloqué → 409 ; sur un lien en attente → activé
si l'auteur de la clé a une adresse confirmée, sinon 409 ; status: disabled sur un lien
en attente → 409 ; changer destination_url avec une clé dont l'auteur n'a plus
d'adresse confirmée, sur un lien qui n'est pas en attente → 409 link_state_conflict ; status: active sur un lien expiré → sans effet s'il est actif,
réactivé s'il est désactivé, mais toujours expired et sans redirection tant que
expires_at n'est pas repoussée ou effacée (null, dans la même requête au besoin) ;
status autre qu'active ou disabled → 422 ; objet vide {} →
lien inchangé ; corps sans aucun octet → 400 invalid_json. La modification est
atomique : une requête refusée ne change aucun champ.
Anti-abus (1.5.0) : nouvelle destination_url refusée par lcrs.fr → 422
(destination_url, destination_refused) ; pour un compte récent ou surveillé, un
changement de destination repasse le lien checking jusqu'à l'analyse de la nouvelle
destination ; status: active sur un lien checking ou under_review → 409
link_state_conflict ; compte du créateur de la clé ou workspace suspendu → 403
account_suspended ou workspace_suspended.
tags est fusionné avec les tags du lien : une clé avec une chaîne est ajoutée ou
remplacée, une clé à null est retirée (sans effet si le lien ne la porte pas), les clés
non envoyées sont conservées ; sans champ tags, les tags sont inchangés. Pour tout
retirer, envoyer chaque clé à null. Plus de tags après la fusion que la limite de
l'offre → 429 quota_exceeded, sauf si le nombre de tags n'augmente pas (lien au-delà de
la limite après un changement d'offre : il garde ses tags, on peut en changer ou en
retirer). Les clics passés gardent leurs tags d'origine.
Paramètres
id(chemin, obligatoire) : Identifiant public du lien (lnk_…). Mal formé → 422 ; inconnu, supprimé ou d'un autre workspace → 404, contrôlé avant la portée de la clé (une cléread_onlyreçoit aussi 404 pour un lien introuvable). Exemple :lnk_1CfhK2qKFyECr9Rx5oPk7i.
Corps de la requête
| Champ | Type | Description |
|---|---|---|
destination_url | texte | 2 048 caractères au plus. |
title | texte ou null | 100 caractères au plus. |
slug | texte | Nouveau slug ; chaîne vide pour un nouveau slug aléatoire. Refusé (422) s'il a la forme d'une clé API. |
expires_at | date ou null | |
forward_query | booléen | |
status | texte | Valeurs : active, disabled. |
tags | objet | Fusion avec les tags du lien : chaîne = ajout ou remplacement, null = retrait. |
domain | texte | Nouveau domaine : identifiant dom_… ou nom (1.3.0). 253 caractères au plus. |
Exemple de requête
curl -X PATCH "https://lcrs.fr/api/v1/links/lnk_1CfhK2qKFyECr9Rx5oPk7i" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…" \
-H "Content-Type: application/json" \
-d '{"destination_url":"https://exemple.fr/soldes/2027","status":"active","tags":{"campaign":"soldes-2027","channel":null}}'
Réponse 200
Lien à jour.
{
"id": "lnk_1CfhK2qKFyECr9Rx5oPk7i",
"short_url": "https://lcrs.fr/soldes-2027",
"domain": {
"id": "dom_1CfhJmHgRwcnJGYzZgkkcf",
"name": "lcrs.fr"
},
"slug": "soldes-2027",
"title": "Black Friday 2026",
"destination_url": "https://exemple.fr/soldes/2027",
"status": "active",
"expires_at": null,
"forward_query": false,
"click_count": 1284,
"last_clicked_at": "2026-10-04T12:02:11Z",
"created_at": "2026-09-28T08:15:00Z",
"updated_at": "2026-10-04T16:20:00Z",
"created_by": {
"type": "api_key",
"name": "Site e-commerce",
"key_prefix": "7Qk2Ab9x"
},
"tags": {
"audience": "pro",
"campaign": "soldes-2027"
},
"preview": {
"status": "pending",
"banner": null,
"title": null,
"description": null,
"image_url": null,
"read_at": null,
"failure": null
},
"previous_slug": "black-friday",
"previous_short_url": "https://lcrs.fr/black-friday"
}
Erreurs
- 400
-
Requête mal formée : JSON invalide (
invalid_json), type de contenu absent ou autre queapplication/json(invalid_content_type), ou requête rejetée avant son traitement (bad_request).Corps qui n'est pas un objet JSON valide
{ "error": { "code": "invalid_json", "message": "Le corps de la requête n'est pas du JSON valide." } }Corps envoyé sans le type application/json
{ "error": { "code": "invalid_content_type", "message": "Envoyez le corps de la requête en Content-Type: application/json." } }Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Portée insuffisante (
insufficient_scope), workspace en attente de suppression (workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0).Écriture avec une clé en lecture seule
{ "error": { "code": "insufficient_scope", "message": "Cette clé est en lecture seule." } }Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 404
-
Lien inconnu, supprimé ou d'un autre workspace ; ou domaine désigné (
domain,idd'un domaine) inconnu, d'un autre titulaire ou retiré.Lien inconnu, supprimé ou d'un autre workspace
{ "error": { "code": "not_found", "message": "Lien introuvable." } }Domaine inconnu ou d'un autre titulaire
{ "error": { "code": "not_found", "message": "Domaine introuvable." } } - 409
-
Slug pris (
slug_taken, avecdetails.suggestion) ou état incompatible (link_state_conflict).Slug déjà pris, même en ignorant la casse
{ "error": { "code": "slug_taken", "message": "Ce lien court est déjà pris.", "details": { "suggestion": "black-friday-2" } } }Réactivation d'un lien bloqué
{ "error": { "code": "link_state_conflict", "message": "Ce lien est bloqué pour raison de sécurité." } }Activation d'un lien en examen ou en vérification (1.5.0)
{ "error": { "code": "link_state_conflict", "message": "Ce lien est en examen par l'équipe lcrs.fr : il ne peut pas être activé." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Identifiant mal formé, validation (
validation_failed, un détail par champ) ou champ inconnu (unknown_field).Domaine du titulaire qui n'est pas actif
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "domain", "code": "domain_not_usable", "message": "Ce domaine n'est pas disponible pour de nouveaux liens." } ] } }Identifiant mal formé
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "id", "code": "invalid_id", "message": "L'identifiant doit être de la forme lnk_…." } ] } }Slug réservé (login)
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "slug", "code": "slug_reserved", "message": "Ce slug est réservé." } ] } }Valeur null pour un champ qui ne l'accepte pas
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "destination_url", "code": "invalid_type", "message": "Ce champ ne peut pas être null." } ] } }Statut demandé autre que active ou disabled
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "status", "code": "invalid_choice", "message": "Le statut demandé doit être active ou disabled." } ] } }Champ inconnu dans le corps (labels)
{ "error": { "code": "unknown_field", "message": "La requête contient un champ ou un paramètre inconnu.", "details": [ { "field": "labels", "code": "unknown_field", "message": "Ce champ ou paramètre n'est pas prévu par cette opération." } ] } }Deux clés identiques à la casse près
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "tags.Campaign", "code": "tag_key_duplicate", "message": "Cette clé figure déjà dans les tags (les clés ne tiennent pas compte de la casse)." } ] } }Nouvelle destination refusée par lcrs.fr (1.5.0)
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "destination_url", "code": "destination_refused", "message": "Cette destination n'est pas acceptée par lcrs.fr." } ] } } - 429
-
Plus de tags après la fusion que la limite de l'offre (
quota_exceeded, avecdetails.quotaàmax_tags_per_link) ou limite de débit (rate_limited, en-têteRetry-After).Plus de tags que la limite de l'offre du titulaire
{ "error": { "code": "quota_exceeded", "message": "Votre offre Starter permet 20 tags par lien.", "details": { "quota": "max_tags_per_link", "limit": 20, "upgrade_plan": null } } }Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Supprimer un lien
Suppression logique : le lien ne redirige plus, son slug n'est jamais réattribué, ses clics restent comptés, il ne compte plus dans la limite de l'offre. Une seconde suppression rend 404. Un corps éventuel est ignoré jusqu'à 1 Mio (au-delà : 413 du serveur web).
Paramètres
id(chemin, obligatoire) : Identifiant public du lien (lnk_…). Mal formé → 422 ; inconnu, supprimé ou d'un autre workspace → 404, contrôlé avant la portée de la clé (une cléread_onlyreçoit aussi 404 pour un lien introuvable). Exemple :lnk_1CfhK2qKFyECr9Rx5oPk7i.
Exemple de requête
curl -X DELETE "https://lcrs.fr/api/v1/links/lnk_1CfhK2qKFyECr9Rx5oPk7i" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…"
Réponse 204
Lien supprimé (aucun corps).
Erreurs
- 400
-
Requête rejetée par le serveur avant son traitement, par exemple un en-tête d'hôte invalide (
bad_request, message générique).Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Portée insuffisante (
insufficient_scope), workspace en attente de suppression (workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0).Écriture avec une clé en lecture seule
{ "error": { "code": "insufficient_scope", "message": "Cette clé est en lecture seule." } }Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 404
-
Lien inconnu, supprimé ou d'un autre workspace ; ou domaine désigné (
domain,idd'un domaine) inconnu, d'un autre titulaire ou retiré.Lien inconnu, supprimé ou d'un autre workspace
{ "error": { "code": "not_found", "message": "Lien introuvable." } }Domaine inconnu ou d'un autre titulaire
{ "error": { "code": "not_found", "message": "Domaine introuvable." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Identifiant mal formé (
validation_failed) ou paramètre de requête non prévu (unknown_field).Identifiant mal formé
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "id", "code": "invalid_id", "message": "L'identifiant doit être de la forme lnk_…." } ] } }Paramètre de requête non prévu (per-page)
{ "error": { "code": "unknown_field", "message": "La requête contient un champ ou un paramètre inconnu.", "details": [ { "field": "per-page", "code": "unknown_field", "message": "Ce champ ou paramètre n'est pas prévu par cette opération." } ] } } - 429
-
Limite de débit (
rate_limited, en-têteRetry-After) : par clé, ou après 100 échecs d'authentification en 15 minutes depuis une même IP.Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Lister les tags du workspace
Couples clé et valeur portés par les liens non supprimés du workspace de la clé, avec le
nombre de liens qui portent chacun. Rien n'est à déclarer : un couple apparaît dès qu'un
lien le porte et disparaît quand plus aucun ne le porte. Ordre : clés par ordre
alphabétique, puis valeurs par nombre de liens décroissant, puis par ordre alphabétique.
Permis aux clés read_only. Pour obtenir les liens d'un couple : listLinks avec
tag[<clé>]=<valeur>.
Cas limites : key invalide → 422 ; per_page=500 → 422 ; workspace sans tag → liste
vide.
Paramètres
page(requête) : Numéro de page, à partir de 1 ; hors bornes ou non numérique → 422. Exemple :1.per_page(requête) : Taille de page, de 1 à 100 ; hors bornes ou non numérique → 422. Exemple :20.key(requête) : Limite la liste aux valeurs d'une clé (normalisée en minuscules). Exemple :campaign.
Exemple de requête
curl "https://lcrs.fr/api/v1/tags?page=1&per_page=20" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…"
Réponse 200
Page de couples clé et valeur.
{
"data": [
{
"key": "campaign",
"value": "black-friday",
"link_count": 12
},
{
"key": "campaign",
"value": "summer-sale",
"link_count": 3
},
{
"key": "channel",
"value": "Email",
"link_count": 8
},
{
"key": "channel",
"value": "email",
"link_count": 5
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 4
}
}
Erreurs
- 400
-
Requête rejetée par le serveur avant son traitement, par exemple un en-tête d'hôte invalide (
bad_request, message générique).Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Workspace en attente de suppression (
workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0) ; une lecture ne reçoit jamaisinsufficient_scope.Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Pagination ou clé invalides (
validation_failed), paramètre non prévu (unknown_field).Clé mal formée
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "key", "code": "tag_key_invalid", "message": "Une clé compte de 1 à 32 caractères : lettres non accentuées, chiffres, tiret et underscore, en commençant par une lettre." } ] } }Pagination hors bornes (per_page=500)
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "per_page", "code": "out_of_range", "message": "La taille de page doit être comprise entre 1 et 100." } ] } } - 429
-
Limite de débit (
rate_limited, en-têteRetry-After) : par clé, ou après 100 échecs d'authentification en 15 minutes depuis une même IP.Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Lire les statistiques du workspace
Clics du workspace de la clé sur une période, éventuellement filtrés et regroupés. Les
chiffres sont ceux du tableau de bord pour la même période et les mêmes filtres.
- Clics comptés : les clics des robots d'aperçu (messageries, réseaux sociaux) ne
sont jamais comptés ; leur nombre est rendu à part (bot_clicks).
- Fuseau : jours et heures découpés selon l'heure de Paris (Europe/Paris), pour tous
les workspaces. Un jour est rendu en date calendaire AAAA-MM-JJ de Paris ; une heure,
par l'instant UTC de son début (le jour du retour à l'heure d'hiver compte 25 heures).
period donne les deux formes : jours de Paris (from, to) et instants UTC
(start, end).
- Période : period (défaut 30d, ou 7d quand l'historique de l'offre compte moins
de 30 jours), ou from et to (jours de Paris, to inclus,
366 jours au plus), dans l'historique de l'offre du titulaire (Free 30 jours, Starter
365, Pro et Business sans limite). 24h : les 24 heures pleines qui précèdent l'heure
en cours, et l'heure en cours (25 lignes avec group_by=hour). 7d, 30d, 90d et
12m : 7, 30, 90 et 365 jours de Paris, aujourd'hui inclus (12m compte toujours
365 jours, année bissextile comprise).
- Tags : les filtres et le regroupement tag:<clé> portent sur les tags du lien à
l'instant de chaque clic, jamais sur ses tags actuels. value: null regroupe les
clics dont l'instantané ne portait pas la clé.
- Sans group_by : total (AnalyticsSummary). Avec group_by : collection
paginée (AnalyticsCollection), chaque jour ou heure de la période présent même à 0.
- Accès : offre Starter et au-delà (sinon 403 plan_feature_unavailable) ; clés de
toute portée. Quand la consommation du mois du titulaire dépasse la limite de son
offre, les statistiques sont suspendues jusqu'au 1er du mois suivant : 429
quota_exceeded (details.quota à max_clicks_per_month, Retry-After jusqu'au
1er du mois à 0 h, heure de Paris). Les liens continuent de rediriger et les clics
d'être comptés.
- Ordre des contrôles : paramètres (422, lien d'un autre workspace → 404, puis
paramètre non prévu → 422 unknown_field, comme pour les autres opérations), puis offre
(403 plan_feature_unavailable), puis quota du mois (429 quota_exceeded). Une clé
Free dont le quota est dépassé reçoit donc 403 ; un filtre mal formé donne 422 même en
Free.
Cas limites : from avant l'historique de l'offre → 422 period_out_of_history ;
group_by=hour sur une période qui commence plus de 30 jours avant aujourd'hui (au-delà
des 31 derniers jours, aujourd'hui compris) → 422 hour_period_too_old ; group_by=day avec period=24h → 422 invalid_group_by
(deux jours de Paris incomplets : regrouper par heure) ; country=ZZ (code hors
ISO 3166-1) ou device=bot → 422 invalid_choice ; tag[campaign] répété → 422
tag_filter_conflict ; link mal formé → 422 invalid_id, plus de 20 liens → 422
too_many_links, lien d'un autre workspace → 404 ; sort, page ou per_page sans
group_by → 422 not_applicable ; page ou per_page hors bornes → 422
out_of_range ; avec device, bot_clicks vaut toujours 0 ; workspace sans clic →
clicks: 0, jours à 0.
Paramètres
period(requête) : Période nommée ; exclusive avecfrom/to. Par défaut30d, ou7dquand l'historique de l'offre compte moins de 30 jours (il en compte toujours au moins 7) : une demande sans période n'est jamais refusée pour l'historique. Exemple :30d.from(requête) : Premier jour (Paris), avecto. Exemple :2026-09-01.to(requête) : Dernier jour inclus (Paris), au plus aujourd'hui, avecfrom. Exemple :2026-09-30.tag(requête) : Filtres de tagstag[<clé>]=<valeur>(1 à 5, une valeur par clé, combinés en ET), sur l'instantané des tags de chaque clic. Exemple :{ "campaign": "black-friday", "channel": "email" }.link(requête) : Liens du workspace (lnk_…, 1 à 20, séparés par des virgules), supprimés compris. Identifiant mal formé → 422invalid_id; plus de 20 liens → 422too_many_links; lien inconnu ou d'un autre workspace → 404. Exemple :lnk_4Vq9cE2mK7pW3xT8yR1nB,lnk_9Hd2Lm5Qx8Wz3Ty6Rc1Pv.country(requête) : Pays (code ISO 3166-1 alpha-2 attribué), ouXXpour inconnu ; tout autre code → 422invalid_choice. Exemple :FR.device(requête) : Type d'appareil ;bot→ 422invalid_choice(les robots ne sont jamais comptés). Exemple :mobile.referrer(requête) : Domaine d'origine tel qu'enregistré (t.co,www.google.com), ou(direct)pour les clics sans origine. Exemple :t.co.group_by(requête) : Regroupement :day(refusé avecperiod=24h),hour(31 derniers jours, aujourd'hui compris),link,country,device,referrer, outag:<clé>. La clé est ramenée en minuscules, comme celle des filtrestag[<clé>](tag:Campaignéquivaut àtag:campaign). Exemple :tag:campaign.sort(requête) : Ordre des lignes (avecgroup_by). Pourlink,country,device,referrerettag:<clé>:-clicks(défaut),clicks,label,-label;timeet-timey sont refusés (422invalid_sort). Pourdayethour, toujours chronologique :time(défaut) ou-time;clicks,-clicks,labelet-labely sont refusés (422invalid_sort). Par nombre de clics (clickscomme-clicks), les ex aequo sont départagés par libellé croissant. Par libellé : le libellé du tableau de bord, et nonvalue(nom français du pays, « Inconnu » pourXX; appareil en français ; titre du lien, à défaut son lien court ; hôte ; valeur du tag), dans l'ordre alphabétique sans casse ni accents, valeursnull,XX(pays) etunknown(appareil) en fin de liste aveclabelcomme avec-label. À libellé égal (deux liens de même titre,Emailetemail), départage final parvaluecomparée octet par octet, dans le sens du libellé (croissant, sauf avec-label) : l'ordre est unique et stable d'une page à l'autre. Sansgroup_by→ 422not_applicable. Exemple :group_by=country&sort=labelrendDE(Allemagne),BE(Belgique),FR(France). Exemple :label.page(requête) : Numéro de page, à partir de 1 ; hors bornes ou non numérique → 422. Exemple :1.per_page(requête) : Taille de page, de 1 à 100 ; hors bornes ou non numérique → 422. Exemple :20.
Exemple de requête
curl "https://lcrs.fr/api/v1/analytics?page=1&per_page=20" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…"
Réponse 200
Total de la période (sans `group_by`) ou page de lignes (avec `group_by`).
Erreurs
- 400
-
Requête rejetée par le serveur avant son traitement, par exemple un en-tête d'hôte invalide (
bad_request, message générique).Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Offre sans statistiques par l'API (
plan_feature_unavailable) ou workspace en attente de suppression (workspace_pending_deletion).Offre Free
{ "error": { "code": "plan_feature_unavailable", "message": "Votre offre Free ne comprend pas l'API des statistiques. Passez à l'offre Starter pour y accéder.", "details": { "feature": "analytics_api", "upgrade_plan": "Starter" } } }Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } } - 404
-
Lien inconnu, supprimé ou d'un autre workspace ; ou domaine désigné (
domain,idd'un domaine) inconnu, d'un autre titulaire ou retiré.Lien inconnu, supprimé ou d'un autre workspace
{ "error": { "code": "not_found", "message": "Lien introuvable." } }Domaine inconnu ou d'un autre titulaire
{ "error": { "code": "not_found", "message": "Domaine introuvable." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Période, filtre, regroupement, tri ou pagination invalides (
validation_failed, un détail par paramètre), paramètre non prévu (unknown_field).Début avant l'historique de l'offre
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "from", "code": "period_out_of_history", "message": "Votre offre Starter conserve 365 jours d'historique : choisissez un début à partir du 2025-10-08." } ] } }Regroupement par heure au-delà des 31 derniers jours
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "group_by", "code": "hour_period_too_old", "message": "Le regroupement par heure couvre les 31 derniers jours, aujourd'hui compris : choisissez un début à partir du 2026-09-07." } ] } }Pagination sans regroupement
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "page", "code": "not_applicable", "message": "La pagination ne s'applique qu'avec group_by." } ] } }period avec from et to
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "period", "code": "invalid_period", "message": "Indiquez soit period, soit from et to." } ] } }Même clé de tag deux fois
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "tag", "code": "tag_filter_conflict", "message": "Le filtre sur la clé campaign figure deux fois." } ] } }Regroupement inconnu
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "group_by", "code": "invalid_group_by", "message": "Regroupement inconnu : day, hour, link, country, device, referrer ou tag:<clé>." } ] } }Pays inconnu
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "country", "code": "invalid_choice", "message": "Pays inconnu : code ISO 3166-1 alpha-2 (FR, BE…) ou XX." } ] } }Tri par clics sur un regroupement par jour
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "sort", "code": "invalid_sort", "message": "Un regroupement par jour ou par heure se trie par date : time ou -time." } ] } }Plus de 20 liens dans le filtre link
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "link", "code": "too_many_links", "message": "Indiquez 20 liens au plus." } ] } }Identifiant de lien mal formé dans le filtre link
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "link", "code": "invalid_id", "message": "L'identifiant doit être de la forme lnk_…." } ] } } - 429
-
Statistiques suspendues pour le mois (
quota_exceeded,details.quotaàmax_clicks_per_month,Retry-Afterjusqu'au 1er du mois suivant à 0 h, heure de Paris) ou limite de débit (rate_limited,Retry-After).Clics du mois au-delà de la limite de l'offre
{ "error": { "code": "quota_exceeded", "message": "Limite de 100 000 clics du mois de l'offre Starter dépassée : statistiques suspendues jusqu'au 1er novembre. Passez à l'offre Pro pour continuer à les suivre.", "details": { "quota": "max_clicks_per_month", "limit": 100000, "upgrade_plan": "Pro" } } }Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Obtenir le QR code d'un lien
Image du QR code de l'URL courte du lien (short_url, sans paramètre), niveau de
correction M, noir sur blanc, marge d'au moins 4 modules. Le code reste valable tant que le lien
garde son domaine et son slug : changer la destination ne le change pas. Toute portée de
clé. Réponse mise en cache 5 minutes par le client (ETag, 304 sur If-None-Match).
Cas limites : lien désactivé, expiré ou en attente → 200 (le code mène à une page
introuvable tant que le lien ne redirige pas) ; lien bloqué → 409 link_blocked ;
size avec format=svg → 422 (not_applicable) ; size hors de la liste, ou trop
petite pour garder 4 pixels par module avec l'URL de ce lien (URL très longue) → 422
(invalid_choice, le message donne la plus petite taille permise) ; sans size, 512, ou
la plus petite taille permise si 512 ne l'est pas.
Représentation binaire admise par la constitution 1.5.0 (principe I) : sous-ressource
unique, au singulier, erreurs au format JSON standard.
Paramètres
id(chemin, obligatoire) : Identifiant public du lien (lnk_…). Mal formé → 422 ; inconnu, supprimé ou d'un autre workspace → 404. Exemple :lnk_1CfhK2qKFyECr9Rx5oPk7i.format(requête) : Format de l'image. Exemple :svg.size(requête) : Côté de l'image PNG en pixels. Sanssize: 512, ou la plus petite taille permise si 512 ne garde pas 4 pixels par module pour l'URL de ce lien. Exemple :1024.
Exemple de requête
curl "https://lcrs.fr/api/v1/links/lnk_1CfhK2qKFyECr9Rx5oPk7i/qr-code" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…"
Réponse 200
Image du QR code.
Type de contenu : image/png ou image/svg+xml.
Réponse 304
Image inchangée (If-None-Match).
Erreurs
- 400
-
Requête rejetée par le serveur avant son traitement, par exemple un en-tête d'hôte invalide (
bad_request, message générique).Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Workspace en attente de suppression (
workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0) ; une lecture ne reçoit jamaisinsufficient_scope.Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 404
-
Lien inconnu, supprimé ou d'un autre workspace ; ou domaine désigné (
domain,idd'un domaine) inconnu, d'un autre titulaire ou retiré.Lien inconnu, supprimé ou d'un autre workspace
{ "error": { "code": "not_found", "message": "Lien introuvable." } }Domaine inconnu ou d'un autre titulaire
{ "error": { "code": "not_found", "message": "Domaine introuvable." } } - 409
-
Lien bloqué pour destination dangereuse (
link_blocked).Lien bloqué
{ "error": { "code": "link_blocked", "message": "Ce lien est bloqué pour raison de sécurité : son QR code n'est pas disponible." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Identifiant mal formé,
formatousizeinvalides (validation_failed), ou paramètre non prévu (unknown_field).Taille hors de la liste
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "size", "code": "invalid_choice", "message": "Choisissez 256, 512, 1024 ou 2048." } ] } }Taille trop petite pour l'URL de ce lien
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "size", "code": "invalid_choice", "message": "Pour ce lien, choisissez au moins 512." } ] } }Taille avec le format SVG
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "size", "code": "not_applicable", "message": "La taille ne s'applique qu'au format PNG." } ] } } - 429
-
Limite de débit (
rate_limited, en-têteRetry-After) : par clé, ou après 100 échecs d'authentification en 15 minutes depuis une même IP.Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Lister les domaines utilisables par le workspace
Le domaine du service (lcrs.fr, toujours en tête) puis les domaines non retirés du
titulaire du workspace, par nom. Seuls les domaines active acceptent de nouveaux liens ;
un domaine verification_lost redirige encore pendant son délai de grâce. Les demandes
en attente de vérification, la preuve de possession et l'adresse de racine ne sont pas
exposées : la gestion des domaines se fait dans le dashboard du titulaire.
Cas limites : per_page=500 ou page=0 → 422 ; paramètre non prévu → 422
unknown_field.
Paramètres
page(requête) : Numéro de page, à partir de 1 ; hors bornes ou non numérique → 422. Exemple :1.per_page(requête) : Taille de page, de 1 à 100 ; hors bornes ou non numérique → 422. Exemple :20.
Exemple de requête
curl "https://lcrs.fr/api/v1/domains?page=1&per_page=20" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…"
Réponse 200
Page de domaines.
{
"data": [
{
"id": "dom_1CfhJmHgRwcnJGYzZgkkcf",
"name": "lcrs.fr",
"display_name": "lcrs.fr",
"status": "active",
"is_service_domain": true,
"is_workspace_default": false,
"created_at": "2026-10-03T00:00:00Z"
},
{
"id": "dom_1CfhK2qKFyECr9Rx5oPk7j",
"name": "go.acme.fr",
"display_name": "go.acme.fr",
"status": "active",
"is_service_domain": false,
"is_workspace_default": true,
"created_at": "2026-10-11T09:12:00Z"
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 2
}
}
Erreurs
- 400
-
Requête rejetée par le serveur avant son traitement, par exemple un en-tête d'hôte invalide (
bad_request, message générique).Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Workspace en attente de suppression (
workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0) ; une lecture ne reçoit jamaisinsufficient_scope.Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Pagination ou filtre invalides (
validation_failed, un détail par paramètre), paramètre non prévu (unknown_field).Pagination hors bornes (per_page=500)
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "per_page", "code": "out_of_range", "message": "La taille de page doit être comprise entre 1 et 100." } ] } }Statut de filtre inconnu
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "status", "code": "invalid_choice", "message": "Statut inconnu." } ] } }Filtre slug mal formé
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "slug", "code": "slug_invalid", "message": "Le slug compte de 3 à 64 caractères : lettres non accentuées, chiffres, tiret et underscore." } ] } }Paramètre de requête non prévu (per-page)
{ "error": { "code": "unknown_field", "message": "La requête contient un champ ou un paramètre inconnu.", "details": [ { "field": "per-page", "code": "unknown_field", "message": "Ce champ ou paramètre n'est pas prévu par cette opération." } ] } }Plus de 5 filtres de tags
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "tag", "code": "too_many_tag_filters", "message": "Filtrez sur 5 clés de tags au plus." } ] } }untagged avec un autre filtre de tag
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "untagged", "code": "tag_filter_conflict", "message": "Le filtre untagged ne se combine pas avec un autre filtre de tag." } ] } } - 429
-
Limite de débit (
rate_limited, en-têteRetry-After) : par clé, ou après 100 échecs d'authentification en 15 minutes depuis une même IP.Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }
Lire un domaine
Un domaine utilisable par le workspace de la clé : le domaine du service, ou un domaine non
retiré du titulaire du workspace. Mêmes champs que dans listDomains.
Cas limites : identifiant mal formé (dom-…, lnk_…) → 422 ; domaine inconnu, retiré,
d'un autre titulaire, ou demande d'ajout encore en attente (elle n'a pas d'identifiant
dom_) → 404 ; paramètre de requête → 422 unknown_field. Un lien resté sur le domaine
d'un ancien titulaire du workspace expose domain.id, mais getDomain sur cet identifiant
rend 404 : ce domaine n'est plus utilisable par le workspace.
Paramètres
id(chemin, obligatoire) : Identifiant public du domaine (dom_…). Mal formé → 422 ; inconnu, retiré ou d'un autre titulaire → 404. Exemple :dom_1CfhK2qKFyECr9Rx5oPk7j.
Exemple de requête
curl "https://lcrs.fr/api/v1/domains/dom_1CfhK2qKFyECr9Rx5oPk7j" \
-H "Authorization: Bearer lcrs_7Qk2Ab9x_…"
Réponse 200
Le domaine.
{
"id": "dom_1CfhK2qKFyECr9Rx5oPk7j",
"name": "liens.xn--caf-dma.fr",
"display_name": "liens.café.fr",
"status": "verification_lost",
"is_service_domain": false,
"is_workspace_default": false,
"created_at": "2026-10-11T09:12:00Z"
}
Erreurs
- 400
-
Requête rejetée par le serveur avant son traitement, par exemple un en-tête d'hôte invalide (
bad_request, message générique).Requête rejetée avant son traitement (en-tête d'hôte invalide…)
{ "error": { "code": "bad_request", "message": "La requête n'a pas pu être interprétée." } } - 401
-
Clé absente, invalide ou révoquée.
Clé absente, invalide ou placée dans l'URL
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } }Clé révoquée (même réponse que toute autre clé refusée)
{ "error": { "code": "unauthenticated", "message": "Clé API absente, invalide ou révoquée." } } - 403
-
Workspace en attente de suppression (
workspace_pending_deletion), compte du créateur de la clé ou workspace suspendu (account_suspended,workspace_suspended, 1.5.0) ; une lecture ne reçoit jamaisinsufficient_scope.Workspace en attente de suppression
{ "error": { "code": "workspace_pending_deletion", "message": "Ce workspace est en attente de suppression : ses clés API sont suspendues jusqu'à sa restauration." } }Compte du créateur de la clé suspendu (1.5.0)
{ "error": { "code": "account_suspended", "message": "Le compte qui a créé cette clé est suspendu par l'équipe lcrs.fr : la clé est refusée tant que la suspension dure." } }Workspace suspendu (1.5.0)
{ "error": { "code": "workspace_suspended", "message": "Ce workspace est suspendu par l'équipe lcrs.fr : ses clés API sont refusées tant que la suspension dure." } } - 404
-
Lien inconnu, supprimé ou d'un autre workspace ; ou domaine désigné (
domain,idd'un domaine) inconnu, d'un autre titulaire ou retiré.Lien inconnu, supprimé ou d'un autre workspace
{ "error": { "code": "not_found", "message": "Lien introuvable." } }Domaine inconnu ou d'un autre titulaire
{ "error": { "code": "not_found", "message": "Domaine introuvable." } } - 413
-
Corps de requête de plus de 16 Kio (
payload_too_large). Au-delà de 1 Mio, quelle que soit l'opération, la réponse vient du serveur web, au même format, sans en-têtesX-RateLimit-*niX-Request-Id.Corps de plus de 16 Kio
{ "error": { "code": "payload_too_large", "message": "Le corps de la requête dépasse 16 Kio." } } - 422
-
Identifiant de domaine mal formé (
validation_failed) ou paramètre de requête non prévu (unknown_field).Identifiant de domaine mal formé (getDomain)
{ "error": { "code": "validation_failed", "message": "La requête contient des valeurs invalides.", "details": [ { "field": "id", "code": "invalid_id", "message": "L'identifiant doit être de la forme dom_…." } ] } }Paramètre de requête non prévu (per-page)
{ "error": { "code": "unknown_field", "message": "La requête contient un champ ou un paramètre inconnu.", "details": [ { "field": "per-page", "code": "unknown_field", "message": "Ce champ ou paramètre n'est pas prévu par cette opération." } ] } } - 429
-
Limite de débit (
rate_limited, en-têteRetry-After) : par clé, ou après 100 échecs d'authentification en 15 minutes depuis une même IP.Limite de débit de la clé dépassée (en-tête Retry-After)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 12 secondes." } }Trop d'échecs d'authentification depuis cette adresse IP (sans en-têtes X-RateLimit)
{ "error": { "code": "rate_limited", "message": "Trop de requêtes : réessayez dans 540 secondes." } } - 500
-
Erreur serveur (
internal_error), avec l'identifiant de corrélationrequest_id, aussi rendu dans l'en-têteX-Request-Id.Erreur serveur, avec l'identifiant de corrélation
{ "error": { "code": "internal_error", "message": "Une erreur interne est survenue. Communiquez cet identifiant au support.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } } - 503
-
Service momentanément indisponible (
service_unavailable), avecrequest_id; réessayer plus tard. Si l'application ne répond pas (arrêt, saturation, délai dépassé), la réponse vient du serveur web, au même format, avecrequest_idetX-Request-Id, sans en-têtesX-RateLimit-*.Service momentanément indisponible
{ "error": { "code": "service_unavailable", "message": "Le service est momentanément indisponible. Réessayez dans quelques instants.", "request_id": "0192f1c4-7b3a-7e21-9c55-3f0e8d2a6b41" } }