HSP Wallet API paiement marchand
API paiement marchand

API Paiement HSP — encaisser depuis un autre site

Cette API permet à un site marchand d’encaisser en euros sur un compte HSP. Le client est redirigé vers une page HSP, saisit le téléphone et le PIN de son application HSP Wallet, puis revient sur votre site. Le PIN n’est jamais envoyé à votre serveur.

URL de base https://comptehsp.comEURHMAC-SHA256
  1. 1Clé dans le bureau
  2. 2Lien de paiement
  3. 3Webhook signé
Espace commerçant

1. Obtenir la clé API (une seule par commerçant)

1. Connectez-vous à l’espace commerçant : https://comptehsp.com/commercant (téléphone + PIN de l’application).
2. Ouvrez la carte « API paiement site web ».
3. Cliquez sur « Obtenir la clé ». HSP affiche deux valeurs :
- key_id (public), commence par hspk_
- secret (privé), commence par hsps_
4. Copiez le secret immédiatement. Il n’est plus jamais relu. En cas de perte, « Régénérer » crée une nouvelle paire et invalide l’ancienne.
5. « Révoquer » coupe l’API sans la remplacer.
La même page existe en français, anglais, espagnol, allemand, portugais et arabe (sélecteur de langue). La documentation suit la langue : /pay/checkout/docs?lang=fr|en|es|de|pt|ar

2. Signature HMAC de chaque appel

Chaque requête marchande porte trois en-têtes :
- X-HSP-Key : le key_id
- X-HSP-Timestamp : heure Unix en secondes
- X-HSP-Signature : HMAC-SHA256 hexadécimal

Message signé, quatre lignes séparées par un saut de ligne (\n) :
timestamp
METHOD
path
sha256_hex(corps)

Base : https://comptehsp.com

3. Créer un paiement

POST /api/v1/checkout/payments

ChampObligatoireDescription
referenceouiVotre numéro de commande, unique. 1 à 80 caractères : lettres, chiffres, . _ : -
amountouiMontant en EUR, de 0,01 à 50000. Nombre JSON, pas une chaîne.
currencyouiUniquement EUR
return_urlouiURL https publique où renvoyer le client après paiement
cancel_urlnonURL https publique si le client annule
webhook_urlnonURL https publique qui reçoit le résultat
descriptionnonLibellé affiché au client, 180 caractères max
expires_innonDurée de vie du lien en secondes, de 60 à 86400. Défaut : 1800 (30 min)
localenonLangue de la page client : fr, en, es, de, pt, ar
customer.emailnonMémorisé pour votre suivi, non affiché comme identifiant de débit
customer.namenonNom libre, 120 caractères max
metadatanonObjet JSON libre, 2000 caractères max

Réponse 201 :
- id : identifiant HSP (pay_…)
- status : PENDING
- checkout_url : lien à ouvrir dans le navigateur du client
- expires_at : fin de validité
- already_exists : true si la même référence était déjà en attente

Redirigez le client vers checkout_url. Il paie sur le domaine HSP. Ensuite HSP le renvoie vers return_url avec hsp_payment_id, hsp_reference et hsp_status. Ces paramètres ne suffisent pas : confirmez toujours avec l’endpoint de statut ou le webhook signé.

4. Anti-doublon, expiration, statuts

Une référence marchand ne peut être débitée qu’une seule fois.
- PENDING encore valide : le même lien est renvoyé, sans second paiement.
- PENDING expiré, CANCELLED ou FAILED sans débit : la même référence est réouverte avec un nouveau lien.
- PAID ou débit en cours : HTTP 409, code already_paid. Aucun nouveau débit.
- Le passage en paiement utilise un verrou : deux confirmations simultanées ne débitent qu’une fois.
- À expiration, le statut devient EXPIRED et le formulaire est refusé (HTTP 410, code payment_expired).
- Cinq PIN incorrects sur le lien bloquent ce lien. Cinq PIN incorrects sur le compte HSP le verrouillent 15 minutes.

Statuts : PENDING, PROCESSING, PAID, EXPIRED, CANCELLED, FAILED.

5. Vérifier le statut

GET /api/v1/checkout/payments/{id}
GET /api/v1/checkout/payments/reference/{reference}

Même signature HMAC, corps vide. Ne considérez la commande payée que si status vaut PAID.

6. Webhook

HSP envoie un POST JSON à webhook_url quand le paiement devient PAID, EXPIRED, CANCELLED ou FAILED.
Nouvel essai jusqu’à 6 fois si votre serveur ne répond pas 2xx.

En-têtes :
- X-HSP-Event : checkout.payment.paid (ou .expired, .cancelled, .failed)
- X-HSP-Timestamp : Unix secondes
- X-HSP-Signature : HMAC-SHA256 du message timestamp + saut de ligne + corps brut reçu

