HSP Wallet
API de pago comercio
Esta API permite a un sitio comercio cobrar euros en una cuenta HSP. El cliente es redirigido a una página HSP, introduce el teléfono y el PIN de la aplicación HSP Wallet y vuelve a su sitio. El PIN nunca se envía a su servidor.
1. Entre en el espacio comercio: https://comptehsp.com/commercant (teléfono + PIN de la aplicación).
2. Abra la tarjeta « API de pago web ».
3. Pulse « Obtener la clave ». HSP muestra:
- key_id (público), empieza por hspk_
- secret (privado), empieza por hsps_
4. Copie el secreto de inmediato. No se vuelve a mostrar. « Regenerar » crea un par nuevo e invalida el anterior.
5. « Revocar » desactiva la API.
Documentación: /pay/checkout/docs?lang=fr|en|es|de|pt|ar
Cada petición del comercio envía:
- X-HSP-Key: key_id
- X-HSP-Timestamp: tiempo Unix en segundos
- X-HSP-Signature: HMAC-SHA256 en hexadecimal
Mensaje firmado, cuatro líneas separadas por \n:
timestamp
METHOD
path
sha256_hex(cuerpo)
Base: https://comptehsp.com
POST /api/v1/checkout/payments
| Campo | Obligatorio | Descripción |
|---|---|---|
| reference | sí | Su pedido, único. 1 a 80 caracteres: letras, cifras, . _ : - |
| amount | sí | Importe en EUR, de 0,01 a 50000. Número JSON. |
| currency | sí | Solo EUR |
| return_url | sí | URL https pública de retorno tras el pago |
| cancel_url | no | URL https pública si el cliente cancela |
| webhook_url | no | URL https pública que recibe el resultado |
| description | no | Texto mostrado al cliente, 180 caracteres |
| expires_in | no | Vida del enlace en segundos, 60 a 86400. Defecto: 1800 |
| locale | no | fr, en, es, de, pt, ar |
| customer.email | no | Guardado para su seguimiento |
| customer.name | no | Nombre libre |
| metadata | no | Objeto JSON, 2000 caracteres máx. |
La respuesta 201 incluye checkout_url. Redirija al cliente allí. Después HSP vuelve a return_url con hsp_payment_id, hsp_reference y hsp_status. Confirme siempre con el estado o el webhook firmado.
Una referencia solo puede cobrarse una vez.
- PENDING vigente: se devuelve el mismo enlace.
- PENDING caducado, CANCELLED o FAILED sin cargo: la referencia se reabre con un enlace nuevo.
- PAID o cargo en curso: HTTP 409, already_paid.
- Dos confirmaciones a la vez solo producen un cargo.
- Caducado: estado EXPIRED, HTTP 410 payment_expired.
- Cinco PIN incorrectos bloquean el enlace. Cinco PIN incorrectos en la cuenta la bloquean 15 minutos.
Estados: PENDING, PROCESSING, PAID, EXPIRED, CANCELLED, FAILED.
GET /api/v1/checkout/payments/{id}
GET /api/v1/checkout/payments/reference/{reference}
Misma firma HMAC y cuerpo vacío. El pedido está pagado solo si status es PAID.
HSP hace POST JSON a webhook_url cuando el pago pasa a PAID, EXPIRED, CANCELLED o FAILED. Hasta 6 intentos si usted no responde 2xx.
Cabeceras: X-HSP-Event, X-HSP-Timestamp, X-HSP-Signature.
La firma es HMAC-SHA256 de timestamp + salto de línea + cuerpo bruto recibido.
Eventos: checkout.payment.paid, .expired, .cancelled, .failed.
El objeto payment incluye id, reference, status, amount, currency, paid_at, payer_label, ledger_ref y metadata.
Compare la firma con el cuerpo bruto. Rechace un reloj fuera de 5 minutos. Entregue el pedido solo si reference, amount y status PAID coinciden.
missing_signature 401, invalid_signature 401, stale_timestamp 401, replayed_signature 401, unknown_api_key 401, revoked_api_key 401, already_paid 409, reference_conflict 409, payment_expired 410, invalid_amount 400, invalid_currency 400, invalid_url 400, invalid_pin 400, insufficient_balance 400, too_many_attempts 429.
Para Node.js, PHP y Python: HSP_KEY (hspk_…) y HSP_SECRET (hsps_…). En WordPress, péguelos en Ajustes → HSP 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);
});<?php
function hsp_canonical($value) {
if (!is_array($value)) {
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
$isList = function_exists('array_is_list')
? array_is_list($value)
: ($value === [] || array_keys($value) === range(0, count($value) - 1));
if ($isList) {
return '[' . implode(',', array_map('hsp_canonical', $value)) . ']';
}
ksort($value);
$parts = [];
foreach ($value as $key => $item) {
$parts[] = json_encode((string) $key, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ':' . hsp_canonical($item);
}
return '{' . implode(',', $parts) . '}';
}
function hsp_sign($secret, $timestamp, $method, $path, $body) {
$payload = $body === null ? '' : hsp_canonical($body);
$message = $timestamp . "\n" . strtoupper($method) . "\n" . $path . "\n" . hash('sha256', $payload);
return hash_hmac('sha256', $message, $secret);
}
$key = getenv('HSP_KEY');
$secret = getenv('HSP_SECRET');
$body = [
'reference' => 'ORDER-' . time(),
'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'],
];
$path = '/api/v1/checkout/payments';
$timestamp = (string) time();
$signature = hsp_sign($secret, $timestamp, 'POST', $path, $body);
$ch = curl_init('https://comptehsp.com' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-HSP-Key: ' . $key,
'X-HSP-Timestamp: ' . $timestamp,
'X-HSP-Signature: ' . $signature,
],
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$payment = json_decode($response, true);
echo $code . ' ' . ($payment['checkout_url'] ?? $response) . PHP_EOL;import hashlib, hmac, json, os, time, urllib.request
def canonical(value):
if isinstance(value, dict):
parts = [
json.dumps(key, ensure_ascii=False) + ":" + canonical(value[key])
for key in sorted(value)
]
return "{" + ",".join(parts) + "}"
if isinstance(value, list):
return "[" + ",".join(canonical(item) for item in value) + "]"
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def sign(secret, timestamp, method, path, body):
payload = "" if body is None else canonical(body)
message = "\n".join([timestamp, method.upper(), path, hashlib.sha256(payload.encode()).hexdigest()])
return hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
body = {
"reference": "ORDER-" + str(int(time.time())),
"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"},
}
path = "/api/v1/checkout/payments"
timestamp = str(int(time.time()))
signature = sign(os.environ["HSP_SECRET"], timestamp, "POST", path, body)
req = urllib.request.Request(
"https://comptehsp.com" + path,
data=json.dumps(body).encode(),
headers={
"Content-Type": "application/json",
"X-HSP-Key": os.environ["HSP_KEY"],
"X-HSP-Timestamp": timestamp,
"X-HSP-Signature": signature,
},
method="POST",
)
with urllib.request.urlopen(req) as res:
payment = json.loads(res.read().decode())
print(res.status, payment.get("checkout_url"))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()));<?php
$reference = 'ORDER-1001';
$path = '/api/v1/checkout/payments/reference/' . rawurlencode($reference);
$timestamp = (string) time();
$signature = hsp_sign(getenv('HSP_SECRET'), $timestamp, 'GET', $path, null);
$ch = curl_init('https://comptehsp.com' . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-HSP-Key: ' . getenv('HSP_KEY'),
'X-HSP-Timestamp: ' . $timestamp,
'X-HSP-Signature: ' . $signature,
],
]);
echo curl_exec($ch) . PHP_EOL;
curl_close($ch);from urllib.parse import quote
reference = "ORDER-1001"
path = "/api/v1/checkout/payments/reference/" + quote(reference, safe="")
timestamp = str(int(time.time()))
signature = sign(os.environ["HSP_SECRET"], timestamp, "GET", path, None)
req = urllib.request.Request(
"https://comptehsp.com" + path,
headers={
"X-HSP-Key": os.environ["HSP_KEY"],
"X-HSP-Timestamp": timestamp,
"X-HSP-Signature": signature,
},
)
with urllib.request.urlopen(req) as res:
print(json.loads(res.read().decode())["status"])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 });
});<?php
$secret = getenv('HSP_SECRET');
$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_HSP_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_HSP_SIGNATURE'] ?? '';
$now = time();
if (!ctype_digit($timestamp) || abs($now - intval($timestamp)) > 300) {
http_response_code(401);
exit;
}
$expected = hash_hmac('sha256', $timestamp . "\n" . $raw, $secret);
if (!hash_equals($expected, strtolower($signature))) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
if (($event['payment']['status'] ?? '') === 'PAID') {
// livrer une seule fois selon payment.reference
}
http_response_code(200);
echo json_encode(['received' => true]);import hashlib, hmac, json, os, time
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["HSP_SECRET"].encode()
@app.post("/hsp/webhook")
def webhook():
raw = request.get_data()
timestamp = request.headers.get("X-HSP-Timestamp", "")
signature = request.headers.get("X-HSP-Signature", "")
try:
ts = int(timestamp)
except ValueError:
return ("", 401)
if abs(int(time.time()) - ts) > 300:
return ("", 401)
expected = hmac.new(SECRET, timestamp.encode() + b"\n" + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature.lower()):
return ("", 401)
event = json.loads(raw)
if event["payment"]["status"] == "PAID":
pass # livrer une seule fois selon payment["reference"]
return {"received": True}