Adion
Cloud

API

Encaisser avec Adion Pay depuis votre logiciel ​

Votre site, votre boutique ou votre logiciel de caisse peut faire payer ses clients par carte avec Adion Pay. Votre logiciel crée une session de paiement, envoie l'acheteur sur la page de paiement d'Adion, puis reçoit le résultat par webhook. L'argent arrive sur votre compte Pay, comme pour un lien de paiement.

La référence est sur adion-api.eu

Ce guide montre le chemin. Chaque route, ses champs et ses réponses sont décrits sur adion-api.eu, groupe Pay.

Plus court avec une bibliothèque

En PHP ou en Node.js, les bibliothèques officielles obtiennent le jeton, créent la session et vérifient les webhooks en quelques lignes. Ce guide montre aussi les appels bruts, pour tout autre langage.

Avant de commencer ​

  • Un compte Adion Pay ouvert et actif dans un de vos projets (voir Adion Pay).

  • Une application serveur dans Mon compte › Applications, limitée au projet du compte Pay, avec les scopes :

    • pay.session.write pour créer des sessions ;
    • pay.payment.read pour relire sessions et paiements ;
    • pay.refund.write seulement si votre logiciel rembourse.

    Voir Créer une application.

  • Une adresse https publique où votre logiciel recevra les webhooks.

Le parcours ​

  1. Votre logiciel obtient un jeton avec l'identifiant et le secret de l'application.
  2. Il crée une session : un montant, une description, votre référence de commande, vos adresses de retour.
  3. Il envoie l'acheteur sur l'adresse url de la session (https://pay.adioncloud.com/…).
  4. L'acheteur paie. La page le renvoie sur votre success_url, avec ?session_id=pay_sess_….
  5. Adion envoie le webhook payment.succeeded à votre adresse. C'est lui qui fait foi : le retour de l'acheteur sur votre site ne prouve rien.

Pour garder l'acheteur sur votre page plutôt que de le rediriger, voir le formulaire intégré.

1. Obtenir un jeton ​

bash
curl -X POST https://adion-api.eu/api/auth/token \
  -d grant_type=client_credentials \
  -d client_id=VOTRE_CLIENT_ID \
  -d client_secret=VOTRE_SECRET

La réponse donne access_token, valable une heure. Gardez-le en mémoire et redemandez-en un quand il expire.

2. Créer une session ​

bash
curl -X POST https://adion-api.eu/api/v1/pay/sessions \
  -H "Authorization: Bearer $JETON" \
  -H "Idempotency-Key: commande-4521" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_cents": 4990,
    "description": "Commande n° 4521",
    "reference": "CMD-4521",
    "metadata": {"order_id": "4521"},
    "customer_email": "client@exemple.fr",
    "success_url": "https://boutique.exemple.fr/commande/4521/merci",
    "cancel_url": "https://boutique.exemple.fr/panier"
  }'
  • amount_cents : le montant TTC en centimes (4990 = 49,90 €), en euros.
  • reference et metadata : les vôtres. Ils reviennent avec le paiement et dans chaque webhook.
  • expires_at (facultatif) : la session expire au bout de 24 h par défaut, de 30 minutes à 7 jours.
  • pay_account_id : seulement si plusieurs comptes Pay encaissent dans vos projets.

La réponse (201) donne id (pay_sess_…), url, status (open) et expires_at. Envoyez l'acheteur sur url.

L'en-tête Idempotency-Key

Donnez une clé par commande. Si le réseau coupe et que votre logiciel réessaie, la même session revient (200) au lieu d'une seconde. La même clé avec un autre contenu est refusée (409).

En PHP (Laravel) ​

php
use Illuminate\Support\Facades\Http;

$token = Http::asForm()->post('https://adion-api.eu/api/auth/token', [
    'grant_type' => 'client_credentials',
    'client_id' => config('services.adion_pay.client_id'),
    'client_secret' => config('services.adion_pay.client_secret'),
])->throw()->json('access_token');

$session = Http::withToken($token)
    ->withHeaders(['Idempotency-Key' => 'commande-'.$order->id])
    ->post('https://adion-api.eu/api/v1/pay/sessions', [
        'amount_cents' => $order->total_cents,
        'description' => 'Commande n° '.$order->id,
        'reference' => (string) $order->id,
        'customer_email' => $order->email,
        'success_url' => route('commande.merci', $order),
        'cancel_url' => route('panier'),
    ])->throw()->json();

$order->update(['adion_session_id' => $session['id']]);

return redirect()->away($session['url']);

En JavaScript (Node.js) ​

js
const tokenResponse = await fetch('https://adion-api.eu/api/auth/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: process.env.ADION_CLIENT_ID,
    client_secret: process.env.ADION_CLIENT_SECRET,
  }),
})
const { access_token } = await tokenResponse.json()

const response = await fetch('https://adion-api.eu/api/v1/pay/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${access_token}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `commande-${order.id}`,
  },
  body: JSON.stringify({
    amount_cents: order.totalCents,
    description: `Commande n° ${order.id}`,
    reference: String(order.id),
    success_url: `https://boutique.exemple.fr/commande/${order.id}/merci`,
  }),
})
const session = await response.json()
// Rediriger l'acheteur vers session.url

