Ciudades: catálogo DANE

La ciudad de contactos, empresas y de la organización se guarda con el código oficial DANE/DIVIPOLA del municipio. Así "BOGOTA", "Bogotá D.C." y "bogota" son el mismo lugar, los filtros por ciudad o departamento funcionan y el dato sirve tal cual para facturación electrónica y envíos.

Cómo se guarda una ciudad

El catálogo tiene los 1.122 municipios y los 33 departamentos de Colombia (Bogotá D.C. cuenta como departamento, código 11). Cuando envías un cityCode válido, FusionCRM escribe por ti el nombre de la ciudad, el departamento y el código del departamento.

  • • Por API, formularios públicos y MCP la ciudad es estricta: o se resuelve sin dudas contra el catálogo, o la petición falla con los candidatos para que elijas.
  • • En la app y en el importador de Excel es flexible: un texto que no se reconoce se guarda igual como ciudad sin confirmar (city con valor y cityCode vacío) y aparece en la lista dinámica "Ciudades sin confirmar".
  • • Solo Colombia: si el contacto tiene un country distinto de Colombia, city se guarda como texto libre sin código ni validación.
  • • FusionCRM nunca elige un "mejor candidato" por ti: 66 municipios comparten nombre (La Unión, Rionegro, Armenia, Villanueva…).

Los cuatro campos

CampoQué esQuién lo escribe
cityCodeCódigo DANE del municipio, 5 dígitos (05001 = Medellín, 76001 = Cali, 11001 = Bogotá).Es la fuente de verdad. Lo envías tú.
cityNombre corto del municipio ("Medellín").Se deriva de cityCode. Si solo mandas city, el servidor busca el código.
stateDepartamento en texto ("Antioquia").Se deriva de cityCode. Enviado solo sirve de pista para desambiguar city.
stateCodeCódigo DANE del departamento, 2 dígitos (05 = Antioquia).Siempre lo calcula el servidor: el valor que envíes se ignora.

Los cuatro llegan en la respuesta de contactos y empresas. Para buscar o filtrar usa siempre los códigos: el texto puede venir con otra grafía en datos antiguos.

Catálogo: municipios y departamentos

Dos endpoints de solo lectura que acepta cualquier token válido, sin permiso especial:

  • • GET /catalogs/municipalities — sin parámetros devuelve el catálogo completo. Con q busca por nombre (sin tildes ni mayúsculas, capitales primero, 20 resultados por defecto); state acota a un departamento ("05") y limit va de 1 a 1200.
  • • GET /catalogs/states — los 33 departamentos con código, nombre oficial, nombre corto e ISO 3166-2.

Buscar el código de una ciudad

curl "https://api.fusioncol.com/api/catalogs/municipalities?q=rionegro" \
  -H "Authorization: Bearer <token>"

Respuesta: dos municipios con el mismo nombre; mira stateName antes de elegir

{
  "success": true,
  "version": "divipola-2024-12-30.b17287ee",
  "count": 2,
  "data": [
    { "code": "05615", "name": "Rionegro", "shortName": "Rionegro",
      "stateCode": "05", "stateName": "Antioquia", "stateIso2": "ANT", "dane8": "05615000" },
    { "code": "68615", "name": "Rionegro", "shortName": "Rionegro",
      "stateCode": "68", "stateName": "Santander", "stateIso2": "SAN", "dane8": "68615000" }
  ]
}

code es el valor que va en cityCode. shortName es lo que el CRM muestra y guarda en city. dane8 es el código de la cabecera municipal (code+ "000"), el que piden algunas transportadoras.

Todos los municipios de un departamento

curl "https://api.fusioncol.com/api/catalogs/municipalities?state=05" \
  -H "Authorization: Bearer <token>"

Caché y ETag

El catálogo cambia muy poco (se regenera cuando el DANE publica un corte nuevo). Las respuestas traen Cache-Control: public, max-age=86400 y un ETag con la versión del catálogo. Descárgalo una vez, búscalo en local y revalida con If-None-Match: mientras no cambie recibes 304 sin cuerpo.

