Guía para desarrolladores

Conectar con NeoJaus

Agrega el botón «Conectar con NeoJaus» a tu portal o CRM. Con el permiso de la inmobiliaria, tu plataforma lee y publica sus propiedades en NeoJaus mediante OAuth 2.0 con PKCE.

Identidad

Quién es el usuario y a qué inmobiliaria pertenece.

Leer propiedades

Las del usuario o las de toda su inmobiliaria.

Publicar propiedades

Crear, actualizar, desactivar y gestionar fotos.

APIhttps://api.neojaus.com
Autorizaciónhttps://neojaus.com/oauth/authorize
SDKhttps://neojaus.com/sdk/neojaus-auth.js
01

Dos tipos de aplicación

Registramos tu aplicación como uno de dos tipos. Los dos usan PKCE (S256); la diferencia es quién habla con /oauth/token.

Quién llama a /oauth/token
Confidencial (tienes un servidor)
Tu servidor
Público (solo navegador)
Tu página, con el SDK
Autenticación del cliente
Confidencial (tienes un servidor)
client_secret (en el cuerpo o HTTP Basic) y PKCE
Público (solo navegador)
Solo PKCE. Si mandas un client_secret la petición se rechaza
Vida del refresh token
Confidencial (tienes un servidor)
30 días
Público (solo navegador)
24 horas
El SDK te devuelve
Confidencial (tienes un servidor)
{code, codeVerifier} para tu servidor
Público (solo navegador)
authorize({exchange: true}) termina el intercambio y devuelve los tokens
CORS en las rutas del API
Confidencial (tienes un servidor)
No: llama desde tu servidor
Público (solo navegador)
Solo los orígenes que registramos (ver Clientes públicos)
¿No sabes cuál te toca?
Elige confidencial: es el camino recomendado y el token nunca pasa por el navegador. Un cliente público puede pasar a confidencial después asignándole un secreto; sus conexiones vivas siguen funcionando.
02

Registro: qué necesitamos de ti

No hay portal de autoservicio: nosotros registramos tu aplicación. Escríbenos con:

  • Nombre, URL de tu sitio y logo. Se muestran en la pantalla de autorización y en «Aplicaciones conectadas» de cada usuario.
  • Redirect URIs: las direcciones exactas a las que vuelve la ventana. La comparación es igualdad de texto (nada de comodines ni prefijos), deben ser https:// y no pueden llevar fragmento (#…).
  • Scopes que vas a pedir (sección 4). Una aplicación no puede pedir un scope para el que no fue registrada.
  • Tipo de aplicación: confidencial o pública.
  • Solo para públicas: los orígenes exactos de tu página (https://app.ejemplo.com, sin ruta). Son los únicos para los que el API contesta CORS.

Te devolvemos tu client_id (nja_ más 20 caracteres hexadecimales, público) y, si eres confidencial, tu client_secret.

El client_secret se muestra una sola vez
Solo guardamos una huella; si lo pierdes hay que registrar una aplicación nueva. Guárdalo en tu servidor, nunca en código de navegador ni en un repositorio.
03

El flujo: botón, ventana, código, tokens

Secuencia
Tu página               NeoJaus (ventana)             Tu servidor          api.neojaus.com
   |  clic en el botón       |                             |                      |
   |----- abre ventana ----->|  inicia sesión y aprueba    |                      |
   |<---- {code, state} -----|  (postMessage a tu origen)  |                      |
   |----- code + codeVerifier --------------------------->|                      |
   |                                                       |-- POST /oauth/token ->|
   |                                                       |<-- access + refresh --|

Con el SDK (recomendado)

HTML
<script src="https://neojaus.com/sdk/neojaus-auth.js"></script>
<div id="conectar"></div>
<script>
  NeoJausAuth.init({
    clientId: "nja_…",
    redirectUri: "https://tu-sitio.com/neojaus/callback",   // uno de los registrados, exacto
    scope: "profile agency agency.properties:write"
  });
  NeoJausAuth.renderButton("#conectar", {
    onSuccess: ({ code, codeVerifier }) => {
      // envía los DOS a TU servidor, por HTTPS
      fetch("/mi-backend/neojaus/conectar", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ code, codeVerifier })
      });
    }
  });
</script>

API del SDK: init, renderButton, authorize, authorize({exchange: true}), fetch, refresh, revoke, logout, getTokens y setTokens.

  • authorize() debe llamarse desde un clic; si no, el navegador bloquea la ventana.
  • Rechaza con access_denied si la persona pulsa «Cancelar», popup_blocked, popup_closed, timeout, invalid_config o unsupported, o con el código de error del endpoint de tokens.
  • El SDK exige crypto.subtle, es decir HTTPS o localhost.

Sin el SDK (servidor, por redirección)

Genera tú un state aleatorio, un code_verifier (43 a 128 caracteres de [A-Za-z0-9-._~]) y code_challenge = BASE64URL(SHA256(code_verifier)) sin relleno. Lleva a la persona a:

URL
https://neojaus.com/oauth/authorize
  ?client_id=nja_…
  &redirect_uri=https%3A%2F%2Ftu-sitio.com%2Fneojaus%2Fcallback
  &scope=profile%20agency%20agency.properties%3Awrite
  &state=<aleatorio>
  &code_challenge=<S256 del verifier>
  &code_challenge_method=S256