Ne créez jamais une session depuis le navigateur : le secret de l'application ne quitte pas votre serveur.

3. Le retour de l'acheteur ​

Payé, l'acheteur revient sur success_url?session_id=pay_sess_…. Si votre adresse contient {SESSION_ID}, l'identifiant est mis à sa place. S'il renonce, la page lui propose de revenir sur cancel_url.

Pour afficher le bon message, relisez la session : GET /api/v1/pay/sessions/{id}. status vaut open (pas encore payée), complete (payée : payment_id) ou expired.

4. Recevoir les webhooks ​

Les adresses de webhook d'un compte Pay se déclarent par l'API (POST /api/account/pay/{pay_account_id}/webhook, droit Gérer les webhooks Pay, scope pay.webhook.manage) ; l'écran de l'espace client est en préparation. Choisissez les événements :

ÉvénementQuand
payment.succeededUn paiement a abouti
payment.failedUn paiement a été refusé
refund.succeededUn remboursement est effectué
dispute.openedL'acheteur conteste un paiement auprès de sa banque (dispute.status : open, avec la date limite des justificatifs)
dispute.closedLa contestation est close (dispute.status : won, lost, ou closed pour une simple demande d'information de la banque)
session.expiredUne session n'a pas été payée à temps

Le secret de signature (whsec_…) s'affiche une seule fois, à la création. Rangez-le avec le secret de l'application.

Chaque envoi est un POST en JSON :

json
{
  "id": "pay_evt_AbCdEf123456",
  "object": "event",
  "type": "payment.succeeded",
  "api_version": "2026-10-14",
  "created": "2026-10-14T10:03:12+00:00",
  "livemode": false,
  "pay_account_id": "pay_acct_AbCdEf123456",
  "data": {
    "object": {
      "id": "pay_pmt_AbCdEf123456",
      "object": "pay.payment",
      "status": "succeeded",
      "amount_cents": 4990,
      "reference": "CMD-4521",
      "metadata": {"order_id": "4521"},
      "session_id": "pay_sess_AbCdEf123456"
    }
  }
}

Vérifier la signature ​

L'en-tête Adion-Signature ressemble à t=1760436192,v1=5257a8…. t est l'heure d'envoi, v1 le HMAC-SHA256, en hexadécimal, de t + . + le corps brut, avec votre secret.

  1. Lisez le corps brut, avant tout décodage JSON.
  2. Recalculez le HMAC et comparez-le en temps constant.
  3. Refusez un t de plus de cinq minutes.
  4. Répondez 2xx vite ; faites le travail long ensuite.
php
// Laravel : routes/web.php, route exclue de la protection CSRF
Route::post('/webhooks/adion-pay', function (Illuminate\Http\Request $request) {
    $body = $request->getContent();
    parse_str(str_replace(',', '&', (string) $request->header('Adion-Signature')), $sig);
    $expected = hash_hmac('sha256', ($sig['t'] ?? '').'.'.$body, config('services.adion_pay.webhook_secret'));

    abort_unless(isset($sig['v1']) && hash_equals($expected, $sig['v1']) && abs(time() - (int) $sig['t']) <= 300, 400);

    $event = json_decode($body, true);
    if ($event['type'] === 'payment.succeeded') {
        Order::where('id', $event['data']['object']['reference'])->update(['paid' => true]);
    }

    return response()->noContent();
});
js
// Node.js (Express) : le corps brut est indispensable
import crypto from 'node:crypto'

app.post('/webhooks/adion-pay', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = Object.fromEntries(String(req.get('Adion-Signature')).split(',').map((p) => p.split('=')))
  const expected = crypto.createHmac('sha256', process.env.ADION_WEBHOOK_SECRET).update(`${parts.t}.${req.body}`).digest('hex')
  const ok = parts.v1 && expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  if (!ok || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return res.sendStatus(400)

  const event = JSON.parse(req.body)
  // event.type, event.data.object.reference…
  res.sendStatus(204)
})

Nouvelles tentatives ​

Sans réponse 2xx en dix secondes, Adion réessaie : après 1 minute, 5 minutes, 30 minutes, 2 heures, 5 heures, 10 heures, puis toutes les 12 heures, pendant trois jours. Un même événement peut donc arriver deux fois : servez-vous de son id pour ne le traiter qu'une fois.

Le journal des livraisons (code rendu, durée, début de la réponse) se lit par l'API, et une livraison se renvoie à la main (…/delivery/{id}/resend).

L'adresse doit être publique : une adresse interne (localhost, réseau privé) est refusée.

Formulaire intégré ​

Au lieu d'envoyer l'acheteur sur la page d'Adion, vous pouvez afficher le paiement dans votre propre page. Le formulaire vit dans une iframe servie par pay.adioncloud.com : les données de carte ne passent jamais par votre page ni par votre serveur.

1. Déclarer vos pages ​

Dans l'espace client, ouvrez le compte Pay, onglet Intégration, et ajoutez l'origine de chaque site qui affichera le paiement : https://boutique.exemple.fr (le protocole, le nom, un port s'il y en a un, rien après). Dix au plus. Un compte d'essai admet aussi http://localhost:<port>.

Le formulaire ne s'affiche que sur ces origines : le navigateur refuse de l'afficher dans toute autre page, et Adion refuse d'ouvrir la session. Sans origine déclarée, l'API refuse de créer une session intégrée (422).

2. Créer la session sur votre serveur ​

La même route, avec ui_mode à embedded. return_url (facultative) remplace success_url et cancel_url, qui sont refusées dans ce mode.

bash
curl -X POST https://adion-api.eu/api/v1/pay/sessions \
  -H "Authorization: Bearer $JETON" \
  -H "Idempotency-Key: commande-4521" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_cents": 4990,
    "description": "Commande n° 4521",
    "reference": "CMD-4521",
    "ui_mode": "embedded",
    "return_url": "https://boutique.exemple.fr/commande/4521/merci"
  }'

