Ves al contingut

Webhooks ​

Bilteo notifica els esdeveniments rellevants amb peticions POST a una URL teva. És la manera recomanada de tancar el cicle asíncron Veri*Factu: emets i reps verifactu.accepted o verifactu.rejected quan respon l'AEAT.

Conceptes ​

  • Endpoint: URL https:// receptora.
  • Esdeveniment: fet que ha passat, com invoice.issued.
  • Lliurament: cada intent d'enviar un esdeveniment.
  • Secret: clau amb què Bilteo signa cada lliurament.

Configurar un endpoint ​

Pots gestionar els endpoints al panell de Bilteo o a l'API pública. Amb una clau API o un token OAuth2 que tingui webhooks:write:

bash
curl -X POST https://api.test.bilteo.com/api/v1/webhook-endpoints \
  -H "Authorization: Bearer bk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://exemple.com/webhooks","description":"La meva integració","events":["invoice.issued","verifactu.*"]}'

La resposta inclou el secret de signatura; desa'l amb seguretat. GET /webhook-endpoints i GET /webhook-endpoints/{id} requereixen webhooks:read. PATCH /webhook-endpoints/{id} i DELETE /webhook-endpoints/{id} requereixen webhooks:write. Els llistats fan servir limit i cursor; les respostes de lectura i edició no inclouen el secret. Pots subscriure't a un tipus exacte, a invoice.* / verifactu.* o a *.

El secret es pot consultar al panell. A l'API pública el pots regenerar amb POST /webhook-endpoints/{id}/actions/roll-secret (webhooks:write), que retorna el nou valor. En regenerar-lo, l'anterior deixa de ser vàlid immediatament i els lliuraments pendents es signen amb el nou.

Esdeveniments disponibles ​

EsdevenimentQuan
invoice.createdEs crea un esborrany.
invoice.updatedS'edita una factura.
invoice.issuedEs numera i emet una factura.
invoice.cancelledS'anul·la una factura emesa.
verifactu.submittedEl registre s'ha enviat a l'AEAT.
verifactu.acceptedL'AEAT l'ha acceptat.
verifactu.rejectedL'AEAT l'ha rebutjat.
verifactu.errorHi ha hagut un error tècnic.

Ignora els tipus desconeguts: afegir-ne de nous és un canvi compatible.

Estructura ​

json
{
  "id": "evt_0192f3a4-...",
  "type": "verifactu.accepted",
  "created": 1716120000,
  "tenant_id": "0192...",
  "livemode": true,
  "api_version": "v1",
  "data": {
    "object": {
      "id": "0192...",
      "invoice_id": "0192...",
      "number": 42,
      "state": "accepted",
      "operation_type": "registration",
      "hash": "A1B2C3...",
      "sent_at": "2026-05-19T10:00:03Z"
    }
  }
}
  • id és estable en reintents i reenviaments: utilitza'l per deduplicar.
  • livemode separa producció de l'entorn de proves.
  • created és un timestamp Unix en segons.
  • data.object és un resum; demana el recurs complet a l'API.

Els esdeveniments invoice.* inclouen identificador, tipus, estat, moneda, total, número i client quan existeixen. Els verifactu.* inclouen registre, factura, estat, operació, empremtes, lot i resposta de l'AEAT.

Verifactu Direct API

verifactu.* també cobreix /external-invoices. En aquest cas invoice_id és null; correlaciona amb l'id de l'objecte, que coincideix amb el verifactu_record_id retornat en registrar la factura externa.

El botó d'esdeveniment de prova envia webhook.test per comprovar recepció i signatura; no representa cap operació de negoci.

Capçaleres ​

Content-Type: application/json
User-Agent: Bilteo-Webhooks/1
Bilteo-Event-Id: evt_0192f3a4-...
Bilteo-Event-Type: verifactu.accepted
Bilteo-Signature: t=1716120000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e...

Verificar la signatura ​

v1 = HMAC_SHA256(secret, "<t>.<cos_cru>"), en hexadecimal.

  1. Calcula l'HMAC amb el cos cru, abans de deserialitzar JSON.
  2. Compara'l amb v1 en temps constant.
  3. Rebutja timestamps fora d'una tolerància curta, per exemple 300 segons.
python
import hashlib, hmac, time

def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp = int(parts["t"])
    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return (
        hmac.compare_digest(expected, parts.get("v1", ""))
        and abs(time.time() - timestamp) <= tolerance
    )
js
const crypto = require('crypto')

function verify(secret, header, rawBody, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
  const timestamp = Number(parts.t)
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1 || '')
  return (
    a.length === b.length &&
    crypto.timingSafeEqual(a, b) &&
    Math.abs(Date.now() / 1000 - timestamp) <= toleranceSec
  )
}

Respon 2xx tan aviat com hagis verificat i acceptat l'esdeveniment; processa la feina pesada en segon pla.

Reintents i duplicats ​

  • Sense 2xx, Bilteo reintenta a 1m, 5m, 30m, 2h, 6h, 12h i després cada 24h, fins a uns deu intents.
  • 410 Gone desactiva l'endpoint.
  • El lliurament és at-least-once: deduplica per id i respon 2xx als repetits.
  • L'historial tancat es purga aproximadament als 90 dies; no l'utilitzis com a registre d'auditoria.

Verifica sempre la signatura

Tracta el cos com a no fiable fins que Bilteo-Signature sigui vàlida.

Bones pràctiques ​

  • Verifica signatura i timestamp.
  • Deduplica per evt_….
  • Respon ràpid i processa asíncronament.
  • Ignora tipus desconeguts.
  • Separa proves i producció amb livemode.
  • Recupera el detall del recurs amb el seu id.

Bilteo · Documentació