Sin response_mode (o con response_mode=query), NeoJaus vuelve a tu redirect_uri con ?code=…&state=…, o con ?error=access_denied&state=… si la persona cancela. Compara el state con el que guardaste antes de hacer nada con el código. Con response_mode=web_message la ventana avisa a la página que la abrió (postMessage con {type: "neojaus:oauth", code, state}), que es lo que hace el SDK: solo entrega el mensaje al origen de tu redirect_uri.

Intercambiar el código

Tu servidor, dentro de los 10 minutos que vive el código (se usa una sola vez):

bash
curl -X POST https://api.neojaus.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "nja_…:TU_CLIENT_SECRET" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=nja_…" \
  --data-urlencode "code=…" \
  --data-urlencode "redirect_uri=https://tu-sitio.com/neojaus/callback" \
  --data-urlencode "code_verifier=…"

Respuesta 200:

JSON
{
  "access_token": "njo_<16 hex>_<64 hex>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "njo_<16 hex>_<64 hex>",
  "scope": "profile agency agency.properties:write"
}
scope es lo que la persona realmente autorizó, que puede ser menos de lo que pediste: léelo y no asumas. Guarda ambos tokens como secretos. Un código usado dos veces revoca la conexión entera.
04

Scopes y quién puede autorizar

profile
Qué permite
/oauth/userinfo: sub, name, email
Quién puede autorizarlo
Cualquier agente con plan premium
agency
Qué permite
/oauth/userinfo: datos de la inmobiliaria y si la persona es administradora
Quién puede autorizarlo
Cualquier agente con plan premium
properties:read
Qué permite
Listar y leer las propiedades del propio usuario
Quién puede autorizarlo
Cualquier agente con plan premium
agency.properties:read
Qué permite
Listar y leer las propiedades de todos los agentes aprobados de la inmobiliaria (incluye properties:read)
Quién puede autorizarlo
Solo administradores de la inmobiliaria
agency.properties:write
Qué permite
Crear, actualizar y desactivar propiedades de la inmobiliaria, añadir y quitar fotos, y crear a nombre de otro agente con assigned_to (incluye agency.properties:read)
Quién puede autorizarlo
Solo administradores de la inmobiliaria
  • Solo un administrador puede dar acceso de escritura. Un agente normal solo puede dar lectura de sus propias propiedades. Si tu integración sube propiedades, quien pulsa el botón debe ser el administrador de la inmobiliaria.
  • El token solo funciona con un plan premium activo en esa inmobiliaria. Sin plan, la pantalla de autorización manda a la persona a los planes y, si el plan vence después, el token responde 401 (sección 7).
  • Una conexión es una persona en una inmobiliaria. Si la persona pertenece a dos inmobiliarias, elige una en la pantalla; el token solo ve esa. Cada inmobiliaria se conecta por su cuenta: no hay un token «de asociación» que cubra a todas.
  • Si pides más scopes de los que la persona puede dar, la pantalla omite los demás y el scope de la respuesta trae solo los concedidos.
  • El scope decide el alcance en todas las rutas: con solo properties:read, un administrador ve únicamente sus propias propiedades, y la propiedad de un colega responde 401.
La persona puede revocar la conexión cuando quiera desde Aplicaciones conectadas en su panel de NeoJaus; un administrador ve (y puede revocar) las conexiones que cualquier persona de su inmobiliaria hizo.
05

Endpoints de OAuth

Todos en https://api.neojaus.com. Las respuestas de /oauth/token y /oauth/revoke llevan Cache-Control: no-store.

POST /oauth/token

Cuerpo solo application/x-www-form-urlencoded: un cuerpo JSON es invalid_request. client_id va en el cuerpo (o en el usuario del HTTP Basic). Si además mandas ?client_id= en la URL, debe coincidir con el del cuerpo o es invalid_request; un client_id solo en la URL no identifica al cliente.

  • Confidencial: client_secret en el cuerpo (client_secret_post) o HTTP Basic (client_secret_basic), no los dos. Más PKCE.
  • Público: solo client_id y PKCE. Mandar un client_secret es un fallo de autenticación.
  • grant_type=authorization_code: campos code, redirect_uri (idéntico al de la autorización) y code_verifier.
  • grant_type=refresh_token: campo refresh_token.

La respuesta 200 es la de la sección 3. Cada refresh devuelve un par nuevo (access_token y refresh_token); el refresh token anterior deja de servir (sección 6).

GET /oauth/userinfo

Authorization: Bearer <access_token>, scope profile.

JSON
{
  "sub": "3f2a…(uid del usuario, 32 hex)",
  "name": "Lorem ipsum",
  "email": "[email protected]",
  "agency": {
    "uid": "9b1c…",
    "name": "Inmobiliaria Lorem",
    "logo": "https://…",
    "is_admin": true
  }
}

agency solo viene con el scope agency, y es siempre la inmobiliaria de la conexión, nunca otra a la que pertenezca la persona.

POST /oauth/revoke (RFC 7009)

Misma autenticación de cliente y mismo formato de cuerpo que /oauth/token, con el campo token (el access o el refresh token). Responde 200 con cuerpo vacío también si el token no existe, ya estaba revocado o no es tuyo. Un token válido revoca la conexión entera de inmediato: el access token, el refresh token y la conexión. Si falla la autenticación del cliente: 401 invalid_client.

06

Vida de los tokens, rotación y revocación

Código de autorización
Duración
10 minutos, un solo uso
Access token
Duración
1 hora (expires_in: 3600)
Refresh token, confidencial
Duración
30 días
Refresh token, público
Duración
24 horas