Corps :
{
"event": "checkout.payment.paid",
"created_at": "2026-10-19T12:00:00.000Z",
"payment": {
"id": "pay_…",
"reference": "ORDER-1001",
"status": "PAID",
"amount": 10.5,
"currency": "EUR",
"description": "Commande 1001",
"checkout_url": "https://comptehsp.com/pay/checkout/…";,
"expires_at": "…",
"paid_at": "…",
"created_at": "…",
"payer_label": "Awa D.",
"ledger_ref": "…",
"metadata": { "cart": "1001" },
"webhook_status": "PENDING"
}
}

Comparez la signature au corps brut (raw body), pas à un JSON ré-encodé. Refusez un horodatage de plus de 5 minutes. Vérifiez que payment.reference et payment.amount correspondent à votre commande, et que status vaut PAID, avant de livrer.

7. Codes d’erreur

CodeHTTPSens
missing_signature401En-tête manquant
invalid_signature401HMAC incorrect
stale_timestamp401Horloge hors fenêtre
replayed_signature401Signature déjà utilisée
unknown_api_key401key_id inconnu
revoked_api_key401Clé révoquée
already_paid409Référence déjà payée ou en cours
reference_conflict409Même référence, montant différent
payment_expired410Lien expiré
invalid_amount400Montant refusé
invalid_currency400Devise autre que EUR
invalid_url400URL non https ou non publique
invalid_pin400Téléphone ou PIN incorrect
insufficient_balance400Solde client insuffisant
too_many_attempts429Trop d’essais PIN

Exemples

Pour Node.js, PHP et Python : variables HSP_KEY (hspk_…) et HSP_SECRET (hsps_…). Sur WordPress, collez-les dans Réglages → HSP Paiement.

Créer un paiement

const crypto = require('crypto');

function canonical(value) {
  if (value === null || typeof value !== 'object') return JSON.stringify(value);
  if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']';
  const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
  return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonical(value[k])).join(',') + '}';
}

function sign({ secret, timestamp, method, path, body }) {
  const payload = body == null ? '' : canonical(body);
  const message = [timestamp, method.toUpperCase(), path, crypto.createHash('sha256').update(payload).digest('hex')].join('\n');
  return crypto.createHmac('sha256', secret).update(message).digest('hex');
}

const body = {
  reference: 'ORDER-' + Date.now(),
  amount: 1,
  currency: 'EUR',
  description: 'Commande de test',
  return_url: 'https://shop.example/hsp/return',
  cancel_url: 'https://shop.example/hsp/cancel',
  webhook_url: 'https://shop.example/hsp/webhook',
  expires_in: 600,
  locale: 'fr',
  metadata: { cart: '1001' }
};
const timestamp = String(Math.floor(Date.now() / 1000));
const path = '/api/v1/checkout/payments';
const signature = sign({ secret: process.env.HSP_SECRET, timestamp, method: 'POST', path, body });

fetch('https://comptehsp.com' + path, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-HSP-Key': process.env.HSP_KEY,
    'X-HSP-Timestamp': timestamp,
    'X-HSP-Signature': signature
  },
  body: JSON.stringify(body)
}).then(async (res) => {
  const payment = await res.json();
  console.log(res.status, payment.checkout_url || payment);
});

Vérifier le statut

const reference = 'ORDER-1001';
const path = '/api/v1/checkout/payments/reference/' + encodeURIComponent(reference);
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = sign({ secret: process.env.HSP_SECRET, timestamp, method: 'GET', path, body: null });

fetch('https://comptehsp.com' + path, {
  headers: {
    'X-HSP-Key': process.env.HSP_KEY,
    'X-HSP-Timestamp': timestamp,
    'X-HSP-Signature': signature
  }
}).then(async (res) => console.log(res.status, await res.json()));

Vérifier le webhook

const crypto = require('crypto');

function verifyWebhook(secret, rawBody, timestamp, signature) {
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestamp)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(timestamp + '\n' + rawBody).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(String(signature || ''), 'hex');
  return a.length === b.length && a.length === 32 && crypto.timingSafeEqual(a, b);
}

// Express : app.use(express.raw({ type: 'application/json' })) sur cette route
app.post('/hsp/webhook', (req, res) => {
  const raw = req.body.toString('utf8');
  const ok = verifyWebhook(process.env.HSP_SECRET, raw, req.get('x-hsp-timestamp'), req.get('x-hsp-signature'));
  if (!ok) return res.status(401).end();
  const event = JSON.parse(raw);
  if (event.payment.status === 'PAID' && event.payment.amount === expectedAmount) {
    // livrer la commande une seule fois (idempotence sur payment.reference)
  }
  res.status(200).json({ received: true });
});