Files
reformix-hackaton/mvp/b2c/api-docs/README.md
Carlos Narro 35ba2f28fe Documenta el EP de ingesta y los webhooks salientes
api-docs/README.md: método/URL, auth Bearer, esquema del cuerpo (items
foto/texto, zona, momento, flags), respuesta y errores (401/404/422), ejemplos
curl de los 5 casos, y los payloads de los 3 webhooks salientes (perfil,
entrega WhatsApp, arranque WhatsApp) con cómo el flujo externo devuelve las
"después" por el mismo EP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:09:59 +02:00

164 lines
5.8 KiB
Markdown

# API — Ingesta del perfil del lead
Endpoint único y asíncrono para enriquecer el perfil de un lead del funnel B2C. Cada llamada puede
traer **imágenes, imagen+texto o solo texto**, etiquetado por **zona** y, en fotos, por **momento**
(antes/después). Lo usan tanto el formulario web como los flujos externos (agente de llamada, bot de
WhatsApp, generador de renders).
## Endpoint
```
POST /api/leads/:id/ingesta
```
- `:id` = UUID del lead (el que crea el funnel al capturar nombre/teléfono/email).
- **Auth:** header `Authorization: Bearer <FUNNEL_API_KEY>`. Sin él, o con clave incorrecta → `401`.
- **Content-Type:** `application/json`.
## Cuerpo (JSON)
| Campo | Tipo | Notas |
| --- | --- | --- |
| `items` | array | Lista de items `foto` o `texto`. Por defecto `[]`. |
| `perfilCompleto` | boolean | Opcional. `true` señala al flujo externo que genere renders/agente. |
| `finalizar` | boolean | Opcional. `true` construye el PDF y lo entrega (email + señal WhatsApp). |
Debe llegar **al menos un item o un flag**; una llamada totalmente vacía → `422`.
### Item `foto`
| Campo | Tipo | Notas |
| --- | --- | --- |
| `tipo` | `"foto"` | Obligatorio. |
| `imagen` | string | Obligatorio. Data URI (`data:image/...;base64,...`) o URL `http(s)`. |
| `zona` | enum | Opcional: `cocina`,`bano`,`salon`,`comedor`,`integral`,`otro`. Si falta, se asume el tipo de reforma del lead. |
| `momento` | `"antes"`\|`"despues"` | Por defecto `"antes"`. Los renders del flujo externo se mandan como `despues`. |
| `orden` | number | Opcional. Si falta, continúa el máximo actual del lead. |
### Item `texto`
| Campo | Tipo | Notas |
| --- | --- | --- |
| `tipo` | `"texto"` | Obligatorio. |
| `texto` | string | Obligatorio, no vacío. Ej. `"suelo premium"`. |
| `zona` | enum | Opcional (mismo enum que en `foto`). |
## Respuesta
`200`:
```json
{
"ok": true,
"fotos": 1,
"notas": 1,
"perfilSenalado": false,
"finalizado": null
}
```
- `fotos` / `notas`: cuántos items de cada tipo se guardaron.
- `perfilSenalado`: `true` si `perfilCompleto` disparó el webhook y respondió ok.
- `finalizado`: `null` si no se pidió `finalizar`; si sí,
`{ ok, emailEnviado, whatsappSenal }`.
### Códigos de error
| Código | Cuándo |
| --- | --- |
| `401` | Falta `Authorization: Bearer`, o la clave no coincide con `FUNNEL_API_KEY`. |
| `404` | El lead `:id` no existe. |
| `422` | JSON inválido, item mal formado, o llamada vacía (sin items ni flag). |
## Ejemplos (curl)
Sustituye `LEAD`, `KEY` y el host según tu entorno.
```bash
# 1) Solo texto: añade un dato a la zona baño
curl -X POST "$HOST/api/leads/$LEAD/ingesta" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"items":[{"tipo":"texto","zona":"bano","texto":"suelo premium"}]}'
# 2) Solo imagen (antes)
curl -X POST "$HOST/api/leads/$LEAD/ingesta" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"items":[{"tipo":"foto","zona":"bano","imagen":"https://cdn/ejemplo/bano-antes.jpg"}]}'
# 3) Imagen + texto en la misma llamada
curl -X POST "$HOST/api/leads/$LEAD/ingesta" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"items":[
{"tipo":"foto","zona":"cocina","imagen":"data:image/jpeg;base64,..."},
{"tipo":"texto","zona":"cocina","texto":"encimera de cuarzo"}
]}'
# 4) Perfil completo: que el flujo externo genere renders / corra el agente
curl -X POST "$HOST/api/leads/$LEAD/ingesta" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"perfilCompleto":true}'
# 5) Devolver renders "después" y finalizar (genera PDF + email + señal WhatsApp)
curl -X POST "$HOST/api/leads/$LEAD/ingesta" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"items":[{"tipo":"foto","zona":"cocina","momento":"despues","imagen":"https://cdn/render-cocina.jpg"}],"finalizar":true}'
```
## Webhooks salientes (hacia el flujo externo)
La app **emite** estas señales; el flujo externo (n8n/generador) las recibe y, cuando toca,
devuelve los resultados llamando de nuevo a este mismo EP (las "después" con `finalizar:true`).
Cada webhook es opcional: sin su URL en el entorno, la señal simplemente no se manda.
### `PERFIL_WEBHOOK_URL` — perfil completo
Disparado por `perfilCompleto:true`. Payload:
```json
{
"leadId": "uuid",
"cliente": { "nombre": "...", "telefono": "...", "email": "...", "provincia": "..." },
"reforma": { "tipo": "cocina", "m2Suelo": 12, "calidad": "media",
"estructural": false, "urgencia": "media", "presupuestoTarget": 800000 },
"empresa": { "tenantId": "uuid", "nombre": "Reformas Ejemplo" },
"zonas": [
{ "zona": "cocina", "notas": ["encimera de cuarzo"],
"fotos": { "antes": ["url", "..."], "despues": [] } }
]
}
```
> El flujo externo genera los renders y los devuelve como items `foto` con `momento:"despues"`
> por `POST /api/leads/:id/ingesta`, y cierra con `finalizar:true`.
### `WHATSAPP_WEBHOOK_URL` — entrega del PDF
Disparado por `finalizar:true`. Payload:
```json
{
"leadId": "uuid",
"telefono": "+34...",
"nombre": "...",
"empresa": "Reformas Ejemplo",
"pdfBase64": "JVBERi0xLj...",
"filename": "presupuesto-nombre.pdf"
}
```
### `WHATSAPP_START_WEBHOOK_URL` — arranque de conversación
Disparado cuando el lead elige continuar por WhatsApp en el funnel. Payload:
```json
{ "leadId": "uuid", "telefono": "+34...", "nombre": "...", "empresa": "Reformas Ejemplo" }
```
## Notas
- **Storage:** las imágenes se guardan tal cual se reciben (data URI o URL) en `lead_fotos.url`;
las notas en `lead_notas`. Ver `mvp/b2c/db-schema/`.
- **Email:** la entrega por email es real vía SMTP (`SMTP_*` + `EMAIL_FROM`). Sin configurar,
`finalizado.emailEnviado` será `false` y el evento queda marcado como simulado.
- Variables de entorno: ver `mvp/b2c/.env.example`.