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 valorCualquiera 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. UsatimingSafeEqual/hash_equals/hmac.compare_digestpara evitar timing attacks. - Guardar el secreto en código de cliente. Es server-only.