Méthode HMAC — un appel signé
Avec la méthode HMAC, vous créez un paiement en un seul appel. Pas de jeton à gérer : vous signez chaque requête avec votre Client Secret, et vous envoyez la signature dans un en-tête.
POST /partners/rtp
À la fin, vous recevez un rtpLink : le lien de paiement à présenter au client.
La collection Postman inclut une requête Create RTP Request prête à l'emploi (dossier 07). Calculez la signature avec un des exemples ci-dessous, renseignez X-Signature, et envoyez.
Vous aurez besoin de
- Votre Client ID et votre Client Secret (depuis la Console Lodin).
- L'horloge de votre serveur à l'heure (synchronisée NTP) — la requête est refusée si elle s'écarte de plus de 300 secondes de l'heure serveur.
Les en-têtes
| En-tête | Requis | Valeur |
|---|---|---|
Content-Type | Oui | application/json |
X-Client-Id | Oui | Votre Client ID |
X-Timestamp | Oui | L'instant courant en ISO-8601 UTC, ex. 2026-06-10T12:00:00Z |
X-Signature | Oui | La signature HMAC-SHA256 (voir ci-dessous) |
Calculer la signature
La signature prouve que la requête vient bien de vous. Elle se calcule en quatre étapes.
Étape 1 — construire la chaîne à signer
Mettez bout à bout, sans aucun séparateur, dans cet ordre exact :
chaîne = X-Client-Id + X-Timestamp + montant + externalReference
- montant : le
amountformaté à 2 décimales avec un point (100.5→100.50,100→100.00). Si le montant est absent, utilisez0.00. - externalReference : votre référence. Si vous n'en envoyez pas, utilisez une chaîne vide.
Exemple :
X-Client-Id = CLIENT_123
X-Timestamp = 2026-06-10T12:00:00Z
amount = 100.50
externalReference= EXT-001
chaîne à signer = CLIENT_1232026-06-10T12:00:00Z100.50EXT-001
Étape 2 — préparer la clé
Votre clé de signature est votre Client Secret. Il vous est remis encodé en Base64 ; décodez-le pour obtenir la clé, exactement comme le fait le serveur.
Étape 3 — calculer le HMAC-SHA256
Calculez HMAC-SHA256(chaîne, clé).
Étape 4 — encoder en Base64URL sans padding
Encodez le résultat en Base64URL (+→-, /→_) et retirez les = de fin. C'est votre X-Signature.
Le serveur recalcule la signature de la même façon et la compare. Le moindre écart (montant non formaté à 2 décimales, séparateur ajouté, padding = laissé, mauvaise heure) donne Invalid signature. Les exemples ci-dessous reproduisent exactement le calcul serveur.
Exemples de code
- PHP
- Python
- Node.js
- Java
$clientId = 'CLIENT_123';
$timestamp = gmdate('Y-m-d\TH:i:s\Z'); // ex. 2026-06-10T12:00:00Z
$amount = 100.50;
$externalRef= 'EXT-001';
// Clé : le Client Secret, décodé depuis sa forme Base64
$key = base64_decode($clientSecret);
$amountStr = number_format($amount, 2, '.', ''); // "100.50"
$payload = $clientId . $timestamp . $amountStr . ($externalRef ?? '');
$raw = hash_hmac('sha256', $payload, $key, true);
$signature = rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
import hmac, hashlib, base64
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_UP
client_id = "CLIENT_123"
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
amount = 100.50
external_ref = "EXT-001"
# Clé : le Client Secret, décodé depuis sa forme Base64
key = base64.b64decode(client_secret)
amount_str = str(Decimal(str(amount)).quantize(Decimal("0.01"), ROUND_HALF_UP)) # "100.50"
payload = f"{client_id}{timestamp}{amount_str}{external_ref or ''}"
raw = hmac.new(key, payload.encode("utf-8"), hashlib.sha256).digest()
signature = base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
const crypto = require("crypto");
const clientId = "CLIENT_123";
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); // 2026-06-10T12:00:00Z
const amount = 100.5;
const externalRef = "EXT-001";
// Clé : le Client Secret, décodé depuis sa forme Base64
const key = Buffer.from(clientSecret, "base64");
const amountStr = amount.toFixed(2); // "100.50"
const payload = `${clientId}${timestamp}${amountStr}${externalRef ?? ""}`;
const raw = crypto.createHmac("sha256", key).update(payload, "utf8").digest();
const signature = raw.toString("base64url"); // Base64URL sans padding
String secret = new String(Base64.getDecoder().decode(clientSecretEnc), StandardCharsets.UTF_8);
String payload = clientId + timestamp + normalizeAmount(amount) + nullToEmpty(externalReference);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = Base64.getUrlEncoder().withoutPadding()
.encodeToString(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
Le corps de la requête
| Champ | Type | Requis | Règles |
|---|---|---|---|
amount | Double | Oui | Non nul, strictement positif |
payeeIban | String | Oui | Non vide, max 34 |
payeeFullName | String | Oui | Non vide, max 150 |
externalReference | String | Non | Max 150 (doit correspondre à la valeur signée) |
currency | String | Non | 3 lettres ; par défaut MAD si absent — envoyez-le |
description | String | Non | Max 2000 |
returnUrl | String | Non | Max 1000 — URL de retour du client |
webhookUrl | String | Non | Max 1000 |
customerName | String | Non | Max 150 |
customerEmail | String | Non | E-mail valide, max 150 |
customerPhone | String | Non | Max 30 |
metadataJson | String | Non | Max 4000 |
paymentType | String | Non | Max 30 |
Si vous omettez currency, le service applique MAD. Envoyez currency explicitement (par exemple EUR) pour éviter toute ambiguïté.
Exemple complet
POST /partners/rtp
Content-Type: application/json
X-Client-Id: CLIENT_123
X-Timestamp: 2026-06-10T12:00:00Z
X-Signature: 9c1f...sans-padding
{
"amount": 100.50,
"currency": "EUR",
"externalReference": "EXT-001",
"description": "Demande RTP de démonstration",
"returnUrl": "https://partner.example.com/return",
"webhookUrl": "https://partner.example.com/webhook",
"customerName": "Nom Client",
"customerEmail": "[email protected]",
"customerPhone": "+212600000002",
"payeeIban": "FR7630003011300040000000000",
"payeeFullName": "Atelier Marquet SAS",
"paymentType": "INST"
}
La réponse
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"rtpLink": "https://gateway.lodinpay.com/pay?id=ORDER-ID-123&type=PARTNER&canal=WEB",
"status": "CREATED",
"accessLogId": 123,
"remainingToday": null,
"usedToday": 1
}
| Champ | Description |
|---|---|
requestId | Identifiant de la demande RTP. |
rtpLink | Le lien de paiement à présenter au client. |
status | CREATED à la création. Voir le cycle de vie. |
usedToday | Nombre de demandes effectuées aujourd'hui. |
Présentez le rtpLink au client. À la fin du parcours, son navigateur revient sur votre returnUrl. Le statut réel se met à jour côté serveur et vous sera notifié par webhook (livraison à venir).
Erreurs courantes
| HTTP | Message | Cause |
|---|---|---|
401 | Invalid clientId | X-Client-Id inconnu |
401 | Request expired | X-Timestamp hors de la fenêtre de 300 secondes |
401 | Invalid signature | La signature ne correspond pas (voir l'encadré plus haut) |
403 | Inactive credentials | Identifiant désactivé |
403 | Application is not active | Application inactive |
400 | Invalid request data | Un champ du corps ne respecte pas les règles |
La référence complète figure sur la page Erreurs.