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:

  1. 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í.
  2. 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.
  3. 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 hacerCaminoQuién lo hace
Cargar miles de clientes y productosAPI + loteTu equipo técnico
«Negocio ganado → avisa al ERP»WorkflowTu equipo comercial, sin código
«El ERP avisa que se pagó»Trigger de webhookUna llamada HTTP, sin credencial
Sync incremental sin perder nadaAPI + webhooksTu 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:

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

WorkflowAPI + webhooks
Reintentos si tu sistema está caído5 intentos, ~1 hora12 intentos, ~24 horas
Registro de entregas consultableEn el registro del workflowSí, con reenvío manual
Un 4xx tuyoSigue como si nadaSe reintenta y queda registrado
FirmaSecreto opcional que configurasHMAC con marca de tiempo
Carga masivaNoHasta 100 por llamada
Quién lo configuraEl equipo comercialEl 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:

EntidadMandaMotivo
Datos tributarios (RUT, razón social, giro)ERPEs lo que va en el DTE
Productos y precios de listaERPEs donde se mantiene el catálogo
Facturas, notas de crédito, pagosERPEl SII solo conoce al ERP
Etapa del negocio, actividad comercialHapeeEs donde trabaja el equipo de ventas
Contactos y su historial de conversaciónHapeeEs 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

Orden recomendado: companiesproduct-categories y productscontacts.

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 ERPHapeeNotas
(id interno)external_idÚnico por subcuenta. Es la llave del sync
RUTrutSe valida el DV. Se guarda 12345678-K
Razón socialrazon_social
Nombre de fantasíanombreObligatorio al crear
Girocustom_fields.giro
Correo de facturacióncustom_fields.billing_emailDistinto de email
Condición de pagocustom_fields.payment_terms
Direccióndireccion, ciudad, region

Contacto / Persona

Tu ERPHapeeNotas
(id interno)external_idÚnico por subcuenta
RUTrutOpcional; puede repetirse entre contactos
Nombre / Apellidofirst_name / last_name
CorreoemailEn minúscula
TeléfonophoneE.164: +56912345678
Empresaempresa_idEl id de Hapee, no el de tu ERP

Negocio / Orden de venta

Tu ERPHapeeNotas
Nº orden de ventaexternal_id
Folio DTEcustom_fields.invoice_number
Estado de pagocustom_fields.payment_statuspending / paid / overdue
Monto pagadocustom_fields.amount_paidTexto decimal
Vencimientocustom_fields.due_dateYYYY-MM-DD
Montoamount + currencyTexto decimal, no número

Checklist de salida a producción

Antes de cambiar hp_test_ por hp_live_:

El cambio a producción

  1. Pedile a la agencia una credencial hp_live_ de la subcuenta real
  2. Cambia la credencial y la URL del webhook
  3. Corre la carga inicial una vez
  4. Verifica con GET /events que los eventos están llegando

No hace falta cambiar nada más. El sandbox y producción hablan la misma API.

Errores frecuentes

CódigoQué pasóQué hacer
401 invalid_keyLa credencial no existe o fue revocadaPide una nueva
403 ip_no_permitidaTu IP no está en la allowlistPide que la agreguen
403 no_es_sandboxCredencial hp_test_ sobre subcuenta realUsa hp_live_
404 not_foundNo existe, o no es de tu subcuentaRevisa el id y la credencial
409 idempotency_key_reusadaMisma clave, cuerpo distintoUsa una clave nueva
422 validation_errorMira details[]: dice el campo y el problema
429Pasaste el límiteEspera 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.