Documentación para desarrolladores

API de inventario de LotesEnVivoAlpha

Esta es la contraparte de entrada de los webhooks salientes: en vez de que LotesEnVivo avise a otro sistema, aquí tu CRM, ERP, automatización o sistema propio consulta y actualiza directamente tu inventario. Si tu equipo administra todo desde LotesEnVivo, no necesitas usar esta API.

Esta API requiere Plan Empresarial. Con un plan inferior puedes crear la key desde el backoffice, pero cada petición responde 403 plan_ineligible hasta que subas de plan — la key sigue siendo válida y no hay que regenerarla ni cambiar tu integración una vez actualizado el plan.

Qué es y para qué sirve

Una API key (empieza con lak_) le da a un sistema externo permiso para leer y modificar el inventario de lotes de tu workspace mediante peticiones HTTP normales, sin sesión de usuario. Sirve cuando ya tienes otro lugar donde se cierran ventas, se actualizan precios o se automatizan procesos, y quieres que esos cambios se reflejen en LotesEnVivo.

Autenticación

Todas las peticiones deben incluir la key en uno de estos dos headers (si mandas ambos, gana Authorization):

text
Authorization: Bearer lak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# o, si tu herramienta no soporta Bearer:
X-API-Key: lak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Si pausas o revocas una key desde el backoffice, deja de funcionar de inmediato: cualquier integración que la use recibirá 401 unauthorized hasta que la reemplaces.

Guía rápida

Cómo obtener una API key

  1. Entra a Backoffice → Integraciones → API y crea una clave: solo necesita un nombre para identificar el sistema que la usará (ej. "Go High Level", "CRM interno" o "Automatización de ventas").
  2. Al crearla se muestra el valor completo una sola vez. Cópiala en ese momento y guárdala en el sistema externo, idealmente como variable de entorno en tu servidor, nunca en el código fuente.
  3. Revisa el registro de uso de la key en el backoffice si algo no funciona como esperas: ahí ves cada consulta o actualización que hizo el sistema externo y si tuvo éxito o error.

Endpoints

GET/api/v1/lots

Lista los lotes del workspace dueño de la API key, paginado. Solo devuelve features de tipo lot — amenidades y áreas comunes no tienen estatus de venta ni precio y no se exponen aquí.

Query paramDescripción
mapIdOpcional. UUID. Filtra a un solo mapa.
statusOpcional. Uno de available, reserved, sold, unavailable. Este último filtra los lotes sin estatus asignado ("no disponible oficialmente").
externalIdOpcional. Filtra por coincidencia exacta de la clave externa del lote (el identificador que usa tu propio sistema, ej. tu CRM, en vez del código de LotesEnVivo). Un lote sin clave externa nunca aparece en este filtro, sin importar el valor que mandes.
limitOpcional. Entero de 1 a 200. Default 50.
offsetOpcional. Entero ≥ 0. Default 0.

Petición

terminal
curl "https://lotesenvivo.mx/api/v1/lots?status=available&limit=50" \
  -H "Authorization: Bearer lak_TU_API_KEY"

Respuesta 200

