Introducción
Los webhooks de Xpendit permiten que tu aplicación reciba notificaciones en tiempo real cuando ocurren eventos específicos en la plataforma. En lugar de consultar constantemente la API para detectar cambios, los webhooks envían datos automáticamente a tu endpoint HTTP cuando se produce un evento.
Configuración
⚠️ Nota Importante: La configuración de webhooks es manejada internamente por el equipo de Xpendit. Si necesitas configurar webhooks para tu integración, por favor contacta al equipo de TI de Xpendit.
Características Técnicas
- Protocolo: HTTPS (requerido)
- Método: POST
- Formato: JSON
- Autenticación: Firma HMAC en headers (proporcionada por Svix)
- Idempotencia: Cada evento incluye un
idempotency_keyúnico - Reintentos: Sistema automático de reintentos con backoff exponencial
Eventos Disponibles
Xpendit ofrece los siguientes eventos webhook:
| Evento | Descripción |
|---|---|
expense.created | Se dispara cuando se crea un nuevo gasto en el sistema |
expense.approved | Se dispara cuando un gasto alcanza el estado de aprobación final |
user.created | Se dispara cuando se crea una nueva cuenta de usuario |
user.updated | Se dispara cuando se modifica la información de un usuario |
Estructura de los Eventos
Todos los eventos webhook siguen una estructura común con los siguientes elementos:
- event_type: Identificador del tipo de evento (ej:
expense.created) - payload_version: Versión del esquema del payload (actualmente
"1.0") - idempotency_key: Clave única para garantizar idempotencia en el procesamiento
1. expense.created
Se dispara cuando se crea un nuevo gasto en el sistema.
Estructura del Payload
{
"event_type": "expense.created",
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Office supplies purchase",
"expense_type": "receipt",
"currency": "USD",
"amount": "150.00",
"usd_amount": "150.00",
"state": "pending",
"date": "2024-01-15T10:30:00Z",
"expense_number": 12345,
"content_type": "invoice",
"additional_questions": {},
"cost_center": {
"id": "cc-001",
"name": "Operations",
"internal_id": "OPS-001"
},
"category": {
"id": "cat-001",
"name": "Office Supplies",
"internal_id": "SUP-001"
},
"user": {
"id": "user-001",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+1234567890",
"internal_id": "EMP-001",
"government_id": null
},
"fund": null,
"is_active": true,
"created_at": "2024-01-15T10:30:00Z",
"additional_file_url": null,
"approved_by": [],
"url": "https://app.xpendit.com/expenses/550e8400-e29b-41d4-a716-446655440000",
"document_id": "doc-001",
"expense_document_type": "invoice",
"expense_document_type_country": "US",
"provider": "Office Depot",
"provider_identifier": "OD-12345",
"provider_address": "123 Main St, New York, NY",
"provider_business_line": null,
"branch_address": null,
"payment_method": null,
"net_amount": "130.00",
"tax_amount": "20.00",
"tax_rate": "0.15"
}
Campos Principales
| Campo | Tipo | Descripción |
|---|---|---|
id | string (UUID) | Identificador único del gasto |
description | string | Descripción del gasto |
amount | string | Monto del gasto (formato decimal como string) |
usd_amount | string | Monto convertido a USD |
currency | string | Código de moneda ISO 4217 (ej: "USD", "CLP") |
state | string | Estado actual del gasto (pending, approved, rejected, etc.) |
date | string (ISO 8601) | Fecha del documento del gasto |
expense_number | integer | Número secuencial del gasto |
cost_center | object | Centro de costo asociado |
category | object | Categoría del gasto |
user | object | Usuario que creó el gasto |
net_amount | string | Monto neto (antes de impuestos) |
tax_amount | string | Monto de impuestos |
tax_rate | string | Tasa impositiva (formato decimal) |
2. expense.approved
Se dispara cuando un gasto alcanza el estado de aprobación final (todas las aprobaciones requeridas han sido obtenidas).
Estructura del Payload
{
"event_type": "expense.approved",
"payload_version": "1.0",
"expense_id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Office supplies purchase",
"currency": "USD",
"amount": "150.00",
"usd_amount": "150.00",
"expense_number": 12345,
"user": {
"id": "user-001",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+1234567890",
"internal_id": "EMP-001",
"government_id": null
},
"cost_center": {
"id": "cc-001",
"name": "Operations",
"internal_id": "OPS-001"
},
"category": {
"id": "cat-001",
"name": "Office Supplies",
"internal_id": "SUP-001"
},
"approved_by": [
{
"id": "user-002",
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"phone": "+1987654321",
"internal_id": "MGR-001",
"government_id": null
}
],
"final_approval_timestamp": "2024-01-16T14:30:00Z",
"created_at": "2024-01-15T10:30:00Z",
"subsidiary_id": "sub-001",
"additional_file_url": null,
"idempotency_key": "expense.approved:550e8400-e29b-41d4-a716-446655440000:1705416600"
}
Campos Principales
| Campo | Tipo | Descripción |
|---|---|---|
expense_id | string (UUID) | Identificador único del gasto |
description | string | Descripción del gasto |
amount | string | Monto del gasto |
approved_by | array | Lista de usuarios que aprobaron el gasto |
final_approval_timestamp | string (ISO 8601) | Timestamp de la aprobación final |
subsidiary_id | string | ID de la subsidiaria |
idempotency_key | string | Clave única para procesamiento idempotente |
3. user.created
Se dispara cuando se crea una nueva cuenta de usuario en el sistema.
Estructura del Payload
{
"event_type": "user.created",
"payload_version": "1.0",
"user": {
"id": "user-001",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+1234567890",
"internal_id": "EMP-001",
"government_id": null
},
"subsidiary_id": "sub-001",
"enterprise_id": "ent-001",
"created_at": "2024-01-15T10:30:00Z",
"idempotency_key": "user.created:user-001:1705316400"
}
Campos Principales
| Campo | Tipo | Descripción |
|---|---|---|
user | object | Información del usuario creado |
subsidiary_id | string | ID de la subsidiaria donde se creó el usuario |
enterprise_id | string | ID de la empresa |
created_at | string (ISO 8601) | Timestamp de creación del usuario |
idempotency_key | string | Clave única para procesamiento idempotente |
4. user.updated
Se dispara cuando se modifica la información de un usuario existente.
Estructura del Payload
{
"event_type": "user.updated",
"payload_version": "1.0",
"user": {
"id": "user-001",
"first_name": "John",
"last_name": "Doe-Smith",
"email": "[email protected]",
"phone": "+1234567890",
"internal_id": "EMP-001",
"government_id": null
},
"subsidiary_id": "sub-001",
"enterprise_id": "ent-001",
"changed_fields": [
"last_name",
"phone"
],
"updated_by_user_id": "admin-001",
"updated_at": "2024-01-15T15:45:00Z",
"idempotency_key": "user.updated:user-001:1705337100"
}
Campos Principales
| Campo | Tipo | Descripción |
|---|---|---|
user | object | Información actualizada del usuario |
changed_fields | array | Lista de nombres de campos que fueron modificados |
updated_by_user_id | string | ID del usuario que realizó la actualización (null si fue el sistema) |
updated_at | string (ISO 8601) | Timestamp de la actualización |
idempotency_key | string | Clave única para procesamiento idempotente |
Nota: Este evento NO incluye los valores anteriores de los campos modificados por razones de privacidad y seguridad. El campo changed_fields indica qué campos cambiaron, pero para obtener los valores actualizados completos, consulta el objeto user en el payload.
Idempotencia
Cada evento incluye un campo idempotency_key único que puedes usar para evitar procesar el mismo evento múltiples veces:
def process_webhook(event_data):
idempotency_key = event_data.get('idempotency_key')
# Check if already processed
if is_already_processed(idempotency_key):
return {"status": "already_processed"}
# Process the event
result = handle_event(event_data)
# Store idempotency key
mark_as_processed(idempotency_key)
return result
Manejo de Errores y Reintentos
Códigos de Respuesta
Tu endpoint debe responder con:
- 200-299: Evento procesado exitosamente
- 400-499: Error del cliente (no se reintentará)
- 500-599: Error del servidor (se reintentará)
Política de Reintentos
Xpendit usa Svix para la entrega de webhooks, que implementa reintentos automáticos:
- Primer intento: Inmediato
- Segundo intento: ~5 minutos después
- Tercer intento: ~30 minutos después
- Siguientes intentos: Backoff exponencial hasta 24 horas
Recomendaciones
- Responde rápido: Tu endpoint debe responder en menos de 5 segundos
- Procesamiento asíncrono: Encola eventos complejos para procesamiento posterior
- Idempotencia: Usa el
idempotency_keypara evitar duplicados - Logging: Registra todos los eventos recibidos para debugging
Ejemplo de Implementación
Webhook de Prueba
Durante el desarrollo, puedes usar herramientas como:
- ngrok: Para exponer tu servidor local a internet
- Webhook.site: Para inspeccionar payloads sin código
- Svix Play: Plataforma de testing de Svix
Ejemplo con ngrok
# Instalar ngrok
npm install -g ngrok
# Exponer tu servidor local
ngrok http 8000
# Usa la URL generada (ej: https://abc123.ngrok.io) como tu webhook endpoint
Preguntas Frecuentes
¿Cómo configuro mis webhooks?
La configuración de webhooks es manejada internamente por Xpendit. Contacta al equipo de TI de Xpendit para configurar tus endpoints.
¿Puedo recibir solo ciertos eventos?
Sí, durante la configuración puedes especificar qué eventos deseas recibir.
¿Qué pasa si mi endpoint está caído?
Svix reintentará la entrega automáticamente siguiendo la política de reintentos descrita anteriormente. Los eventos se almacenan por hasta 7 días.
¿Puedo ver el historial de webhooks enviados?
Contacta al equipo de TI de Xpendit para acceso al dashboard de Svix donde puedes ver el historial y estado de entrega.
¿Los webhooks incluyen datos sensibles?
Los webhooks incluyen solo la información necesaria para la integración. No se envían contraseñas, tokens de autenticación ni información de tarjetas de crédito.
Soporte
Para configuración de webhooks, problemas técnicos o preguntas:
- Email: [email protected]
- Equipo TI: Contacta internamente al equipo de TI de Xpendit
Última actualización: Noviembre 2025
Versión del documento: 1.0