La duración del refresh token se reinicia en cada rotación: un refresh token que no se usa en 30 días (24 horas si eres público) caduca, y la inmobiliaria tiene que volver a pulsar el botón. Si tu integración es una tarea periódica, refresca al menos una vez dentro de esa ventana.

Rotación: cómo refrescar sin perder la conexión

  • Refresca antes de que venza el access token, no después de recibir un 401. Con un margen de un par de minutos basta.
  • Cada refresh devuelve un par nuevo: guarda los dos tokens nuevos de forma atómica (los dos o ninguno) antes de usarlos.
  • Tus peticiones en vuelo no se cortan: el access token anterior sigue aceptándose 60 segundos después del refresh, así que una carga masiva en curso no falla por rotar.
  • Si la respuesta de un refresh se pierde (timeout, corte de red), repite el refresh con el mismo refresh token. Si lo haces dentro de los 60 segundos posteriores y nadie ha usado todavía el par nuevo, recibes otro par nuevo que funciona, y el par que no llegaste a ver queda anulado. No puedes recibir «el mismo par» otra vez: por diseño no guardamos tokens en claro.
  • Un refresh token que quedó anulado sin que tú lo rotaras es simplemente inválido: el refresh responde invalid_grant y la conexión no se corta. Pasa cuando la persona volvió a autorizar la aplicación, o cuando el reintento de los 60 segundos reemplazó un par que nunca llegaste a ver. Vuelve a empezar con el par más reciente que tengas (o, si no hay, pide a la persona que reconecte).
  • Los refresh tienen un límite de 10 por minuto por conexión.
Reusar un refresh token ya rotado revoca la conexión
Reusar un refresh token que tú ya rotaste fuera de la ventana de 60 segundos, o cuando el par que le sucedió ya se usó o ya fue reemplazado (la ventana sirve para un reintento, no para dos), se trata como un posible robo: se revoca la conexión y la inmobiliaria tiene que volver a conectarla. Haz que un solo proceso refresque a la vez por conexión (un candado o una cola), sin reintentos en paralelo.

Revocación y qué significa un 401

Una conexión se acaba cuando la persona (o un administrador de su inmobiliaria) la revoca en «Aplicaciones conectadas», tú llamas a /oauth/revoke, la persona sale de la inmobiliaria, hay un reuso de código o de refresh token, o NeoJaus suspende tu aplicación.

No hay notificación: te enteras porque tus llamadas empiezan a devolver 401 y el refresh a devolver invalid_grant. Trátalo como «la inmobiliaria se desconectó»: borra sus tokens y muestra el botón de conectar de nuevo.
07

Errores

/oauth/token y /oauth/revoke

Cuerpo {"error": "<código>", "error_description": "<texto en inglés>"} (RFC 6749 §5.2):

400
error
invalid_request
Cuándo
Falta un campo (grant_type is required., code and redirect_uri are required., code_verifier is required., refresh_token is required., token is required.); cuerpo que no es form (The request body must be application/x-www-form-urlencoded.); dos formas de autenticar el cliente (Use one client authentication method, not two.); client_id de la URL, el cuerpo o la cabecera que no coinciden
401
error
invalid_client
Cuándo
Falló la autenticación del cliente. Siempre el mismo cuerpo (Client authentication failed.): cliente desconocido, aplicación suspendida, secreto ausente, equivocado o no esperado
400
error
invalid_grant
Cuándo
The authorization code is invalid, expired or already used.; The code_verifier is malformed.; PKCE que no corresponde; redirect_uri distinto; The refresh token is invalid, expired or revoked.
400
error
unsupported_grant_type
Cuándo
grant_type must be authorization_code or refresh_token.
429
error
rate_limit_exceeded
Cuándo
Too many requests. Retry after the number of seconds in Retry-After. Con cabecera Retry-After
500
error
server_error
Cuándo
Something went wrong on our side. Request ID: …

Con HTTP Basic, el 401 trae WWW-Authenticate: Basic realm="neojaus". Un código caducado o de otra aplicación es invalid_grant y no toca la conexión; un código ya usado por tu aplicación sí la revoca.

Rutas con token (/oauth/userinfo y /v1/properties*)

Cuerpos con la forma {"error": "<texto>"}:

401
Cuerpo
{"error": "Invalid access token."} con WWW-Authenticate: Bearer error="invalid_token"
Significa
Token mal formado, desconocido, vencido o superado hace más de 60 s, conexión revocada, aplicación suspendida, la persona ya no es agente aprobada de la inmobiliaria, o el scope es de administrador y la persona ya no lo es. Es un solo cuerpo a propósito. Intenta refrescar; si el refresh responde invalid_grant, la conexión terminó
401
Cuerpo
{"error": "Get a premium plan at https://neojaus.com/plans to continue."}
Significa
La inmobiliaria ya no tiene un plan activo. La conexión sigue existiendo y vuelve a funcionar cuando el plan se reactive. Un plan vencido puede tardar hasta una hora en notarse
403
Cuerpo
{"error": "This access token does not carry the scope <scope>."} con WWW-Authenticate: Bearer error="insufficient_scope", scope="<scope>"
Significa
La ruta necesita un scope que esta conexión no concedió

Cuando la persona pierde su lugar en la inmobiliaria, la conexión se revoca; cuando solo deja de ser administradora, la conexión queda «dormida» (401) y vuelve si la vuelven a nombrar administradora.

