Integrar tu ERP con Hapee
Los flujos completos, el mapeo de campos y el checklist de salida. Escrita para que alcance sin nosotros al lado.
Antes de escribir una línea
Tres cosas que definen todo lo demás:
- La subcuenta sale de la credencial. Nunca se manda por parámetro. Una credencial pertenece a una subcuenta y solo puede leer y escribir ahí.
- Hay dos formas de enterarte de lo que pasa y sirven exactamente lo mismo: webhooks (te avisamos) o
GET /events(consultas tú). Eliges según si tu ERP puede exponer una URL pública, no según qué quieres recibir. - Toda escritura acepta
Idempotency-Key. Un reintento por corte de red no duplica nada. Úsalo desde el primer día, no cuando aparezca el problema.
curl https://beta.hapee.ai/api/v1/me \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL"
Si esto anda, el resto anda.
¿API o workflows?
Hay tres formas de conectar y no compiten: se usan juntas. La diferencia real es quién escribe código.
| Qué quieres hacer | Camino | Quién lo hace |
|---|---|---|
| Cargar miles de clientes y productos | API + lote | Tu equipo técnico |
| «Negocio ganado → avisa al ERP» | Workflow | Tu equipo comercial, sin código |
| «El ERP avisa que se pagó» | Trigger de webhook | Una llamada HTTP, sin credencial |
| Sync incremental sin perder nada | API + webhooks | Tu equipo técnico |
Con workflows, sin escribir código del lado de Hapee
Dentro de Hapee, en el constructor de workflows, hay dos piezas que hablan con tu sistema:
- Nodo «Webhook saliente» — se pone después del paso que te interese
(por ejemplo, «el negocio pasó a Ganado») y hace un
POSTa tu URL. Se configura URL, método, cuerpo JSON con variables del contacto, cabeceras y un secreto para firmar. - Activador «Webhook recibido» — te da una URL a la que tu sistema le hace
POSTpara arrancar el workflow. No necesita credencial: alcanza con el token que va en la propia URL.
Es el camino más rápido para empezar: lo arma la persona que ya usa Hapee, en una tarde, sin esperar a nadie.
⚠️ Un 4xx tuyo NO detiene el workflow. El motor solo reintenta cuando tu
sistema responde 5xx. Si responde 401 porque el token está mal, o
400 porque el cuerpo no le gusta, el workflow sigue como si hubiera
funcionado y nadie se entera.
El código de respuesta queda en la variable _http_status. Pon siempre un
nodo «Si / Si no» que lo revise justo después del webhook saliente, y manda el caso malo a
una notificación interna. Sin eso, el día que tu token venza, las ventas «se avisaron» y no
llegaron.
En el registro de ejecución del workflow ese paso aparece marcado como «sin efecto», con el código HTTP al lado — pero eso lo ve quien entra a mirar, y el nodo «Si / Si no» avisa solo.
Cuándo conviene la API en vez del workflow
| Workflow | API + webhooks | |
|---|---|---|
| Reintentos si tu sistema está caído | 5 intentos, ~1 hora | 12 intentos, ~24 horas |
| Registro de entregas consultable | En el registro del workflow | Sí, con reenvío manual |
| Un 4xx tuyo | Sigue como si nada | Se reintenta y queda registrado |
| Firma | Secreto opcional que configuras | HMAC con marca de tiempo |
| Carga masiva | No | Hasta 100 por llamada |
| Quién lo configura | El equipo comercial | El equipo técnico |
Nuestra recomendación: empieza con workflows para probar la conexión de punta a punta, y pasa a la API lo que no se puede perder — cobranza, facturas y la carga inicial.
Fuente de verdad: quién manda sobre qué
Hay que acordar esto antes de sincronizar nada, porque decide qué pasa cuando los dos lados cambian el mismo dato. La propuesta por omisión:
| Entidad | Manda | Motivo |
|---|---|---|
| Datos tributarios (RUT, razón social, giro) | ERP | Es lo que va en el DTE |
| Productos y precios de lista | ERP | Es donde se mantiene el catálogo |
| Facturas, notas de crédito, pagos | ERP | El SII solo conoce al ERP |
| Etapa del negocio, actividad comercial | Hapee | Es donde trabaja el equipo de ventas |
| Contactos y su historial de conversación | Hapee | Es donde ocurren |
| Datos de contacto (teléfono, correo) | el último que lo tocó | Los dos son válidos |
Lo que no puede pasar es no decidirlo. Sin este acuerdo, cada lado pisa al otro y el síntoma aparece semanas después como «se borran los datos».
Flujo A — Carga inicial
Una sola vez, al conectar. Usa POST /{recurso}/batch con hasta 100 items por llamada. El lote no se aborta ante un item malo: entran los que puede y te dice cuáles fallaron.
curl -X POST https://beta.hapee.ai/api/v1/companies/batch \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: carga-inicial-empresas-lote-001" \
-d '{
"items": [
{"external_id": "ERP-CLI-4471", "rut": "76.543.210-K",
"nombre": "Comercial Andes SpA",
"email": "contacto@andes.cl"},
{"external_id": "ERP-CLI-4472", "rut": "77.111.222-3",
"nombre": "Distribuidora Sur Ltda"}
]
}'
{
"job_id": 812,
"total": 2, "succeeded": 2, "failed": 0,
"results": [
{"index": 0, "ok": true, "status": 201, "created": true, "data": {"id": 9001}},
{"index": 1, "ok": true, "status": 201, "created": true, "data": {"id": 9002}}
]
}
Tres cosas que te ahorran un dolor de cabeza
- Manda siempre
external_idcon el id de tu ERP. Es lo que hace que la carga se pueda repetir sin duplicar: la segunda corrida actualiza en vez de crear ("created": false). - El
job_idte deja releer el resultado conGET /jobs/{id}si pierdes la respuesta. UnPOSTde 100 items cuyo resultado se pierde por un corte no se puede repetir a ciegas; con el job, se consulta. - Carga empresas antes que contactos, para poder asociarlos.
Orden recomendado: companies → product-categories y products → contacts.
Flujo B — Sincronización incremental
ERP → Hapee
Usa upsert, no create. Emparejar es la parte delicada y la precedencia es determinista:
external_id > rut > email > phone
Se prueba un criterio a la vez, en orden, y gana el primero que encuentra algo. Puedes fijarlo con match_by:
curl -X POST https://beta.hapee.ai/api/v1/contacts/upsert \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sync-contacto-CON-991-v3" \
-d '{
"match_by": "external_id",
"external_id": "ERP-CON-991",
"email": "jefe.compras@andes.cl",
"first_name": "Rodrigo", "last_name": "Pinto",
"phone": "+56912345678",
"custom_fields": {"payment_terms": "30 días"}
}'
match_by es exclusivo. Si pides emparejar por external_id y no hay coincidencia, se crea — no cae al correo. Es a propósito: caer sería desobedecerte justo cuando fuiste explícito, y podría pisar a alguien que tu ERP considera otra persona.
El RUT se valida. Un dígito verificador equivocado devuelve 422 y no se guarda. Un RUT mal guardado no identifica a nadie y además envenena toda deduplicación futura: la próxima carga con el RUT correcto no lo encuentra y crea un duplicado.
Hapee → ERP
Dos opciones, mismos datos. Opción 1 — Webhooks (recomendada si puedes exponer una URL https):
curl -X POST https://beta.hapee.ai/api/v1/webhooks/endpoints \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.tuempresa.cl/hooks/hapee",
"description": "ERP producción",
"events": ["deal.won", "quote.accepted", "contact.updated", "company.updated"]
}'
La respuesta trae el secret una sola vez. Ver Webhooks para la verificación de firma.
Opción 2 — GET /events (si el ERP está instalado en la empresa, sin puerto abierto):
curl "https://beta.hapee.ai/api/v1/events?cursor=10432&limit=100" \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL"
{
"data": [
{"id": "evt_10433", "type": "deal.won", "api_version": "v1",
"created_at": "2026-09-10T14:02:11Z", "subaccount_id": 63,
"data": {"id": 5521, "title": "Reposición Q4",
"amount": "4200000", "currency": "CLP"}}
],
"next_cursor": "10433",
"has_more": false
}
Guarda el next_cursor y pásalo en la llamada siguiente. Los eventos vienen del más viejo al más nuevo a propósito: te estás poniendo al día, y hay que procesar en el orden en que ocurrieron. Retención: 30 días.
Flujo C — Negocio ganado → factura
El flujo que justifica la integración.
Hapee Tu ERP
│
│ El equipo marca el negocio como ganado
│
├──── deal.won ─────────────────►│
│ (webhook o /events) │
│ │ Crea la orden de venta
│ │ Emite el DTE ante el SII
│ │
│◄─── PATCH /deals/{id} ─────────┤ Escribe el folio de vuelta
│ invoice_number: "F-8842" │
│ payment_status: "pending"│
│ │
│◄─── POST custom-objects/… ────┤ Publica el documento como
│ │ registro visible en el CRM
│
│ El ejecutivo VE la factura dentro de la ficha del cliente
Paso 1 — Recibes el evento
deal.won trae el negocio completo, con la misma forma que devuelve GET /deals/{id}.
Paso 2 — Escribes de vuelta
curl -X PATCH https://beta.hapee.ai/api/v1/deals/5521 \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: folio-F-8842" \
-d '{"custom_fields": {
"invoice_number": "F-8842",
"payment_status": "pending",
"due_date": "2026-10-10"}}'
Paso 3 — Publicas el documento como registro
Así el ejecutivo ve la factura sin salir de Hapee:
curl -X POST https://beta.hapee.ai/api/v1/custom-objects/{objeto_id}/records \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL" \
-H "Content-Type: application/json" \
-d '{
"external_id": "ERP-DTE-8842",
"values": {
"folio": "F-8842", "tipo_dte": "33",
"monto_neto": "3529412", "iva": "670588", "total": "4200000",
"moneda": "CLP", "fecha_emision": "2026-09-10",
"estado": "emitida",
"url_pdf": "https://erp.tuempresa.cl/dte/8842.pdf"
}
}'
Flujo D — Cobranza
# 1. El ERP avisa que el documento venció impago
curl -X PATCH https://beta.hapee.ai/api/v1/deals/5521 \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL" \
-d '{"custom_fields": {"payment_status": "overdue", "amount_paid": "0"}}'
# 2. Se etiqueta al contacto
curl -X POST https://beta.hapee.ai/api/v1/contacts/8821/tags \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL" \
-d '{"tag": "cobranza-vencida"}'
# 3. Se lo inscribe en el workflow de cobranza
curl -X POST https://beta.hapee.ai/api/v1/workflows/44/enroll \
-H "Authorization: Bearer hp_test_TU_CREDENCIAL" \
-d '{"contacto_id": 8821}'
Desde ahí, Hapee se encarga: los recordatorios, el WhatsApp, el correo. Cuando el cliente paga, el ERP cierra el ciclo con payment_status: "paid" y la etiqueta se quita.
Mapeo de campos
Plantilla para completar con tu equipo. Los campos de Hapee son los que la API expone hoy.
Empresa / Cliente
| Tu ERP | Hapee | Notas |
|---|---|---|
| (id interno) | external_id | Único por subcuenta. Es la llave del sync |
| RUT | rut | Se valida el DV. Se guarda 12345678-K |
| Razón social | razon_social | |
| Nombre de fantasía | nombre | Obligatorio al crear |
| Giro | custom_fields.giro | |
| Correo de facturación | custom_fields.billing_email | Distinto de email |
| Condición de pago | custom_fields.payment_terms | |
| Dirección | direccion, ciudad, region |
Contacto / Persona
| Tu ERP | Hapee | Notas |
|---|---|---|
| (id interno) | external_id | Único por subcuenta |
| RUT | rut | Opcional; puede repetirse entre contactos |
| Nombre / Apellido | first_name / last_name | |
| Correo | email | En minúscula |
| Teléfono | phone | E.164: +56912345678 |
| Empresa | empresa_id | El id de Hapee, no el de tu ERP |
Negocio / Orden de venta
| Tu ERP | Hapee | Notas |
|---|---|---|
| Nº orden de venta | external_id | |
| Folio DTE | custom_fields.invoice_number | |
| Estado de pago | custom_fields.payment_status | pending / paid / overdue |
| Monto pagado | custom_fields.amount_paid | Texto decimal |
| Vencimiento | custom_fields.due_date | YYYY-MM-DD |
| Monto | amount + currency | Texto decimal, no número |
Checklist de salida a producción
Antes de cambiar hp_test_ por hp_live_:
GET /meresponde y muestra la subcuenta correcta- La carga inicial corrió dos veces en sandbox y no duplicó nada
- Todos los registros tienen
external_idcon el id de tu ERP - Los RUT pasan la validación (sin
422en el lote) - El endpoint de webhook verifica la firma y rechaza una firma inválida
- El receptor deduplica por el
iddel evento (el orden no está garantizado y una entrega puede repetirse) - Toda escritura manda
Idempotency-Key - Se probó un reintento con la misma clave y no duplicó
- El flujo C completo funciona de punta a punta en sandbox
- Está acordada la fuente de verdad por entidad
- Hay un lugar donde el ERP registra los errores de la API, con el
request_idde la respuesta - Si el ERP tiene IP fija, está cargada en la allowlist de la credencial
El cambio a producción
- Pedile a la agencia una credencial
hp_live_de la subcuenta real - Cambia la credencial y la URL del webhook
- Corre la carga inicial una vez
- Verifica con
GET /eventsque los eventos están llegando
No hace falta cambiar nada más. El sandbox y producción hablan la misma API.
Errores frecuentes
| Código | Qué pasó | Qué hacer |
|---|---|---|
401 invalid_key | La credencial no existe o fue revocada | Pide una nueva |
403 ip_no_permitida | Tu IP no está en la allowlist | Pide que la agreguen |
403 no_es_sandbox | Credencial hp_test_ sobre subcuenta real | Usa hp_live_ |
404 not_found | No existe, o no es de tu subcuenta | Revisa el id y la credencial |
409 idempotency_key_reusada | Misma clave, cuerpo distinto | Usa una clave nueva |
422 validation_error | Mira details[]: dice el campo y el problema | |
429 | Pasaste el límite | Espera lo que dice Retry-After |
Guarda siempre el X-Request-Id de la respuesta. Es lo que nos permite encontrar tu llamada exacta en nuestros registros. Escríbenos a info@hapee.ai.