200-ok.json
{
  "data": [
    {
      "code": "A-14",
      "externalId": "AC28",
      "mapId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "label": "Lote A-14",
      "featureType": "lot",
      "lotStatus": "available",
      "pricingMode": "total",
      "priceMxn": 850000,
      "pricePerM2Mxn": null,
      "description": "<p>Esquina, doble frente. Financiamiento directo a 12 meses sin intereses.</p>",
      "areaM2": 240.5,
      "frontageM": 12,
      "depthM": 20,
      "constructionAreaM2": null,
      "houseModel": null,
      "blockCode": "A",
      "lotNumber": "14",
      "lotCategory": "residencial",
      "segmentLabel": "Fase 1",
      "deliveryLabel": "Entrega inmediata",
      "priceValidUntil": null,
      "contactName": "Karla Méndez",
      "contactPhone": "+52 55 1234 5678",
      "contactEmail": "karla.mendez@example.com",
      "updatedAt": "2026-09-05T18:32:00.000Z",
      "assignedSeller": { "name": "Luis Torres", "email": "luis.torres@example.com" }
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 1 }
}
GET/api/v1/lots/{code}

Obtiene un lote por su código. code es único por mapa, no por workspace: si tu desarrolladora tiene dos mapas que casualmente usan el mismo código de lote, esta llamada responde 400 validation_error pidiéndote que agregues mapId para desambiguar, en vez de adivinar cuál querías. Recomendado: si tu integración maneja varios mapas, manda mapId desde la primera petición en vez de esperar este error y reintentar — es la práctica sugerida, no solo un parche para el caso ambiguo.

Query paramDescripción
matchByOpcional. code (default) o externalId. Decide cómo se interpreta el valor de la URL: como el código de LotesEnVivo, o como la clave externa que usa tu propio sistema. Ver Clave externa más abajo.
mapIdOpcional. UUID. Solo se exige si el valor (code o externalId) es ambiguo entre mapas, pero si tu integración maneja varios mapas se recomienda mandarlo siempre desde la primera petición — evita el reintento y es más eficiente, no solo un requisito para el caso ambiguo.

Petición

terminal
curl "https://lotesenvivo.mx/api/v1/lots/A-14" \
  -H "Authorization: Bearer lak_TU_API_KEY"
terminal (reintento si el código es ambiguo — o recomendado desde el inicio si manejas varios mapas)
# Si 'A-14' existe en más de un mapa de tu workspace, el paso anterior responde
# 400 validation_error con 'details.matchingMapIds'. Repite la petición agregando mapId:
curl "https://lotesenvivo.mx/api/v1/lots/A-14?mapId=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \
  -H "Authorization: Bearer lak_TU_API_KEY"

# Recomendado: si ya sabes que tu integración maneja varios mapas, manda mapId desde la
# PRIMERA petición (no esperes a que el código resulte ambiguo). Es la práctica sugerida,
# no un parche que solo aplique cuando ya te topaste con el error de arriba.

Respuesta 200

200-ok.json
{
  "data": {
  "code": "A-14",
  "externalId": "AC28",
  "mapId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "label": "Lote A-14",
  "featureType": "lot",
  "lotStatus": "available",
  "pricingMode": "total",
  "priceMxn": 850000,
  "pricePerM2Mxn": null,
  "description": "<p>Esquina, doble frente. Financiamiento directo a 12 meses sin intereses.</p>",
  "areaM2": 240.5,
  "frontageM": 12,
  "depthM": 20,
  "constructionAreaM2": null,
  "houseModel": null,
  "blockCode": "A",
  "lotNumber": "14",
  "lotCategory": "residencial",
  "segmentLabel": "Fase 1",
  "deliveryLabel": "Entrega inmediata",
  "priceValidUntil": null,
  "contactName": "Karla Méndez",
  "contactPhone": "+52 55 1234 5678",
  "contactEmail": "karla.mendez@example.com",
  "updatedAt": "2026-09-05T18:32:00.000Z",
  "assignedSeller": { "name": "Luis Torres", "email": "luis.torres@example.com" }
}
}

Buscar y actualizar por clave externa

Si el sistema donde llevas tus lotes (un ERP, un CRM, una hoja de cálculo con su propio folio) identifica cada lote con su propia nomenclatura — distinta a la de LotesEnVivo — no necesitas mantener una tabla de equivalencias a mano. Por ejemplo: en LotesEnVivo el lote se llama M-1-L-2, pero en tu CRM ese mismo lote es AC28. Guarda AC28 como la clave externa (external_id) del lote en LotesEnVivo, y luego consulta o actualiza ese lote usando AC28 directamente, agregando ?matchBy=externalId a la URL.

Un lote sin clave externa configurada nunca coincide con una búsqueda por matchBy=externalId, sin importar el valor que mandes — no hay coincidencias accidentales con "vacío" o "sin asignar".

¿Cómo llenar la clave externa de tus lotes? El importador de CSV/XLSX del backoffice (Lotes → Importar) ya soporta externalId como columna — así puedes poblarla de forma masiva para todo tu inventario en un solo archivo, en vez de configurarla lote por lote.

Petición (buscar por clave externa)

terminal
# Si tu sistema identifica este lote como "AC28" (en vez del código "M-1-L-2" de
# LotesEnVivo), busca por clave externa con matchBy=externalId:
curl "https://lotesenvivo.mx/api/v1/lots/AC28?matchBy=externalId" \
  -H "Authorization: Bearer lak_TU_API_KEY"
PATCH/api/v1/lots/{code}

Actualiza estatus, precio y/o metadata de un lote existente. Es el endpoint que usarías al cerrar una venta en tu CRM y reflejarla en LotesEnVivo, o al mantener sincronizada la ficha de un lote (superficie, manzana, etapa, etc.) desde tu propio sistema. El body acepta los mismos campos que puedas necesitar cambiar — manda solo los que quieras tocar, pero al menos uno. Los campos actualizables son el mismo catálogo que soporta el importador de CSV/XLSX del backoffice — ver Relación con el importador de CSV más abajo.

Igual que en GET, acepta el query param matchBy (code por default, o externalId) para decidir cómo se interpreta el valor de la URL — ver Clave externa arriba.

Campo del bodyDescripción
mapIdOpcional. UUID. Igual que en GET, solo se exige si el valor (code o externalId) es ambiguo entre mapas — pero aquí es aún más recomendable mandarlo siempre desde la primera petición si tu integración maneja varios mapas: un PATCH que responde 400 por ambigüedad no modifica nada (no hay riesgo de tocar el lote equivocado), pero evitar el reintento de todos modos simplifica tu flujo.
lotStatusOpcional. Uno de available, reserved, sold, o null ("no disponible", sin estatus). Nota: a diferencia del filtro status=unavailable de GET, aquí ese mismo significado se expresa con null, no con el string "unavailable". No se puede forzar sobre un lote con reserva activa/convertida (ver Reglas de negocio).
pricingModeOpcional. total o per_m2, o null.
priceMxnOpcional. Número ≥ 0, o null.
pricePerM2MxnOpcional. Número ≥ 0, o null.
labelOpcional. String, máx. 500 caracteres. A diferencia del resto de los campos de texto, nunca acepta null — es el nombre del lote y siempre debe tener un valor.
descriptionOpcional. String de texto plano (máx. 5000 caracteres) o null. Nunca mandes HTML: cualquier etiqueta se guarda como texto literal, nunca se interpreta como formato — el saneo es el mismo que aplica el importador de CSV.
areaM2Opcional. Número ≥ 0, o null.
blockCodeOpcional. String, máx. 120 caracteres, o null.
lotNumberOpcional. String, máx. 120 caracteres, o null.
lotCategoryOpcional. String, máx. 120 caracteres, o null.
segmentLabelOpcional. String, máx. 60 caracteres (límite real de la base de datos), o null.
deliveryLabelOpcional. String, máx. 60 caracteres (límite real de la base de datos), o null.
priceValidUntilOpcional. String de fecha en formato ISO yyyy-mm-dd, o null.
frontageMOpcional. Número ≥ 0, o null.
depthMOpcional. Número ≥ 0, o null.
constructionAreaM2Opcional. Número ≥ 0, o null.
houseModelOpcional. String, máx. 120 caracteres, o null.

Un body sin ninguno de estos campos responde 400 validation_error — no existe un "PATCH vacío" que no haga nada silenciosamente. Un campo de texto que exceda su límite de caracteres también responde 400 validation_error con el motivo en message, nunca un error crudo de base de datos.

Petición

terminal
curl -X PATCH "https://lotesenvivo.mx/api/v1/lots/A-14" \
  -H "Authorization: Bearer lak_TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lotStatus": "sold",
    "priceMxn": 850000
  }'