08

API de propiedades (v1)

Base: https://api.neojaus.com/v1. Con OAuth, cada petición lleva Authorization: Bearer <access_token>. Son las mismas rutas que usan las integraciones que hoy se autentican con x-api-key (documentación de la llave); si una petición trae las dos cabeceras, gana la llave.

GET /properties
Scope
properties:read
Qué hace
Lista paginada
GET /properties/<public_id>
Scope
properties:read
Qué hace
Una propiedad completa
POST /properties
Scope
agency.properties:write
Qué hace
Crea (o actualiza con external_id)
PUT /properties/<public_id>
Scope
agency.properties:write
Qué hace
Actualiza campos (JSON)
PATCH /properties/<public_id>/is_active
Scope
agency.properties:write
Qué hace
Publica o despublica
POST /properties/<public_id>/images
Scope
agency.properties:write
Qué hace
Añade fotos
DELETE /properties/<public_id>/images/<name>
Scope
agency.properties:write
Qué hace
Quita una foto
El id de las rutas es el public_id de la respuesta (NJ- más hexadecimal, p. ej. NJ-1A2B3), no el uid. Uno en otro formato es 400 Invalid property ID format.

Las rutas de escritura exigen que quien conectó sea administrador y que la inmobiliaria tenga plan activo (400 You need an active plan for the account for this endpoint. o 403 You need an admin account for this endpoint.).

GET /properties

Parámetros de consulta, todos opcionales:

page
Valores
Desde 1 (por defecto 1)
limit
Valores
1 a 50 (por defecto 20)
property_types
Valores
Lista separada por comas de los tipos de la tabla de enumeraciones
statuses
Valores
Lista separada por comas: published, not_published, reserved, sold, renter, suspended
operation_type
Valores
sale o rental
min_price, max_price
Valores
Enteros; exigen operation_type
min_bedrooms, min_bathrooms, min_parking_spaces
Valores
Enteros ≥ 0
min_construction_size, max_construction_size, min_lot_size, max_lot_size
Valores
Enteros ≥ 0, en m²
updated_after, updated_before
Valores
ISO 8601, p. ej. 2026-09-01T00:00:00Z
sort_by
Valores
updated_at-desc (por defecto) o updated_at-asc

Con properties:read ves las propiedades del usuario que conectó; con agency.properties:read o agency.properties:write, las de todos los agentes aprobados de la inmobiliaria.

JSON
{
  "pagination": {"limit": 20, "page": 1, "total": 57, "total_pages": 3,
                 "next_page": "https://api.neojaus.com/v1/properties?page=2&limit=20"},
  "content": [{
    "public_id": "NJ-1A2B3", "title": "Casa En Zihuatanejo",
    "agent": "Lorem ipsum", "agent_info": {"phone": "521234567", "email": "[email protected]"},
    "title_image_full": "https://cdn.neojaus.com/properties/<uid>/<foto>.webp",
    "title_image_thumb": "https://cdn.neojaus.com/properties/<uid>/<foto>_thumb.webp",
    "bedrooms": 3, "bathrooms": 2, "parking_spaces": 2,
    "location": "Centro, Zihuatanejo", "property_type": "Casa",
    "updated_at": "2026-09-20T10:00:00+00:00", "show_prices": true, "share_commission": false,
    "operations": [{"type": "sale", "amount": 2500000.0, "currency": "MXN", "unit": "valor_total",
                    "formated_amount": "MXN$ 2,500,000.00",
                    "commission": {"type": null, "value": null, "currency": null}}]
  }]
}

Errores de la lista (todos 400 con {"error": …}): Invalid property type: <x> (con valid_types), Invalid status: <x> (con valid_statuses), Invalid operation_type: <x> (con valid_types), OperationType: 'temporary_rental' is not currently available, operation_type is required when using min_price or max_price, min_price must be >= 0, min_price cannot be greater than max_price (y los equivalentes de los demás rangos), Invalid updated_after format. Use ISO 8601 (e.g., 2020-03-01T23:20:00), updated_after cannot be greater than updated_before, Invalid sort_by: <x> (con valid_sorts).

GET /properties/<public_id>

Devuelve la propiedad completa (la misma forma que content de las respuestas de escritura, abajo). 404 Invalid property ID.; 401 Not authorized to view that property. si la propiedad no es tuya y tu conexión no tiene nivel de inmobiliaria, o es de alguien que no es agente aprobado de tu inmobiliaria; 400 You need an active plan for the account for this endpoint.

POST /properties: crear

multipart/form-data (cualquier otro Content-Type es 400 Content-Type needs to be multipart/form-data). Los campos usan notación de puntos (location.coords.lat) y las fotos van como partes de archivo. Es una sola petición: primero se validan los campos y las fotos, y si algo falla no se crea nada.

bash
curl -X POST https://api.neojaus.com/v1/properties \
  -H "Authorization: Bearer njo_…" \
  -F "external_id=mi-portal-12345" \
  -F "property_type=Casa" \
  -F "operation_types=sale" \
  -F "name=Casa en Zihuatanejo" \
  -F "description=Casa de dos plantas a cinco minutos de la playa." \
  -F "pricing.sales.price=2500000" \
  -F "location.mexican_state=guerrero" \
  -F "location.coords.lat=17.6" \
  -F "location.coords.lng=-101.5" \
  -F "location.street=Av. Principal" \
  -F "location.neighborhood=Centro" \
  -F "location.municipality=Zihuatanejo" \
  -F "location.zip=40880" \
  -F "amenities=pet friendly,pool" \
  -F "[email protected]" \
  -F "[email protected]"
