SkipDocumentación Skipdocs
AAPDWebhooks

Verificación de firma

Verificar el header X-Gokeipay-Signature HMAC-SHA256.

Cada webhook de SkipPay viene firmado con HMAC-SHA256 usando tu webhook_secret. Verifica siempre la firma antes de confiar en el payload — sino cualquiera puede mandar POST falsos a tu endpoint.

Headers

Content-Type: application/json
X-Gokeipay-Signature: sha256={hex}
X-Skippay-Signature: sha256={hex}   # alias legacy, mismo valor

Cualquiera de los dos sirve; siempre coinciden. Código nuevo debería leer X-Gokeipay-Signature.

Cómo verificar

La firma es HMAC-SHA256(webhook_secret, raw_request_body). Lee los bytes crudos del body — no dejes que tu framework parsee JSON primero, porque al re-serializar cambia el whitespace y rompe la firma.

Node.js

import crypto from "node:crypto";

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody) // Buffer o string — NO objeto parseado
    .digest("hex");
  const received = signatureHeader.replace("sha256=", "");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "utf8"),
    Buffer.from(received, "utf8"),
  );
}

En Express, configura el body parser para mantener el cuerpo crudo:

app.post(
  "/webhooks/skippay",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.headers["x-gokeipay-signature"];
    if (!verifyWebhook(req.body, sig, process.env.SKIPPAY_WEBHOOK_SECRET)) {
      return res.status(401).json({ error: "Invalid signature" });
    }
    const event = JSON.parse(req.body);
    // procesa el evento...
    res.status(200).json({ received: true });
  },
);

Python

import hmac
import hashlib

def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    received = signature_header.replace("sha256=", "")
    return hmac.compare_digest(expected, received)

En FastAPI:

@app.post("/webhooks/skippay")
async def skippay_webhook(request: Request):
    raw = await request.body()
    sig = request.headers.get("x-gokeipay-signature", "")
    if not verify_webhook(raw, sig, settings.SKIPPAY_WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")
    event = json.loads(raw)
    # procesa el evento...
    return {"received": True}

PHP

function verify_webhook(string $rawBody, string $signatureHeader, string $secret): bool {
    $expected = hash_hmac("sha256", $rawBody, $secret);
    $received = str_replace("sha256=", "", $signatureHeader);
    return hash_equals($expected, $received);
}

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strings"
)

func verifyWebhook(rawBody []byte, signatureHeader, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    received := strings.TrimPrefix(signatureHeader, "sha256=")
    return hmac.Equal([]byte(expected), []byte(received))
}

Errores comunes

  • Parsear JSON antes de firmar. La firma es sobre los bytes crudos. Si re-serializas, las diferencias de whitespace causan mismatch.
  • Usar comparación == regular. Usa timingSafeEqual / hash_equals / hmac.compare_digest para evitar timing attacks.
  • Guardar el secreto en código de cliente. Es server-only.

On this page