Billing

Les deux routes qui font le lien entre un compte Atlas et Stripe : ouvrir une session de paiement pour un palier payant, ou obtenir un lien vers le Customer Portal pour gérer un abonnement déjà actif.

Aucune de ces deux routes ne s'utilise avec une clé API. POST /api/billing/checkout exige une session compte (cookie posé par /compte/) accompagnée d'un jeton CSRF — impossible à obtenir depuis un client API externe, seulement depuis le front Atlas connecté. POST /api/billing/portal n'exige ni session ni clé : elle s'authentifie par preuve de possession de l'email (un lien magique envoyé par courriel), ce qui la rend en pratique appelable depuis n'importe quel client — mais elle existe avant tout pour le flux /compte/, pas comme point d'entrée d'intégration. La seule route de facturation réellement pensée pour une clé API est GET /api/account/key-tier, documentée sur la page « Compte & clés API ».
POST/api/billing/checkout

Crée une Checkout Session Stripe pour un palier et un cycle de facturation donnés, et renvoie son URL. Requiert une session compte depuis le 18/08/2026 — l'email facturé vient de la session, jamais du corps de la requête, pour empêcher de payer au nom d'un tiers.

Session compte + en-tête X-Atlas-CSRF obligatoires (require_session/require_csrf). Seule la présence de cet en-tête est vérifiée, jamais sa valeur. Sans compte connecté côté front, cette route renvoie 401/403 avant même de valider le corps de la requête.
Paramètre
Type
Requis
Défaut
Description
tier
string (corps JSON)
Oui
Palier visé (ex. starter, pro, business).
cycle
string (corps JSON)
Oui
Cycle de facturation (ex. monthly).
cgv_accepted
bool (corps JSON)
Non
false
Doit être true pour que la session Stripe soit créée.
lang
string ou null (corps JSON)
Non
null
Langue i18n active côté front au moment du checkout. Une valeur absente ou non supportée retombe sur le français, jamais une 5e langue devinée.
Code
Erreur
Cas
429
checkout_too_many_attempts
Au-delà de 5 tentatives/minute par IP.
400
cgv_required
cgv_accepted est false — « l'acceptation des CGV est obligatoire pour toute commande ».
400
education_email_required
tier == "education" et l'email de la session n'est pas reconnu comme email éducation.
400
(message brut)
Combinaison palier/cycle inconnue — ex. « combinaison palier/cycle inconnue : bidon/monthly ».
401
account_session_required
Pas de session compte valide (cookie atlas_session absent ou expiré).
403
account_csrf_required
En-tête X-Atlas-CSRF absent — seule son absence déclenche l'erreur, sa valeur n'est jamais vérifiée.
CURL
curl -X POST "https://atlasfeed.fr/api/billing/checkout" \
  -H "Content-Type: application/json" \
  -H "Cookie: atlas_session=..." \
  -H "X-Atlas-CSRF: 1" \
  -d '{"tier": "pro", "cycle": "monthly", "cgv_accepted": true, "lang": "fr"}'
PYTHON
import requests

# session + jeton CSRF obtenus au préalable via le flux /compte/,
# pas construits par un script indépendant.
resp = requests.post(
    "https://atlasfeed.fr/api/billing/checkout",
    cookies={"atlas_session": "..."},
    headers={"X-Atlas-CSRF": "1"},
    json={"tier": "pro", "cycle": "monthly", "cgv_accepted": True, "lang": "fr"},
)
resp.raise_for_status()
print(resp.json())
JAVASCRIPT
const resp = await fetch("https://atlasfeed.fr/api/billing/checkout", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "X-Atlas-CSRF": "1",
  },
  body: JSON.stringify({ tier: "pro", cycle: "monthly", cgv_accepted: true, lang: "fr" }),
});
const data = await resp.json();
RÉPONSE (succès)
{"url": "https://checkout.stripe.com/c/pay/..."}
POST/api/billing/portal

Envoie un lien de connexion signé à usage unique (valable 15 minutes) vers le Customer Portal Stripe, si l'email correspond à un client existant. La réponse est volontairement identique que l'email corresponde ou non, pour empêcher un tiers de vérifier par tâtonnement quels emails sont clients d'Atlas.

Réponse 202 systématique, sans jamais révéler si l'email est réellement associé à un abonnement.
Paramètre
Type
Requis
Défaut
Description
email
string (corps JSON)
Oui
Email à vérifier contre les clients Stripe existants.
Code
Erreur
Cas
429
too_many_attempts_minute
Au-delà de 5 tentatives/minute par IP.
429
too_many_requests_for_email
Au-delà de 3 demandes/heure pour le même email.
CURL
curl -X POST "https://atlasfeed.fr/api/billing/portal" \
  -H "Content-Type: application/json" \
  -d '{"email": "client@exemple.fr"}'
PYTHON
import requests

resp = requests.post(
    "https://atlasfeed.fr/api/billing/portal",
    json={"email": "client@exemple.fr"},
)
print(resp.status_code, resp.json())
JAVASCRIPT
const resp = await fetch("https://atlasfeed.fr/api/billing/portal", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "client@exemple.fr" }),
});
const data = await resp.json();
RÉPONSE (202)
{"message": "Si cet email correspond à un compte Atlas, un lien de connexion vient de lui être envoyé (valable 15 minutes, à usage unique)."}

Hors détail

Deux routes du même groupe existent mais ne sont pas des points d'entrée pour un client API — elles ne sont mentionnées ici que pour mémoire.

  • GET /api/billing/portal/{token} — consomme le jeton du lien magique envoyé par POST /api/billing/portal et redirige (302) vers le Customer Portal Stripe ; 404 générique dans tous les cas d'échec. Une redirection à usage unique, pas une ressource à interroger.
  • POST /api/billing/webhook — réception des événements Stripe (paiement complété), authentifiée par signature stripe-signature et consommée uniquement par les serveurs Stripe. Un webhook serveur-à-serveur, sans pertinence pour un client de l'API.
La seule route de ce domaine appelable par clé API est GET /api/account/key-tier — voir la page « Compte & clés API ».