Saltar al contenido principal

Firma de requests

Toda request a la API PSP debe ir firmada. La firma es un HMAC-SHA256 calculado sobre un canonical string determinístico de la request, usando tu signature_secret. El servidor reconstruye el mismo canonical string a partir de la request que recibe y compara las firmas: si no coinciden (o el timestamp está fuera de la ventana), rechaza con 401.

Si usás Postman, el script hace todo esto por vos. Esta página describe el contrato exacto para quien quiera implementarlo en su propio backend.

Headers

Cada request lleva estos headers:

HeaderDescripción
x-signature-keyTu signature_key.
x-signature-valueLa firma HMAC-SHA256 en hexadecimal.
x-signature-timestampTimestamp epoch en milisegundos usado al firmar.
x-idempotencyClave de idempotencia (mínimo 5 caracteres). Ver Idempotencia.

Headers firmados (allowlist)

Solo estos headers entran en el canonical string. Cualquier otro header de la request se ignora a los efectos de la firma:

  • content-type
  • host
  • x-idempotency

Se normalizan así: nombre en minúsculas, valor con .trim(), ordenados alfabéticamente por nombre. Para host se quita el esquema (http:// / https://).

aviso

Los headers x-signature-* no se firman (no pueden: la firma es uno de ellos). Y como la lista es un allowlist, agregar headers extra a la request (por ejemplo accept o headers custom) no afecta la firma: el servidor los descarta. Si tu implementación los incluye en el canonical, la firma no va a coincidir.

Canonical string

Se arma con estas seis líneas unidas por \n, en este orden exacto:

{METHOD}
{normalizedPath}
{queryString}
{headersString}
{signedHeaders}
{bodyHash}

Y luego, antes de firmar, se le agrega una línea más con el timestamp:

{canonical}
{timestamp}

Cada parte:

  1. METHOD — método HTTP en mayúsculas (GET, POST, PUT, DELETE).
  2. normalizedPath — el path (sin la base_url ni el query string), colapsando barras repetidas (///) y quitando la barra final. Ejemplo: /api/v1/account/cvu//api/v1/account/cvu.
  3. queryString — parámetros de query con las claves ordenadas alfabéticamente, cada par como encodeURIComponent(k)=encodeURIComponent(v), unidos por &. Si no hay query, es "".
  4. headersString — los headers firmados (ver arriba) como nombre:valor, uno por línea, unidos por \n.
  5. signedHeaders — los nombres de los headers firmados, ordenados, unidos por ; (ejemplo: content-type;host;x-idempotency).
  6. bodyHash — SHA-256 en hex del body serializado (ver abajo). En GET o sin body: "".

Serialización del body

El body debe serializarse de forma determinística: se parsea el JSON y se re-serializa con las claves ordenadas alfabéticamente de forma recursiva (objetos anidados incluidos), sin espacios. Ese string es el que se hashea con SHA-256.

Orden de claves del body

Este es el error de integración más común. El servidor hashea el body con las claves ordenadas. Si tu cliente firma el JSON con las claves en el orden en que las escribiste (sin ordenar) pero el servidor las ordena, los hashes difieren y la firma falla. Serializá siempre con claves ordenadas recursivamente antes de hashear.

Firma

signature = HMAC_SHA256( canonical + "\n" + timestamp, signature_secret )   // en hex
  • timestamp es epoch en milisegundos (Date.now()).
  • El mismo timestamp viaja en el header x-signature-timestamp.

Ventana de validez y anti-replay

  • El servidor acepta la firma solo si el timestamp está dentro de una ventana de 5 minutos respecto de su hora actual.
  • No se aceptan timestamps en el futuro (tolerancia cero de skew hacia adelante). Si el reloj de tu servidor adelanta respecto del de PSP, las requests se rechazan. Mantené el reloj sincronizado (NTP).
  • La protección anti-replay se basa en esa ventana de 5 minutos combinada con el header x-idempotency.

Diagrama del flujo

Ejemplo de canonical string

Para un POST /api/v1/transfer con body {"to":"...","from":"...","amount":"100.00",...}:

POST
/api/v1/transfer

content-type:application/json
host:api.psp.stg.stickbank.net
x-idempotency:mi-clave-unica-123
content-type;host;x-idempotency
9f2c...e1 (SHA-256 hex del body con claves ordenadas)
1721822400000

(La tercera línea está vacía porque no hay query params.)