terminal (por clave externa)
# Mismo PATCH, pero identificando el lote con tu propia clave externa (AC28) en vez
# del código de LotesEnVivo:
curl -X PATCH "https://lotesenvivo.mx/api/v1/lots/AC28?matchBy=externalId" \
  -H "Authorization: Bearer lak_TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lotStatus": "sold",
    "priceMxn": 850000
  }'
terminal (metadata del lote)
# También puedes actualizar metadata del lote (no solo estatus/precio) en la misma
# llamada — manda solo los campos que quieras tocar:
curl -X PATCH "https://lotesenvivo.mx/api/v1/lots/A-14" \
  -H "Authorization: Bearer lak_TU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "areaM2": 245,
    "blockCode": "A",
    "lotNumber": "14",
    "segmentLabel": "Fase 1",
    "deliveryLabel": "Entrega inmediata",
    "description": "Esquina, doble frente. Financiamiento directo a 12 meses sin intereses."
  }'

Respuesta 200

200-ok.json
{
  "data": {
  "code": "A-14",
  "externalId": "AC28",
  "mapId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "label": "Lote A-14",
  "featureType": "lot",
  "lotStatus": "sold",
  "pricingMode": "total",
  "priceMxn": 850000,
  "pricePerM2Mxn": null,
  "description": "<p>Esquina, doble frente. Financiamiento directo a 12 meses sin intereses.</p>",
  "areaM2": 240.5,
  "frontageM": 12,
  "depthM": 20,
  "constructionAreaM2": null,
  "houseModel": null,
  "blockCode": "A",
  "lotNumber": "14",
  "lotCategory": "residencial",
  "segmentLabel": "Fase 1",
  "deliveryLabel": "Entrega inmediata",
  "priceValidUntil": null,
  "contactName": "Karla Méndez",
  "contactPhone": "+52 55 1234 5678",
  "contactEmail": "karla.mendez@example.com",
  "updatedAt": "2026-09-06T15:05:01.000Z",
  "assignedSeller": { "name": "Luis Torres", "email": "luis.torres@example.com" }
}
}

