Webhooks

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:

EventoDescripción
expense.createdSe dispara cuando se crea un nuevo gasto en el sistema
expense.approvedSe dispara cuando un gasto alcanza el estado de aprobación final
user.createdSe dispara cuando se crea una nueva cuenta de usuario
user.updatedSe 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

CampoTipoDescripción
idstring (UUID)Identificador único del gasto
descriptionstringDescripción del gasto
amountstringMonto del gasto (formato decimal como string)
usd_amountstringMonto convertido a USD
currencystringCódigo de moneda ISO 4217 (ej: "USD", "CLP")
statestringEstado actual del gasto (pending, approved, rejected, etc.)
datestring (ISO 8601)Fecha del documento del gasto
expense_numberintegerNúmero secuencial del gasto
cost_centerobjectCentro de costo asociado
categoryobjectCategoría del gasto
userobjectUsuario que creó el gasto
net_amountstringMonto neto (antes de impuestos)
tax_amountstringMonto de impuestos
tax_ratestringTasa 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

CampoTipoDescripción
expense_idstring (UUID)Identificador único del gasto
descriptionstringDescripción del gasto
amountstringMonto del gasto
approved_byarrayLista de usuarios que aprobaron el gasto
final_approval_timestampstring (ISO 8601)Timestamp de la aprobación final
subsidiary_idstringID de la subsidiaria
idempotency_keystringClave ú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

CampoTipoDescripción
userobjectInformación del usuario creado
subsidiary_idstringID de la subsidiaria donde se creó el usuario
enterprise_idstringID de la empresa
created_atstring (ISO 8601)Timestamp de creación del usuario
idempotency_keystringClave ú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

CampoTipoDescripción
userobjectInformación actualizada del usuario
changed_fieldsarrayLista de nombres de campos que fueron modificados
updated_by_user_idstringID del usuario que realizó la actualización (null si fue el sistema)
updated_atstring (ISO 8601)Timestamp de la actualización
idempotency_keystringClave ú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

  1. Responde rápido: Tu endpoint debe responder en menos de 5 segundos
  2. Procesamiento asíncrono: Encola eventos complejos para procesamiento posterior
  3. Idempotencia: Usa el idempotency_key para evitar duplicados
  4. 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:


Última actualización: Noviembre 2025
Versión del documento: 1.0