Volkern · Developer Hub

Partner API v1 & Webhook Tester

Integradores externos (ERPs, POS, e-commerce) encuentran aquí el spec OpenAPI navegable y un playground firmado con HMAC-SHA256 para validar que tu endpoint recibe eventos Volkern correctamente.

1

Genera tu API Key

En el dashboard del tenant (/dashboard/developers) crea una API key con scope invoices:write.

2

Configura tu webhook

Asocia un webhookUrl público y suscríbete a invoice.accepted, invoice.rejected. Guarda el webhookSecret (se muestra una sola vez).

3

Verifica HMAC

Tu endpoint recalcula sha256(secret, `$${ts}.$${payload}`) y la compara en tiempo constante contra X-Volkern-Signature.

Webhook Tester

Firma payloads con HMAC-SHA256 (Stripe-compat) y los envía a tu URL. La firma usa X-Volkern-Signature: sha256=<hex> sobre $${timestamp}.$${payload}.

Tu endpoint público. Si es localhost, usa el botón "Vía Volkern (proxy)" o el cURL de abajo.

Lo obtuviste al crear la API Key en /dashboard/developers. La firma HMAC nunca se envía a un servidor (salvo el proxy opcional).

AEAT aceptó la factura y emitió CSV firmado.

La petición directa requiere que tu endpoint acepte CORS o que lo pruebes desde terminal (cURL abajo). El proxy server-side salta CORS pero pasa momentáneamente tu secret por Volkern: no lo uses en producción con secrets reales.

Request preview

POST https://your-app.example.com/webhook
X-Volkern-Event: invoice.accepted
X-Volkern-Timestamp: —
X-Volkern-Signature: (introduce tu secret)
Content-Type: application/json

cURL

curl -X POST 'https://your-app.example.com/webhook' \
  -H 'Content-Type: application/json' \
  -H 'X-Volkern-Event: invoice.accepted' \
  -H 'X-Volkern-Timestamp: <TIMESTAMP>' \
  -H 'X-Volkern-Signature: <SIGNATURE>' \
  --data-raw '{
  "id": "evt_demo_jfp6zbd2",
  "type": "invoice.accepted",
  "createdAt": 1792320591,
  "tenantId": "cm_demo_tenant",
  "data": {
    "invoiceId": "cm_demo_invoice_abc123",
    "numero": "F-00425",
    "amount": 1210,
    "currency": "EUR",
    "fingerprint": "a1b2c3d4e5f691f50d79eea2fd8dd17083461a9c7caa49e2b1fc2fb189964861",
    "aeat_csv_url": "https://aeat.volkern.app/csv/cm_demo_invoice_abc123.csv",
    "acceptedAt": "2026-10-18T10:49:51.656Z"
  }
}'

Reemplaza <TIMESTAMP> y <SIGNATURE> con los valores actuales mostrados arriba, o ejecuta el comando y mira los headers del request recibido en tu servidor.

Response

Envía una request para ver aquí la respuesta de tu endpoint.

Verifica la firma en tu servidor

Compara la firma entrante con un HMAC-SHA256 calculado sobre $${X-Volkern-Timestamp}.$${rawBody} con crypto.timingSafeEqual.

Node.js (Express)

import crypto from 'node:crypto'

app.post('/webhook', (req, res) => {
  const ts = req.header('X-Volkern-Timestamp')
  const sigHeader = req.header('X-Volkern-Signature') || ''
  const match = sigHeader.match(/^sha256=([a-f0-9]+)$/)
  if (!ts || !match) return res.status(400).send('Invalid signature header')

  const expected = crypto
    .createHmac('sha256', process.env.VOLKERN_WEBHOOK_SECRET)
    .update(`${ts}.${req.rawBody}`)
    .digest('hex')

  if (
    expected.length !== match[1].length ||
    !crypto.timingSafeEqual(
      Buffer.from(expected, 'hex'),
      Buffer.from(match[1], 'hex'),
    )
  ) {
    return res.status(401).send('Bad signature')
  }
  // OK → procesa req.body
  return res.status(200).send({ received: true })
})

Python (Flask)

import hmac, hashlib
from flask import request, abort

@app.post('/webhook')
def webhook():
    ts = request.headers.get('X-Volkern-Timestamp')
    sig_header = request.headers.get('X-Volkern-Signature', '')
    if not ts or not sig_header.startswith('sha256='):
        abort(400)

    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        f'{ts}.{request.get_data()}'.encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, sig_header.removeprefix('sha256=')):
        abort(401)
    # OK → procesa request.json
    return {'received': True}, 200