Relación con el importador de CSV

El PATCH de esta API y el importador de CSV/XLSX del backoffice (Lotes → Importar) son la misma capacidad expuesta por dos vías distintas: actualizar lotes existentes desde fuera de LotesEnVivo. Ambos comparten el mismo catálogo de campos actualizables (estatus, precio, superficie, manzana, etapa, entrega, etc.) y las mismas reglas duras — nunca crean ni eliminan lotes, nunca fuerzan "disponible" sobre un lote con reserva activa, y respetan mapas cerrados y el modo de solo lectura del plan.

Úsalos según el caso:

  • Importador de CSV/XLSX: carga o actualización masiva y manual — subes un archivo con decenas o cientos de lotes de una vez (ej. la carga inicial de un desarrollo, o una actualización de precios trimestral).
  • API (este endpoint): actualización automatizada y de un lote a la vez, disparada por eventos de tu propio sistema — ej. tu CRM llama a este PATCH en cuanto un vendedor cierra una venta, sin intervención manual.

Una diferencia a tener en cuenta: el importador de CSV recibe texto de una celda de spreadsheet y lo normaliza de forma tolerante (acepta "Disponible", "disponible" o "available" para el mismo estatus, fechas en dd/mm/aaaa, números con separador de miles, etc.). Esta API recibe JSON ya tipado, así que es más estricta a propósito: los enums deben venir en su forma exacta ("available", no "Disponible") y las fechas en formato ISO (yyyy-mm-dd).

Forma de los datos que recibes

Modelo de lote