Revalidar la copia local

curl -i "https://api.fusioncol.com/api/catalogs/municipalities" \
  -H "Authorization: Bearer <token>" \
  -H 'If-None-Match: "<etag de la respuesta anterior>"'
# HTTP/1.1 304 Not Modified

Crear y actualizar con ciudad

En POST y PUT de /contacts y /companies tienes tres formas de indicar la ciudad, de mejor a peor:

  1. 1. cityCode: gana sobre cualquier texto y deriva todo lo demás.
  2. 2. city + state: el departamento desambigua homónimos (Rionegro + Santander → 68615).
  3. 3. Solo city: funciona si el nombre es único y está bien escrito; si no, 422.

Crear un contacto con el código DANE

curl -X POST "https://api.fusioncol.com/api/contacts" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Ana",
    "email": "ana@example.com",
    "cityCode": "76001"
  }'
# Guarda city "Cali", state "Valle del Cauca" y stateCode "76"

Si el body no trae city, cityCode ni state, la ubicación guardada no se toca. En PUT /contacts/{id}, un city vacío ("") limpia la ciudad, el código y el departamento derivado.

Errores de ciudad

HTTPcodeCuándo
422CITY_UNRESOLVEDcity es ambigua (Rionegro existe en Antioquia y en Santander), está mal escrita (Medelin) o no existe. No se guarda nada; candidates trae los municipios posibles.
400INVALID_CITY_CODEEl cityCode no existe en el catálogo. candidates llega vacío.
400STATE_UNKNOWN / LIMIT_INVALIDSolo en /catalogs/municipalities: state no es un departamento o limit está fuera de 1-1200.

422 CITY_UNRESOLVED: reenvía con uno de los candidates como cityCode

{
  "success": false,
  "error": "CITY_UNRESOLVED",
  "code": "CITY_UNRESOLVED",
  "message": "La ciudad \"Rionegro\" corresponde a varios municipios; indique cityCode o el departamento",
  "input": "Rionegro",
  "status": "ambiguous",
  "candidates": [
    { "code": "05615", "shortName": "Rionegro", "stateName": "Antioquia", "stateCode": "05" },
    { "code": "68615", "shortName": "Rionegro", "stateName": "Santander", "stateCode": "68" }
  ]
}

status dice por qué falló: ambiguous (varios municipios), fuzzy (parecido pero no exacto, como "Medelin"), none (nada parecido; candidates vacío) o invalid_code (el 400 de un cityCode inexistente).

Nota
El módulo de Envíos usa su propio error de ciudad, 422 INVALID_CITY, con otra forma de cuerpo (reason, input como objeto y candidatos con dane8). Está explicado en su página.

Formularios, importación y filtros

  • • Formularios web (POST /forms/{id}/submit y el formulario público): la ciudad es estricta. Una ciudad no resuelta responde 400 (no 422) con code: CITY_UNRESOLVED, un mensaje para mostrarle al visitante y los candidates; el contacto no se crea. Un campo departamento, depto, dpto o estado se toma como state y ayuda con los homónimos.
  • • Importación por API (POST /import/contacts): locationMode: "lenient" (por defecto, igual que el Excel de la app) guarda las ciudades no reconocidas como sin confirmar y las resume en warnings; "strict" rechaza esas filas en rejectedRows con los candidatos en reason. Prueba primero con dryRun: true.
  • • Filtrar contactos o empresas: por municipio ?cityCode=05001&matchMode=exact; por departamento entero ?stateCode=05&matchMode=exact. Repetir la clave hace un OR (?cityCode=05001&cityCode=76001).
  • • MCP: los tools de contactos y empresas aceptan cityCode y devuelven los mismos candidatos; ver MCP.
Consejo
Para limpiar una base vieja: filtra los contactos con city con valor y cityCodevacío (la lista "Ciudades sin confirmar" ya lo hace en la app), resuelve cada ciudad con /catalogs/municipalities?q= y actualiza con PUT /contacts/{id} mandando solo cityCode.