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:
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
| Esdeveniment | Quan |
|---|---|
invoice.created | Es crea un esborrany. |
invoice.updated | S'edita una factura. |
invoice.issued | Es numera i emet una factura. |
invoice.cancelled | S'anul·la una factura emesa. |
verifactu.submitted | El registre s'ha enviat a l'AEAT. |
verifactu.accepted | L'AEAT l'ha acceptat. |
verifactu.rejected | L'AEAT l'ha rebutjat. |
verifactu.error | Hi ha hagut un error tècnic. |
Ignora els tipus desconeguts: afegir-ne de nous és un canvi compatible.
Estructura
{
"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.livemodesepara 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.
- Calcula l'HMAC amb el cos cru, abans de deserialitzar JSON.
- Compara'l amb
v1en temps constant. - Rebutja timestamps fora d'una tolerància curta, per exemple 300 segons.
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
)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 Gonedesactiva l'endpoint.- El lliurament és at-least-once: deduplica per
idi respon2xxals 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.