CampoTipoDescripción
codestringCódigo del lote, único dentro de su mapa (no del workspace). Es el identificador que usas en la URL de los otros dos endpoints.
externalIdstring | nullClave externa del lote (el identificador que usa tu propio sistema — ERP, CRM — en vez del código de LotesEnVivo, ej. "AC28"). null si el lote no tiene una configurada. Se puede usar en la URL en vez de code agregando ?matchBy=externalId — ver la sección "Clave externa" más abajo.
mapIdstring (uuid)Mapa al que pertenece el lote.
labelstringNombre visible del lote, ej. "Lote A-14".
featureType"lot"Siempre "lot" en esta API — amenidades y áreas comunes no se exponen aquí.
lotStatus"available" | "reserved" | "sold" | nullEstatus comercial. null significa "no disponible" (sin estatus asignado).
pricingMode"total" | "per_m2" | nullSi el precio se expresa como monto total o por metro cuadrado.
priceMxnnumber | nullPrecio total en pesos mexicanos, cuando pricingMode es "total".
pricePerM2Mxnnumber | nullPrecio por metro cuadrado en pesos mexicanos, cuando pricingMode es "per_m2".
descriptionstringTexto libre del lote (financiamiento, notas de venta, etc.), mismo campo que acepta el PATCH. Se devuelve tal cual está guardado: si se escribió por esta API es texto envuelto en párrafos/saltos de línea seguros; si el lote se editó en el backoffice puede traer HTML enriquecido. Cadena vacía si el lote no tiene descripción.
areaM2number | nullSuperficie del lote en metros cuadrados.
frontageMnumber | nullFrente del lote en metros.
depthMnumber | nullFondo del lote en metros.
constructionAreaM2number | nullSuperficie de construcción en metros cuadrados.
houseModelstring | nullModelo de casa, ej. "Modelo A".
blockCodestring | nullManzana o bloque, ej. "A".
lotNumberstring | nullNúmero de lote dentro de su manzana.
lotCategorystring | nullCategoría libre definida en el mapa, ej. "residencial", "comercial".
segmentLabelstring | nullEtiqueta de etapa/fase del desarrollo, ej. "Fase 1".
deliveryLabelstring | nullTexto libre de entrega, ej. "Entrega inmediata".
priceValidUntilstring (ISO 8601) | nullFecha hasta la que el precio mostrado es válido, si el mapa la define.
contactNamestring | nullNombre del cliente asociado al lote (dato personal — ver nota de privacidad).
contactPhonestring | nullTeléfono del cliente asociado al lote (dato personal — ver nota de privacidad).
contactEmailstring | nullCorreo del cliente asociado al lote (dato personal — ver nota de privacidad).
updatedAtstring (ISO 8601)Última fecha de modificación del lote.
assignedSeller{ name: string, email: string } | nullVendedor asignado al lote. null si el lote no tiene vendedor asignado. Solo lectura: no se puede modificar con el PATCH de esta API.

Errores

Todos los errores tienen la misma forma: { error: { code, message, details? } }. Programa tu integración contra error.code, no contra el texto de message (está en inglés, pensado para logs).

codeHTTPQué significa
unauthorized401Falta la API key, tiene formato inválido, o fue revocada
plan_inactive403El plan del workspace no está vigente
plan_ineligible403La API pública requiere el Plan Empresarial
validation_error400La petición está mal formada, o el código de lote es ambiguo
not_found404No existe un lote con ese código en este workspace
lot_locked409El lote tiene una reserva activa o convertida y no se puede forzar su estatus
map_lifecycle_closed409El mapa está cerrado a nuevas reservaciones
rate_limited429Superaste el límite de 60 peticiones por minuto de esta API key
internal_error500Error inesperado del lado de LotesEnVivo
unauthorizedHTTP 401

Qué hacer: Revisa que estás mandando el header correcto ('Authorization: Bearer lak_...' o 'X-API-Key: lak_...') y que la key sigue activa en el backoffice. No reintentes con la misma key sin corregir esto — vas a seguir recibiendo 401.

plan_inactiveHTTP 403

Qué hacer: No es un problema de tu integración: el plan del workspace dueño de esta API key está cancelado (ya pagó alguna vez y dejó de hacerlo). El workspace necesita reactivar su plan desde el backoffice de LotesEnVivo. Un trial activo, un plan pagado vigente o un workspace con billing_override nunca disparan este error.

plan_ineligibleHTTP 403

Qué hacer: No es un problema de tu integración ni de la API key en sí (sigue siendo válida): la API pública es exclusiva del Plan Empresarial. El workspace dueño de esta key está en un plan inferior (Básico o Premium) y necesita subir a Plan Empresarial desde el backoffice. En cuanto lo haga, la misma key vuelve a funcionar sin ningún cambio de tu lado — no hay que regenerarla.

validation_errorHTTP 400

Qué hacer: Revisa el body/query contra el esquema documentado (tipos, rangos, campos requeridos) — el campo y motivo exactos vienen en 'message'. Un caso particular: si el 'code' que mandaste existe en más de un mapa de este workspace (ver 'code' no es único por workspace más abajo), también responde 400 (la petición quedó ambigua/incompleta sin 'mapId', no es un conflicto de estado del recurso) — reintenta agregando 'mapId', sugerido en 'details.matchingMapIds'. Si ya sabes que tu integración maneja varios mapas, manda 'mapId' desde la primera petición y evítate el reintento.