property_type
Requerido
sí
Formato
Uno de los tipos (enumeraciones, abajo)
operation_types
Requerido
sí
Formato
sale, rental o ambos separados por coma: sale,rental. temporary_rental aún no se admite
name
Requerido
sí
Formato
5 a 100 caracteres. No puede contener datos personales ni enlaces externos
description
Requerido
sí
Formato
10 a 4096 caracteres
pricing.sales.price
Requerido
si hay sale
Formato
Número > 0
pricing.sales.currency_type
Requerido
no
Formato
mxn (por defecto), usd, eur, crc
pricing.sales.based_on
Requerido
no
Formato
valor_total (por defecto), m2, ha
pricing.sales.commission.value
Requerido
no
Formato
Número; si lo mandas, manda también .commission_type (fixed_amount, months, percentaje) y opcionalmente .currency_type
pricing.rents.price, .currency_type, .based_on, .commission.*
Requerido
si hay rental
Formato
Igual que pricing.sales.*
location.mexican_state
Requerido
sí
Formato
Un estado (enumeraciones, abajo), en minúsculas y con guion bajo
location.coords.lat, location.coords.lng
Requerido
sí
Formato
Números; lat de −90 a 90, lng de −180 a 180
location.street, location.neighborhood, location.municipality
Requerido
sí
Formato
1 a 255 caracteres
location.zip
Requerido
sí
Formato
1 a 10 caracteres
location.number_ext, location.number_int
Requerido
no
Formato
Hasta 10 caracteres
rooms, bathrooms, half_bathrooms, parking_spots, floors
Requerido
no
Formato
Enteros ≥ 0
apt_total_floors, apt_floor
Requerido
no
Formato
Enteros ≥ 0, juntos o ninguno; apt_floor ≤ apt_total_floors
construction_area
Requerido
no
Formato
Número ≥ 0 (m²)
land.area, land.front, land.side
Requerido
no
Formato
Números ≥ 0
land.unit
Requerido
no
Formato
m2 (por defecto) o ha
construction_year
Requerido
no
Formato
Un año (entero ≥ 0), first_use o building
monthly_maintenance_fee
Requerido
no
Formato
Número ≥ 0 (por defecto 0)
internal_key
Requerido
no
Formato
Hasta 16 caracteres
months_of_rent_deposit
Requerido
no
Formato
Entero de 0 a 6
exclusivity.has_exclusivity, .shared_commission, .shares_half
Requerido
no
Formato
0 o 1
exclusivity.conditions
Requerido
no
Formato
Hasta 1000 caracteres
amenities
Requerido
no
Formato
Lista separada por comas, con espacios en lugar de guion bajo: pet friendly,pool,gym
other_links.youtube, other_links.matterport
Requerido
no
Formato
URL
external_id
Requerido
no (solo OAuth)
Formato
Texto de 1 a 255 caracteres; ver cargas idempotentes
assigned_to
Requerido
no (solo OAuth)
Formato
uid de un agente aprobado y humano de la inmobiliaria conectada. Solo al crear; queda como responsable de la propiedad. created_by siempre es quien conectó

Las fotos se explican en la sección 9. Toda propiedad creada por este API nace publicada: is_active es siempre verdadero al crear.

Respuesta 200 (las rutas de escritura con fotos usan esta envoltura):

JSON
{"error": null, "invalid_images": 0, "details": null, "content": { …propiedad completa… }}

invalid_images cuenta las fotos que no se pudieron procesar. La propiedad completa (content):

JSON
{
  "public_id": "NJ-1A2B3", "title": "Casa en Zihuatanejo", "foreclosure": false,
  "images": [{"url": "https://cdn.neojaus.com/properties/<uid>/<name>", "title": null,
              "name": "<name>", "order": 1}],
  "description": "…", "bedrooms": 3, "bathrooms": 2, "half_bathrooms": 1, "parking_spaces": 2,
  "lot_size": 200.0, "construction_size": 150.0, "lot_length": 10.0, "lot_width": 10.0,
  "covered_space": 0, "floors": 2, "floor": null, "age": 5, "internal_id": "a-101",
  "expenses": "$ 1,500.00", "property_type": "Casa",
  "agent": {"id": "<uid>", "name": "…", "full_name": "…", "mobile_phone": "…",
            "profile_image_url": null, "email": "…"},
  "created_at": "…", "updated_at": "…", "published_at": "…",
  "features": [{"name": "Pet Friendly", "category": "General"}],
  "public_url": "https://neojaus.com/propiedades/<slug>", "collaboration_notes": null,
  "property_files": [], "videos": [], "virtual_tour": null,
  "exclusive": false, "shared_commission_percentage": null, "private_description": null,
  "location": {"name": "Centro, Zihuatanejo, Guerrero", "latitude": 17.6, "longitude": -101.5,
               "street": "…", "postal_code": "40880", "show_exact_location": true,
               "exterior_number": null, "interior_number": null, "country": "…"},
  "tags": [], "show_prices": true, "share_commission": false,
  "operations": [{"type": "sale", "amount": 2500000.0, "formated_amount": "MXN$ 2,500,000.00",
                  "currency": "MXN", "unit": "valor_total",
                  "commission": {"type": null, "value": null, "currency": null}}],
  "status": "published"
}

