Webhooks salientes

Recibe notificaciones HTTP en tu servidor cuando ocurren eventos en tu cuenta Kligrafia: firmas completadas, KYC, formularios enviados, etc.

Resumen rápido

Formato del payload

POST {tu-url}
content-type: application/json
user-agent: Kligrafia-Webhook/1.0
kligrafia-signature: t=1715000000,v1=a3b4c5...

{
  "id": "whkdel_01k...",
  "type": "envelope.completed",
  "created": 1715000000,
  "data": {
    "envelopeId": "env_...",
    "title": "Contrato XYZ",
    "completedAt": "2026-05-12T19:30:00Z",
    "signers": [
      { "id": "sgn_...", "email": "[email protected]", "signedAt": "..." }
    ]
  }
}

Eventos disponibles

envelope.createdenvelope.sentenvelope.viewedenvelope.signedenvelope.completedenvelope.declinedenvelope.voidedenvelope.expiredsigner.viewedsigner.signedsigner.declinedkyc.verifiedkyc.failedkyc.fraud_detectedpublic_form.submitted

Podés suscribir a * para recibir todos los eventos presentes y futuros, o usar prefijos como envelope.*.

Verificación de firma

El header Kligrafia-Signature tiene el formato t=<timestamp>,v1=<hmac-sha256-hex>. La firma se calcula sobre <t>.<raw-body>.

Tolerancia: 300 segundos contra el timestamp para evitar replay attacks.

Node.js

// Node.js — verificación de firma
const crypto = require('node:crypto');

function verifySignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map(p => p.trim().split('='))
  );
  const ts = Number(parts.t);
  const sig = parts.v1;
  if (!ts || !sig) return false;
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5 min tolerance

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(sig, 'hex');
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

// Express example
app.post('/webhooks/kligrafia', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['kligrafia-signature'];
  const ok = verifySignature(req.body.toString('utf-8'), sig, process.env.KLIGRAFIA_WEBHOOK_SECRET);
  if (!ok) return res.status(401).send('invalid signature');
  const event = JSON.parse(req.body.toString('utf-8'));
  // handle event.type === 'envelope.completed' etc.
  res.status(200).send('ok');
});

Python

# Python — verificación de firma
import hmac, hashlib, time

def verify_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.strip().split('=', 1) for p in header.split(','))
    ts = int(parts.get('t', 0))
    sig = parts.get('v1', '')
    if not ts or not sig:
        return False
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{ts}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig)

# Flask example
@app.route('/webhooks/kligrafia', methods=['POST'])
def kligrafia_webhook():
    sig = request.headers.get('Kligrafia-Signature', '')
    if not verify_signature(request.get_data(), sig, os.environ['KLIGRAFIA_WEBHOOK_SECRET']):
        return 'invalid signature', 401
    event = request.get_json()
    # handle event
    return 'ok', 200

PHP

<?php
// PHP — verificación de firma (compatible con WordPress plugin)
function kligrafia_verify_signature(string $body, string $header, string $secret, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(',', $header) as $p) {
        [$k, $v] = explode('=', trim($p), 2);
        $parts[$k] = $v;
    }
    $ts = (int) ($parts['t'] ?? 0);
    $sig = $parts['v1'] ?? '';
    if (!$ts || !$sig) return false;
    if (abs(time() - $ts) > $tolerance) return false;
    $expected = hash_hmac('sha256', $ts . '.' . $body, $secret);
    return hash_equals($expected, $sig);
}

Browser (JS SDK)

<!-- Browser — JS SDK auto-loaded -->
<script src="https://kligrafia.com/sdk/v1/kligrafia.js"></script>
<script>
  // Embed form en un container existente
  Kligrafia.embedForm({
    publicId: 'pubform_abc123',
    container: '#kligrafia-mount',
    onSubmit: (e) => console.log('signed:', e),
  });

  // O modal overlay
  document.getElementById('cta').onclick = () => Kligrafia.openForm('pubform_abc123');

  // Verificar firma (cuando recibes webhooks vía edge function, etc.)
  await Kligrafia.verifySignature(rawBody, signatureHeader, secret); // boolean
</script>

Buenas prácticas