La réponse n'a pas d'url, mais un client_secret propre à la session. Votre serveur le passe à la page de paiement. Il ne sert qu'à afficher et payer cette session, expire avec elle, n'ouvre aucun autre accès, et ne figure jamais dans les webhooks. Le secret de votre application, lui, ne quitte jamais votre serveur.

3. Afficher le formulaire ​

Une page complète. Le serveur de la boutique répond à POST /paiement/session avec {"clientSecret": "…"}, créé comme ci-dessus.

html
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Paiement de la commande 4521</title>
  <script src="https://pay.adioncloud.com/v1/adion-pay.js"></script>
</head>
<body>
  <h1>Commande n° 4521</h1>
  <div id="paiement"></div>
  <p id="message" hidden></p>

  <script type="module">
    // 1. Votre serveur crée la session intégrée et rend son client_secret.
    const reponse = await fetch('/paiement/session', { method: 'POST' })
    const { clientSecret } = await reponse.json()

    // 2. Le formulaire d'Adion s'affiche dans la page.
    const pay = AdionPay()
    const checkout = await pay.initEmbeddedCheckout({
      clientSecret,
      appearance: { theme: 'auto' },     // 'light', 'dark' ou 'auto' (celui de l'acheteur)
      onComplete: ({ sessionId }) => {
        // L'acheteur a payé : remerciez-le. La commande se valide sur le webhook, pas ici.
        checkout.destroy()
        document.getElementById('message').hidden = false
        document.getElementById('message').textContent = 'Merci ! Votre paiement est enregistré.'
      },
    })
    checkout.on('error', ({ message }) => console.warn('Adion Pay :', message))
    checkout.mount('#paiement')
  </script>
</body>
</html>
  • initEmbeddedCheckout() rend le formulaire ; mount() le place dans l'élément donné (un sélecteur ou un élément) ; destroy() le retire.
  • La hauteur de l'iframe suit celle du formulaire, toute seule.
  • Événements : ready (le formulaire est affiché), complete (payé), error (session introuvable, expirée, page non déclarée…), par les options onReady, onComplete, onError ou par checkout.on(…).
  • Sans onComplete, l'acheteur part une fois payé sur return_url, avec ?session_id=pay_sess_… (ou à la place de {SESSION_ID}).
  • Le script est versionné : /v1/ ne changera pas de façon incompatible.

onComplete ne prouve rien

Comme le retour sur success_url, onComplete se passe dans le navigateur de l'acheteur : n'importe qui peut l'imiter. Le paiement fait foi par le webhook payment.succeeded (ou en relisant la session : status = complete). Ne livrez qu'à ce moment-là.

Rembourser ​

POST /api/v1/pay/payments/{id}/refunds, avec amount_cents pour un remboursement partiel, sans pour tout rembourser. Il faut le scope pay.refund.write. Le montant est repris sur votre compte Pay ; la commission n'est pas rendue. L'événement refund.succeeded suit.

Un paiement contesté ne se rembourse plus (409) : c'est la banque de l'acheteur qui décide de l'issue.

Les contestations ​

Quand un acheteur conteste un paiement auprès de sa banque, le paiement porte disputed: true et un objet dispute :

ChampCe qu'il dit
statusopen tant que la banque n'a pas tranché, puis won, lost, ou closed (simple demande d'information, rien n'est débité)
amount_centsLe montant contesté
evidence_due_atLa date limite pour fournir vos justificatifs
debited_centsCe qui a été débité de votre compte Pay à l'ouverture de la contestation
recredited_centsCe qui vous a été rendu, si la contestation est gagnée

Le montant contesté est débité de votre compte Pay dès l'ouverture (conditions particulières, article 12.2) et recrédité si la banque vous donne raison. net_cents en tient compte.

Un agent IA ne rembourse pas

Un assistant branché par MCP peut créer une session de paiement, pas rembourser : c'est un geste d'argent, fait par vous ou par votre logiciel.

Tester ​

Un compte Pay en mode essai donne des sessions avec livemode: false. La page de paiement l'annonce et accepte la carte d'essai indiquée à l'écran. Aucun montant réel n'est débité.