Firma ECDSA-SHA256
Cada request a la API requiere headers firmados con ECDSA-SHA256 (secp256k1). No hay tokens Bearer ni sesiones.
Headers requeridos
Sección titulada «Headers requeridos»| Header | Descripción | Ejemplo |
|---|---|---|
x-client-id | ID de tu API Key | rivo_abc123xyz |
x-timestamp | Fecha/hora actual ISO 8601 (±5 min) | 2026-03-18T14:00:00.000Z |
x-nonce | String único aleatorio por request | a3f9b2c1d8e7... |
x-signature | Firma ECDSA-SHA256 en Base64 | MEYCIQDx... |
x-content-sha256 | SHA-256 del body en hex minúscula | e3b0c442... |
Headers adicionales
Sección titulada «Headers adicionales»| Header | Descripción |
|---|---|
x-idempotency-key | Obligatoria en los 6 POST que mueven dinero; el resto de peticiones no la necesitan. No entra en el string a firmar. Ver Idempotencia |
String a firmar
Sección titulada «String a firmar»El string a firmar es un texto plano separado por saltos de línea (\n), no un objeto JSON:
{METHOD}\n{PATH}\n{TIMESTAMP}\n{NONCE}\n{SHA256_HEX_DEL_BODY}Ejemplo concreto:
POST/v1/co/breb/payin/qr2026-03-18T14:00:00.000Za3f9b2c1d8e7f4a1b2c3d4e5f6a7b8c99f2b8e1c0a4d7f36c5b1e8a09d4f27c3b6a5e0d918c7f42a3b6d5e8c1f0a9b74Pasos:
- Serializa el body una sola vez y calcula su SHA-256 en hex minúscula → ese es
x-content-sha256 - Construye el string a firmar concatenando método, path, timestamp, nonce y el digest, separados por
\n - Firma ese string con ECDSA-SHA256 usando tu clave privada secp256k1
- Encode la firma en Base64 → ese es
x-signature - Envía el mismo string que hasheaste como body — no lo vuelvas a serializar
Reglas del digest
Sección titulada «Reglas del digest»- El digest es sobre los bytes exactos que se envían. Si haces
JSON.stringifydos veces con distinto orden de campos o espaciado, el digest no cuadra y la respuesta es401. Esta es la causa nº 1 de errores de integración. - Sin body (
GET,DELETE) el digest es el del string vacío:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. - Quitar la cabecera
x-content-sha256invalida la firma: el digest forma parte del string firmado. - Un body que no corresponde al digest firmado devuelve
401con códigoUNAUTHORIZED.
Reglas de validación
Sección titulada «Reglas de validación»Todas estas comprobaciones ocurren antes de tocar la lógica de negocio. Cualquiera que falle responde 401 UNAUTHORIZED:
| Regla | Detalle |
|---|---|
x-timestamp dentro de ±5 minutos | Sincroniza el reloj del servidor por NTP: un desfase mayor invalida todas tus firmas |
x-nonce no reutilizado | TTL de 5 minutos |
Llave active | Una llave revocada deja de firmar |
| Llave de la región del path | Una key CO solo firma /v1/co/*; cruzar regiones es 401, no 403 |
IP de origen dentro de ipWhitelist | Obligatoria: sin al menos una IP configurada, la llave no firma desde ningún origen |
Perfil regional ACTIVE o SANDBOX y habilitado | Ver Modo Sandbox |
| Body coincide con el digest firmado | Causa nº 1 de errores de integración |
Por qué el body va firmado
Sección titulada «Por qué el body va firmado»El digest del body forma parte del string firmado: cualquier intermediario que vea el tráfico en claro —un proxy corporativo, un balanceador que termine TLS— no puede alterar el amount o la llave destino sin invalidar la firma.
Ejemplo en Node.js
Sección titulada «Ejemplo en Node.js»const crypto = require('crypto');
function buildRequest(method, path, body, privateKeyPem, keyId) { const timestamp = new Date().toISOString(); const nonce = crypto.randomUUID();
// Serializar UNA sola vez: lo que se hashea tiene que ser lo que se envía. const rawBody = body === undefined ? '' : JSON.stringify(body); const bodyHash = crypto.createHash('sha256').update(rawBody, 'utf8').digest('hex');
const stringToSign = `${method}\n${path}\n${timestamp}\n${nonce}\n${bodyHash}`; const signature = crypto.createSign('SHA256').update(stringToSign).sign(privateKeyPem, 'base64');
return { headers: { 'Content-Type': 'application/json', 'x-client-id': keyId, 'x-timestamp': timestamp, 'x-nonce': nonce, 'x-signature': signature, 'x-content-sha256': bodyHash, }, // Enviar `rawBody` tal cual, sin volver a serializar. body: rawBody, };}Ejemplo en Python
Sección titulada «Ejemplo en Python»import base64import hashlibimport jsonimport secretsfrom datetime import datetime, timezonefrom cryptography.hazmat.primitives import hashes, serializationfrom cryptography.hazmat.primitives.asymmetric import ec
def build_request(method, path, body=None, private_key_pem=None, key_id=None): timestamp = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') nonce = secrets.token_hex(16)
# Serializar UNA sola vez: lo que se hashea tiene que ser lo que se envía. raw_body = '' if body is None else json.dumps(body, separators=(',', ':')) body_hash = hashlib.sha256(raw_body.encode('utf-8')).hexdigest()
string_to_sign = f'{method}\n{path}\n{timestamp}\n{nonce}\n{body_hash}'
private_key = serialization.load_pem_private_key(private_key_pem, password=None) signature = private_key.sign(string_to_sign.encode('utf-8'), ec.ECDSA(hashes.SHA256()))
headers = { 'Content-Type': 'application/json', 'x-client-id': key_id, 'x-timestamp': timestamp, 'x-nonce': nonce, 'x-signature': base64.b64encode(signature).decode(), 'x-content-sha256': body_hash, } # Enviar `raw_body` tal cual, sin volver a serializar. return headers, raw_bodyEjemplo con cURL
Sección titulada «Ejemplo con cURL»# VariablesTIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")NONCE=$(openssl rand -hex 16)KEY_ID="rivo_abc123xyz"METHOD="POST"PATH_="/v1/co/breb/payin/qr"BODY='{"amount":50000,"reference":"orden-8842"}'
# SHA-256 del body exacto que se envíaBODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 | awk '{print $2}')
# String a firmar: campos separados por saltos de líneaSIGNATURE=$(printf '%s\n%s\n%s\n%s\n%s' "$METHOD" "$PATH_" "$TIMESTAMP" "$NONCE" "$BODY_HASH" \ | openssl dgst -sha256 -sign private.pem | base64 | tr -d '\n')
# Hacer el request con el MISMO body que se hasheócurl -X POST "https://api.sandbox.rivopaylatam.com$PATH_" \ -H "Content-Type: application/json" \ -H "x-client-id: $KEY_ID" \ -H "x-timestamp: $TIMESTAMP" \ -H "x-nonce: $NONCE" \ -H "x-signature: $SIGNATURE" \ -H "x-content-sha256: $BODY_HASH" \ -d "$BODY"