validation_error.json
{
  "error": {
    "code": "validation_error",
    "message": "Code 'A-14' matches more than one map in this workspace. Retry with 'mapId' to disambiguate.",
    "details": { "matchingMapIds": ["a1b2c3d4-...", "e5f6a7b8-..."] }
  }
}
not_foundHTTP 404

Qué hacer: Verifica el código y, si lo mandaste, el 'mapId'. La API nunca crea un lote a partir de un código inexistente — un 404 significa exactamente eso, no un fallo transitorio. No tiene sentido reintentar sin cambiar el código.

lot_lockedHTTP 409

Qué hacer: Esto NO es un error técnico ni algo transitorio — es una regla de negocio deliberada. El lote ya tiene una reserva activa o convertida (vendida) en LotesEnVivo, y la API pública nunca puede pisar esa reserva para forzarlo de vuelta a 'available' (o a cualquier otro estatus) por debajo. Un humano debe liberar o convertir la reserva desde el backoffice primero. Si tu integración reintenta este PATCH en bucle esperando que 'se arregle solo', nunca lo hará — el bloqueo se quita liberando la reserva, no reintentando la petición.

lot_locked.json
{
  "error": {
    "code": "lot_locked",
    "message": "This lot has an active or converted reservation. Its status can't be changed from the public API — release or convert the reservation first.",
    "details": { "reason": "active_reservation" }
  }
}
map_lifecycle_closedHTTP 409

Qué hacer: Un mapa cerrado sigue aceptando cobranza y liberar lotes, pero rechaza que un lote 'available' o 'sin estatus' salte directo a 'reserved'/'sold'. La única transición que sí se permite en un mapa cerrado es 'reserved' → 'sold' (cerrar una venta que ya estaba apartada). Si necesitas mover el lote de otra forma, contacta a quien administra ese mapa — no es algo que la API deba (ni pueda) forzar.

rate_limitedHTTP 429

Qué hacer: El límite es de 60 requests/minuto por API key (ventana fija). Espera y reintenta con backoff (por ejemplo, exponencial empezando en 1-2 segundos). 'details.retryAfterMs' te dice cuántos milisegundos faltan para la siguiente ventana. Si tu integración necesita más throughput de forma sostenida, agrupa peticiones (usa 'GET /api/v1/lots' con paginación en vez de un GET por lote) antes de pedir un límite mayor.

internal_errorHTTP 500

Qué hacer: No es algo que puedas corregir en tu petición. Reintenta con backoff; si persiste, contacta a soporte de LotesEnVivo con el 'code', el endpoint y un timestamp aproximado.

Lo que la API nunca hace

Reglas de negocio

  • Nunca crea ni elimina lotes — solo lee y actualiza lotes existentes.
  • Nunca puede forzar un lote a "disponible" (ni a ningún otro estatus) si tiene una reserva activa o convertida — primero hay que liberar o convertir la reserva desde el backoffice.
  • Respeta el mismo ciclo de vida de mapas que el backoffice: si un mapa está cerrado a nuevas reservaciones, la API no puede saltarse esa regla (salvo la transición reservado → vendido).
  • Requiere Plan Empresarial — un workspace en Básico o Premium puede crear la key desde el backoffice, pero cada petición responde 403 plan_ineligible hasta que suba de plan.
  • Si el plan de tu workspace no está activo, la API queda en modo solo lectura como el resto del producto.
  • Un campo de texto que exceda su límite de caracteres (ej. segmentLabel o deliveryLabel, máximo 60 caracteres cada uno) responde 400 validation_error con el motivo — nunca deja pasar el dato ni revienta con un error crudo de base de datos.
  • El campo internalNotes del importador de CSV (notas internas del equipo, nunca visibles en el mapa público) no se expone por esta API — no hay caso de uso de integración automatizada escribiendo notas internas de tu equipo de ventas.

Evita procesar tu propio eco

Relación con los webhooks