status es published o not_published según is_active. age es un año, new (first_use), under_construction (building) o null.

Errores de validación (400). details junta todos los errores del formulario:

JSON
{"error": "Validation(s) failed. Request Id: …", "details": [ … ], "invalid_images": 0, "content": null}
  • Campos requeridos: property_type is required., operation_types is required., name is required., description is required., location.mexican_state is required., location.coords.lat is required., location.coords.lng is required., location.street is required., location.neighborhood is required., location.zip is required., location.municipality is required., pricing.sales.price is required for sale properties., pricing.rents.price is required for rental properties.
  • Enumeraciones: Invalid property_type: '<x>'. Valid values: …, Invalid operation_type: '<x>'. Valid values: …, temporary_rental operation type is not yet supported., At least one operation type (sale or rental) must be specified., Invalid location.mexican_state: '<x>'., Invalid land.unit: '<x>'., Invalid pricing.sales.currency_type: '<x>'., Invalid pricing.sales.based_on: '<x>'., Invalid pricing.sales.commission data: … (y los de pricing.rents.*), Invalid amenity: '<x>'. Valid amenities: …
  • Rangos y formatos: name must be between 5 and 100 characters., name appears to contain personal information or external links., description must be between 10 and 4096 characters., rooms must be greater than or equal to 0. / rooms must be a valid integer. (igual para parking_spots, bathrooms, half_bathrooms, floors, months_of_rent_deposit), months_of_rent_deposit must not exceed 6., construction_area must be a valid number., land.area must be greater than or equal to 0., construction_year must be a valid year or first_use or building., monthly_maintenance_fee must be a valid number., internal_key must not exceed 16 characters., pricing.sales.price must be greater than 0., pricing.sales.price must be a valid number., location.coords.lat must be between -90 and 90., location.coords.lng must be between -180 and 180., location.street must be between 1 and 255 characters., location.number_ext must not exceed 10 characters., location.zip must be between 1 and 10 characters., apt_floor (<n>) cannot be greater than apt_total_floors (<n>)., Both apt_total_floors and apt_floor must be provided together, or neither., exclusivity.has_exclusivity must be a boolean value (0 or 1)., exclusivity.conditions must not exceed 1000 characters.
  • Solo OAuth: external_id must be between 1 and 255 characters., Invalid assigned_to: '<x>'. It must be the id of an approved agent of the connected agency.

Otros errores de la creación: 400 You need an admin account for this endpoint.; 400 There needs to be at least 1 image as part of the property.; 400 A property cannot have more than 40 images.; 400 Image number: <n> does not have a valid format. Image: '<archivo>', Valid formats: …; 400 Please ensure the uploaded files do not exceed the maximum allowed size (50 MB).; 500 Ouch! An unexpected error ocurred. Please contact 'hola [at] neojaus [dot] com' for help. Request Id: … (cita el Request Id si escribes a soporte).

PUT /properties/<public_id>: actualizar

application/json (400 Content-Type must be application/json). Solo manda lo que cambia: lo que no va, se conserva. Acepta la forma que devuelve GET; los nombres no son los del formulario de creación.

title
Formato
5 a 100 caracteres (mismas reglas que name)
description
Formato
10 a 4096 caracteres
property_type
Formato
Uno de los tipos
bedrooms, bathrooms, half_bathrooms, parking_spaces, floors, floor
Formato
Enteros ≥ 0
construction_size, lot_size, lot_width, lot_length, expenses
Formato
Números ≥ 0
age
Formato
Año, first_use o building
internal_id
Formato
Hasta 16 caracteres
operations
Formato
Lista de {type: "sale"|"rental", amount, currency, unit, commission: {value, type, currency}}. Si la mandas, reemplaza las operaciones: las que no incluyas dejan de existir. amount > 0
location
Formato
{latitude, longitude, street, exterior_number, interior_number, postal_code, name}; name es "Colonia, Municipio, Estado"
features
Formato
Lista de {"name": "pet friendly"}
videos
Formato
Lista; se toma la primera (YouTube)
virtual_tour
Formato
URL de Matterport
exclusive, share_commission, shared_commission_percentage
Formato
Booleanos

PUT no cambia las fotos ni is_active. Errores: 400 Invalid property ID format., 404 Property not found., 403 Not authorized to update this property., 400 Invalid JSON passed., y 400 con details para la validación (por ejemplo title must be between 5 and 100 characters., bedrooms must be a valid integer., Invalid operation type: '<x>'., operation.amount is required for <tipo>., location.latitude must be between -90 and 90., Invalid feature: '<x>'. Valid features: …). Si la propiedad tiene una propuesta o renta en curso, el PUT responde 500; el POST con external_id responde 409.

PATCH /properties/<public_id>/is_active

Cuerpo JSON {"is_active": true} o {"is_active": false} (un booleano de verdad; otra cosa es 400 Invalid JSON passed.). Responde 200 con {"error": null, "details": null, "content": {…}}. Errores 400 con el mensaje del servidor, por ejemplo No puedes activar una anuncio sin ninguna imagen. y No puedes activar una propiedad que está en renta.

Valores de las enumeraciones

property_type, con mayúscula inicial y espacios, tal cual:

Bodega comercialBodega industrialCasaCasa con uso de sueloCasa en condominioDepartamentoEdificioHuertaLocal comercialLocal en centro comercialNave industrialOficinaQuintaRanchoTerrenoTerreno comercialTerreno industrialVilla

