HSP Wallet
API de pagamento
Esta API permite a um site comerciante receber euros numa conta HSP. O cliente é redirecionado para uma página HSP, introduz o telefone e o PIN da aplicação HSP Wallet e volta ao seu site. O PIN nunca é enviado ao seu servidor.
1. Entre no espaço comerciante: https://comptehsp.com/commercant (telefone + PIN da aplicação).
2. Abra o cartão « API de pagamento web ».
3. Clique em « Obter a chave ». A HSP mostra:
- key_id (público), começa por hspk_
- secret (privado), começa por hsps_
4. Copie o segredo de imediato. Não volta a ser mostrado. « Regenerar » cria um par novo e invalida o anterior.
5. « Revogar » desliga a API.
Documentação: /pay/checkout/docs?lang=fr|en|es|de|pt|ar
Cada pedido do comerciante envia:
- X-HSP-Key: key_id
- X-HSP-Timestamp: tempo Unix em segundos
- X-HSP-Signature: HMAC-SHA256 em hexadecimal
Mensagem assinada, quatro linhas separadas por \n:
timestamp
METHOD
path
sha256_hex(corpo)
Base: https://comptehsp.com
POST /api/v1/checkout/payments
| Campo | Obrigatório | Descrição |
|---|---|---|
| reference | sim | A sua encomenda, única. 1 a 80 caracteres: letras, dígitos, . _ : - |
| amount | sim | Montante em EUR, de 0,01 a 50000. Número JSON. |
| currency | sim | Apenas EUR |
| return_url | sim | URL https pública de regresso após o pagamento |
| cancel_url | não | URL https pública se o cliente cancelar |
| webhook_url | não | URL https pública que recebe o resultado |
| description | não | Texto mostrado ao cliente, 180 caracteres |
| expires_in | não | Vida do link em segundos, 60 a 86400. Omissão: 1800 |
| locale | não | fr, en, es, de, pt, ar |
| customer.email | não | Guardado para o seu acompanhamento |
| customer.name | não | Nome livre |
| metadata | não | Objeto JSON, 2000 caracteres no máximo |
A resposta 201 inclui checkout_url. Redirecione o cliente para lá. A HSP volta a return_url com hsp_payment_id, hsp_reference e hsp_status. Confirme sempre com o estado ou com o webhook assinado.
Uma referência só pode ser debitada uma vez.
- PENDING ainda válido: devolve-se o mesmo link.
- PENDING expirado, CANCELLED ou FAILED sem débito: a referência reabre com um link novo.
- PAID ou débito em curso: HTTP 409, already_paid.
- Duas confirmações em simultâneo só debitam uma vez.
- Expirado: estado EXPIRED, HTTP 410 payment_expired.
- Cinco PIN errados bloqueiam o link. Cinco PIN errados bloqueiam a conta durante 15 minutos.
Estados: PENDING, PROCESSING, PAID, EXPIRED, CANCELLED, FAILED.
GET /api/v1/checkout/payments/{id}
GET /api/v1/checkout/payments/reference/{reference}
A mesma assinatura HMAC, corpo vazio. A encomenda está paga apenas se status for PAID.
A HSP faz POST JSON para webhook_url quando o pagamento passa a PAID, EXPIRED, CANCELLED ou FAILED. Até 6 tentativas se não responder 2xx.
Cabeçalhos: X-HSP-Event, X-HSP-Timestamp, X-HSP-Signature.
A assinatura é HMAC-SHA256 de timestamp + quebra de linha + corpo bruto recebido.
Eventos: checkout.payment.paid, .expired, .cancelled, .failed.
O objeto payment inclui id, reference, status, amount, currency, paid_at, payer_label, ledger_ref e metadata.
Compare a assinatura com o corpo bruto. Recuse um relógio fora de 5 minutos. Entregue a encomenda só se reference, amount e status PAID coincidirem.
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 e Python: HSP_KEY (hspk_…) e HSP_SECRET (hsps_…). No WordPress, cole em Definições → 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}