Cuando un lote pasa a sold por esta API, también se emite el webhook saliente lot.sold (si tienes uno configurado), con data.source: "api". Si el mismo sistema que hizo el PATCH también escucha ese webhook, revisa ese campo para reconocer y descartar el eco de su propia acción — de lo contrario podrías terminar reprocesando la misma venta en bucle.

Consultas por minuto

Límite de peticiones

Cada API key tiene un límite de 60 requests por minuto, medido en una ventana fija (no un token bucket). Si lo superas, recibes 429 rate_limited con details.retryAfterMs indicando cuántos milisegundos esperar antes de la siguiente ventana. Al recibir este error, espera y reintenta con backoff (por ejemplo, exponencial empezando en 1-2 segundos) en vez de reintentar de inmediato. Si tu integración necesita más throughput de forma sostenida, agrupa peticiones (usa GET /api/v1/lots con paginación en vez de un GET por lote) en lugar de pedir un límite mayor.

De la teoría a una integración real

Caso de uso completo

El flujo típico de Kanlúm (Go High Level) y desarrolladoras similares: la venta se cierra en el CRM, y ese mismo cierre debe reflejarse en el mapa público de LotesEnVivo sin que nadie tenga que entrar al backoffice a mano.

  1. Un vendedor marca la oportunidad como ganada en el CRM. Un Workflow de Go High Level (o el código de tu integración) dispara la actualización hacia LotesEnVivo.

  2. (Opcional pero recomendado) Confirma el lote antes de escribir. Si tu Workflow solo conoce el código del lote (no el mapId), un GET previo evita sorpresas si el código resultara ambiguo entre mapas.

    terminal
    # 1. Confirma que estás actualizando el lote correcto antes de escribir nada.
    curl "https://lotesenvivo.mx/api/v1/lots/A-14" \
      -H "Authorization: Bearer lak_TU_API_KEY"
  3. Marca el lote como vendido con un PATCH. Este es el paso que cierra el círculo.

    terminal
    # 2. Ciérralo como vendido y registra el precio final pactado con el cliente.
    curl -X PATCH "https://lotesenvivo.mx/api/v1/lots/A-14" \
      -H "Authorization: Bearer lak_TU_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "lotStatus": "sold",
        "priceMxn": 850000
      }'

    Respuesta 200

    200-ok.json
    {
      "data": {
        "code": "A-14",
        "mapId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "label": "Lote A-14",
        "featureType": "lot",
        "lotStatus": "sold",
        "pricingMode": "total",
        "priceMxn": 850000,
        "pricePerM2Mxn": null,
        "areaM2": 240.5,
        "frontageM": 12,
        "depthM": 20,
        "blockCode": "A",
        "lotNumber": "14",
        "lotCategory": "residencial",
        "segmentLabel": "Fase 1",
        "deliveryLabel": "Entrega inmediata",
        "priceValidUntil": null,
        "contactName": "Karla Méndez",
        "contactPhone": "+52 55 1234 5678",
        "contactEmail": "karla.mendez@example.com",
        "updatedAt": "2026-09-06T15:05:01.000Z",
        "assignedSeller": { "name": "Luis Torres", "email": "luis.torres@example.com" }
      }
    }

Este mismo PATCH dispara internamente un webhook saliente lot.sold hacia cualquier conexión de webhooks que tengas configurada, con data.source: "api". Si ese mismo Workflow de Go High Level también escucha ese webhook, va a recibir el eco de su propia acción — ver la sección "Relación con los webhooks" antes de conectar ambas direcciones, o vas a terminar re-procesando la misma venta en bucle.

Privacidad de los datos

Los campos contactName, contactPhone y contactEmail son datos personales de tus clientes finales. Trátalos como información sensible en tu CRM, ERP o sistema externo: guárdalos solo donde ya manejas ese tipo de datos, igual que con los payloads de los webhooks salientes.

Para el equipo de Kanlúm y clientes similares

Nota sobre Go High Level

Go High Level puede llamar esta API directamente desde un Workflow (paso HTTP Request), agregando el header Authorization con tu key. No necesitas Zapier ni Make de por medio — igual que con los webhooks salientes.

¿Ya tienes tu API key?

Entra al backoffice para crear tu clave cuando tengas un sistema externo listo para conectarse.

Ir al backoffice