location.mexican_state:

aguascalientesbaja_californiabaja_california_surcampechechiapaschihuahuaciudad_de_mexicocoahuilacolimadurangoestado_de_mexicoguanajuatoguerrerohidalgojaliscomichoacanmorelosnayaritnuevo_leonoaxacapueblaqueretaroquintana_roosan_luis_potosisinaloasonoratabascotamaulipastlaxcalaveracruzyucatanzacatecas

El servidor también acepta other, los estados de EE. UU. en minúsculas con guion bajo (new_york, texas, …, district_of_columbia) y las provincias de Costa Rica:

san_josealajuelacartagoherediaguanacastepuntarenaslimon

amenities: aquí con guion bajo, como las guardamos, pero se mandan con espacios (amenities=pet friendly,pool,security 247). El mensaje Invalid amenity lista los valores válidos exactos.

is_amuebladapet_friendlypoolparkingjacuzzibathtubintegral_kitchenairconcoworkingcarbon_monoxide_alarmsmoke_alarmfire_extinguisherstep_free_entrywayaccessibility_featuresgymrooftop_terracemultiple_uses_terracelobbyoutdoor_bbq_areaconciergesecurity_247delivery_zonepet_parkcovered_parkingbike_parkingstorage_unitev_charging_stationsclubhouselaundry_facilitiesbusiness_centersaunasoccer_courttennis_courtpadel_courtping_pong_tablekids_playgroundguests_parkingwifi_common_areasprivate_balconyprivate_patiocontrolled_access_entrygranite_countertopsstainless_steel_applianceshardwood_floorswalk_in_closetssmart_homewashing_roomgreen_areasservice_roomgardenis_condo
  • currency_type / currency: mxn, usd, eur, crc
  • based_on / unit: valor_total, m2, ha
  • commission_type: fixed_amount, months, percentaje (así se escribe)
  • land.unit: m2, ha
  • construction_year: año, first_use, building
09

Fotos

  • Una propiedad necesita entre 1 y 40 fotos, enviadas como partes de archivo del multipart/form-data del POST. El nombre de cada parte solo importa para ordenar: si son numéricos (1, 2, 10) se ordenan numéricamente; si no, en el orden de llegada. Formatos: png, jpg, jpeg, webp, heic, heif. Tamaño total de la petición: 50 MB.
  • Cada entrada de images en la respuesta trae url, name y order. name es el nombre de archivo (el último segmento de la URL) y es la única forma de direccionar una foto: order es informativo, puede repetirse y deja huecos al borrar.
Añadir
Cómo
POST /properties/<public_id>/images, multipart/form-data, al menos una foto. El total (las que ya tiene más las nuevas) no puede pasar de 40. Las nuevas se agregan al final. Responde con la envoltura de creación; error es No images uploaded correctly solo si no se pudo subir ninguna. Sin fotos: 400 There needs to be at least 1 image as part of the request.
Quitar
Cómo
DELETE /properties/<public_id>/images/<name>. 404 Image not found., 400 Invalid image name., y 400 A property needs at least 1 image. Add another one before removing this one. si es la última. Responde 200 con {"error": null, "details": null, "content": {…}}
Reemplazar todas
Cómo
Re-envía el POST con el mismo external_id y las fotos nuevas (sección 10)

Para cambiar la única foto de una propiedad, añade la nueva y luego quita la vieja. Las rutas de fotos tienen los mismos permisos que el PUT (incluido 403 Not authorized to update this property.).

Si ninguna foto de una creación se procesa
La propiedad queda creada y desactivada (no puede estar publicada sin fotos), y la respuesta puede llegar como 500 en lugar del 200 No images uploaded correctly. Re-envía con el mismo external_id y fotos válidas, y luego publícala con PATCH …/is_active.
10

external_id: cargas idempotentes

Sin external_id, reintentar un POST crea un duplicado. Con OAuth puedes mandar tu propio id (external_id, hasta 255 caracteres) y el POST se vuelve un upsert:

  • Id nuevo (para esa aplicación y esa inmobiliaria): crea la propiedad, como siempre.
  • Id ya conocido: aplica el cuerpo a la propiedad existente como lo haría un PUT, y reemplaza sus fotos por las que mandas (sube primero las nuevas y luego cambia; nunca hay un momento sin fotos). Responde 200 con la propiedad actualizada e invalid_images como siempre. Si ninguna foto nueva se sube, las viejas se quedan y la respuesta es error: "No images uploaded correctly".
  • Un reenvío debe traer las fotos (1 a 40): el formulario completo se valida igual que al crear.
  • El mismo external_id en dos inmobiliarias distintas son dos propiedades distintas.
  • 409 si la propiedad tiene una propuesta o renta en curso, con el mensaje del servidor (p. ej. No puedes actualizar una propiedad que tiene alguna propuesta o renta en curso…), antes de tocar ninguna foto. Reintenta más tarde.
  • assigned_to solo cuenta al crear; un reenvío no reasigna la propiedad.
Un reenvío nunca re-publica
Conserva el is_active que la inmobiliaria dejó en NeoJaus: si la desactivó, sigue desactivada aunque tu carga nocturna la mande otra vez. Para ponerla de vuelta en línea: PATCH /properties/<public_id>/is_active con {"is_active": true}.

Guarda el public_id que te devolvemos junto a tu external_id: lo necesitas para PUT, PATCH y las rutas de fotos.

11

Límites de uso

Los límites son por conexión: cada inmobiliaria conectada tiene los suyos, y no se reinician cuando rota el token.

Escrituras: POST /properties, PUT, PATCH …/is_active, POST …/images, DELETE …/images/<name>
Límite
1 por segundo y 30 por minuto, por ruta (cada ruta tiene su propia ventana)
GET /properties y GET /properties/<id>
Límite
10 por segundo y 500 por minuto
POST /oauth/token
Límite
120 por minuto por client_id; además, authorization_code 10 por minuto por client_id, y refresh_token 10 por minuto por conexión
POST /oauth/revoke
Límite
120 por minuto por client_id

Una carga inicial larga simplemente tarda lo que tarda: 600 propiedades de una inmobiliaria son unos 20 minutos a 30 por minuto. No hay un límite más alto por aplicación. Reparte las cargas, espacia las peticiones y reintenta cuando recibas 429:

  • En /oauth/* el 429 trae Retry-After (segundos) y cuerpo {"error": "rate_limit_exceeded", …}.
  • En /v1/properties* el 429 llega con el cuerpo {"msg": "Rate limit exceeded. Please try again later.", "data": null} sin garantía de Retry-After: espera al menos un segundo y retrocede progresivamente (hasta un minuto si es el tope por minuto).

Las cargas de varias inmobiliarias no se estorban entre sí, pero sí los refresh de una misma conexión.

12

Antes de salir a producción

  • No hay notificaciones. Una revocación o un plan vencido solo se ven como 401. Los cambios que la inmobiliaria haga en NeoJaus (editar una propiedad, despublicarla) los descubres consultando: GET /v1/properties?updated_after=<ISO 8601>&sort_by=updated_at-asc, guardando la marca de tiempo de la última sincronización.
  • Un refresh token sin usar caduca (30 días; 24 horas si eres cliente público) y la inmobiliaria debe volver a conectar. Reparte tus refresh en el tiempo (no los lances todos a la misma hora) y reintenta los 429.
  • La propiedad puede editarse también en NeoJaus. El reenvío por external_id pisa los campos que mandas (pero no is_active), así que manda el formulario completo solo cuando quieras que tu sistema mande.
  • Guarda el Request Id de los 500 para soporte: [email protected].
Las propiedades llegan a los portales de la inmobiliaria
Las propiedades creadas por este API se sindican a los portales que la inmobiliaria tenga activados en NeoJaus, igual que las que crea a mano. Si tu plataforma también publica en esos portales, la inmobiliaria verá duplicados allí, salvo que desactive la sincronización del portal para esas propiedades (es una opción por propiedad en NeoJaus). Avísale a la inmobiliaria antes de la primera carga.
13

Clientes públicos (solo navegador)

Si tu plataforma no tiene servidor, la registramos como pública: sin secreto y con tus orígenes. Con NeoJausAuth.authorize({exchange: true}) el SDK hace el intercambio desde el navegador y devuelve {access_token, refresh_token, expires_in, scope}. El SDK guarda los tokens en memoria, los refresca antes de que venzan (NeoJausAuth.refresh()), y NeoJausAuth.fetch(path, init) agrega el Bearer (solo hacia apiUrl).

Puedes persistir los tokens con getTokens() / setTokens(), pero cualquier script de tu página puede leerlos, y un refresh token de navegador es una credencial de 24 horas.

CORS en las rutas con token. /oauth/userinfo y las siete rutas de propiedades contestan CORS con la misma regla que /oauth/token: solo tus orígenes registrados, exactos, con Access-Control-Allow-Methods = los métodos de esa ruta, Access-Control-Allow-Headers: Content-Type, Authorization y nunca Access-Control-Allow-Credentials.

  • Cada llamada lleva ?client_id=<tu client_id>. El Authorization: Bearer hace que el navegador mande un preflight (OPTIONS) antes de cada llamada, y el preflight no lleva el token: la única forma de saber de qué aplicación se trata es ese parámetro. Lo mismo vale para los 401 de token inválido o vencido: sin ?client_id=, el navegador no te deja leerlos. NeoJausAuth.fetch() lo agrega solo; si llamas con tu propio fetch, agrégalo tú.
  • Una vez reconocido el token, manda la aplicación del token, no el parámetro: un ?client_id= de otra aplicación no abre tu respuesta a sus orígenes.
  • Una aplicación confidencial no recibe CORS en estas rutas aunque le hayamos registrado orígenes, y una aplicación suspendida tampoco.
  • «Sin CORS» no es «no se puede llamar». Un POST con application/x-www-form-urlencoded no hace preflight, así que desde un origen no registrado la petición sí llega y se ejecuta; el navegador solo retiene la respuesta. No lo uses como control de acceso.
  • Las peticiones con credenciales se rechazan. Un fetch con credentials: "include" lo bloquea el navegador. No mandes cookies: el SDK ya hace la petición sin credenciales.

En /oauth/token y /oauth/revoke el SDK manda ?client_id= en la URL además del cuerpo para que el preflight sepa de qué aplicación se trata; por eso ambos deben coincidir. Paso a paso en la demo del SDK.

¿Listo para integrar?

Escríbenos con el nombre de tu plataforma, tus redirect URIs, los scopes que necesitas y el tipo de aplicación. Te enviamos tu client_id para que empieces.