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.
client_secret (en el cuerpo o HTTP Basic) y PKCEclient_secret la petición se rechaza{code, codeVerifier} para tu servidorauthorize({exchange: true}) termina el intercambio y devuelve los tokens| Confidencial (tienes un servidor) | Público (solo navegador) | |
|---|---|---|
| Quién llama a /oauth/token | Tu servidor | Tu página, con el SDK |
| Autenticación del cliente | client_secret (en el cuerpo o HTTP Basic) y PKCE | Solo PKCE. Si mandas un client_secret la petición se rechaza |
| Vida del refresh token | 30 días | 24 horas |
| El SDK te devuelve | {code, codeVerifier} para tu servidor | authorize({exchange: true}) termina el intercambio y devuelve los tokens |
| CORS en las rutas del API | No: llama desde tu servidor | Solo los orígenes que registramos (ver Clientes públicos) |
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 flujo: botón, ventana, código, tokens
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)
<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_deniedsi la persona pulsa «Cancelar»,popup_blocked,popup_closed,timeout,invalid_configounsupported, o con el código de error del endpoint de tokens. - El SDK exige
crypto.subtle, es decir HTTPS olocalhost.
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:
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=S256Sin 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):
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:
{
"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.Scopes y quién puede autorizar
profile/oauth/userinfo: sub, name, emailagency/oauth/userinfo: datos de la inmobiliaria y si la persona es administradoraproperties:readagency.properties:readproperties:read)agency.properties:writeassigned_to (incluye agency.properties:read)| Scope | Qué permite | Quién puede autorizarlo |
|---|---|---|
profile | /oauth/userinfo: sub, name, email | Cualquier agente con plan premium |
agency | /oauth/userinfo: datos de la inmobiliaria y si la persona es administradora | Cualquier agente con plan premium |
properties:read | Listar y leer las propiedades del propio usuario | Cualquier agente con plan premium |
agency.properties:read | Listar y leer las propiedades de todos los agentes aprobados de la inmobiliaria (incluye properties:read) | Solo administradores de la inmobiliaria |
agency.properties:write | 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) | 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
scopede 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 responde401.
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_secreten el cuerpo (client_secret_post) o HTTP Basic (client_secret_basic), no los dos. Más PKCE. - Público: solo
client_idy PKCE. Mandar unclient_secretes un fallo de autenticación. grant_type=authorization_code: camposcode,redirect_uri(idéntico al de la autorización) ycode_verifier.grant_type=refresh_token: camporefresh_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.
{
"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.
Vida de los tokens, rotación y revocación
expires_in: 3600)| Duración | |
|---|---|
| Código de autorización | 10 minutos, un solo uso |
| Access token | 1 hora (expires_in: 3600) |
| Refresh token, confidencial | 30 días |
| Refresh token, público | 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_granty 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.
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.
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.Errores
/oauth/token y /oauth/revoke
Cuerpo {"error": "<código>", "error_description": "<texto en inglés>"} (RFC 6749 §5.2):
invalid_requestgrant_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 coincideninvalid_clientClient authentication failed.): cliente desconocido, aplicación suspendida, secreto ausente, equivocado o no esperadoinvalid_grantThe 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.unsupported_grant_typegrant_type must be authorization_code or refresh_token.rate_limit_exceededToo many requests. Retry after the number of seconds in Retry-After. Con cabecera Retry-Afterserver_errorSomething went wrong on our side. Request ID: …| HTTP | error | Cuándo |
|---|---|---|
| 400 | invalid_request | 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 | invalid_client | 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 | invalid_grant | 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 | unsupported_grant_type | grant_type must be authorization_code or refresh_token. |
| 429 | rate_limit_exceeded | Too many requests. Retry after the number of seconds in Retry-After. Con cabecera Retry-After |
| 500 | server_error | 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>"}:
{"error": "Invalid access token."} con WWW-Authenticate: Bearer error="invalid_token"invalid_grant, la conexión terminó{"error": "Get a premium plan at https://neojaus.com/plans to continue."}{"error": "This access token does not carry the scope <scope>."} con WWW-Authenticate: Bearer error="insufficient_scope", scope="<scope>"| HTTP | Cuerpo | Significa |
|---|---|---|
| 401 | {"error": "Invalid access token."} con WWW-Authenticate: Bearer error="invalid_token" | 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 | {"error": "Get a premium plan at https://neojaus.com/plans to continue."} | 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 | {"error": "This access token does not carry the scope <scope>."} con WWW-Authenticate: Bearer error="insufficient_scope", scope="<scope>" | 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.
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 /propertiesproperties:readGET /properties/<public_id>properties:readPOST /propertiesagency.properties:writeexternal_id)PUT /properties/<public_id>agency.properties:writePATCH /properties/<public_id>/is_activeagency.properties:writePOST /properties/<public_id>/imagesagency.properties:writeDELETE /properties/<public_id>/images/<name>agency.properties:write| Método y ruta | Scope | Qué hace |
|---|---|---|
GET /properties | properties:read | Lista paginada |
GET /properties/<public_id> | properties:read | Una propiedad completa |
POST /properties | agency.properties:write | Crea (o actualiza con external_id) |
PUT /properties/<public_id> | agency.properties:write | Actualiza campos (JSON) |
PATCH /properties/<public_id>/is_active | agency.properties:write | Publica o despublica |
POST /properties/<public_id>/images | agency.properties:write | Añade fotos |
DELETE /properties/<public_id>/images/<name> | agency.properties:write | Quita una foto |
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:
pagelimitproperty_typesstatusespublished, not_published, reserved, sold, renter, suspendedoperation_typesale o rentalmin_price, max_priceoperation_typemin_bedrooms, min_bathrooms, min_parking_spacesmin_construction_size, max_construction_size, min_lot_size, max_lot_sizeupdated_after, updated_before2026-09-01T00:00:00Zsort_byupdated_at-desc (por defecto) o updated_at-asc| Parámetro | Valores |
|---|---|
page | Desde 1 (por defecto 1) |
limit | 1 a 50 (por defecto 20) |
property_types | Lista separada por comas de los tipos de la tabla de enumeraciones |
statuses | Lista separada por comas: published, not_published, reserved, sold, renter, suspended |
operation_type | sale o rental |
min_price, max_price | Enteros; exigen operation_type |
min_bedrooms, min_bathrooms, min_parking_spaces | Enteros ≥ 0 |
min_construction_size, max_construction_size, min_lot_size, max_lot_size | Enteros ≥ 0, en m² |
updated_after, updated_before | ISO 8601, p. ej. 2026-09-01T00:00:00Z |
sort_by | 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.
{
"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.
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_typeoperation_typessale, rental o ambos separados por coma: sale,rental. temporary_rental aún no se admitenamedescriptionpricing.sales.pricepricing.sales.currency_typemxn (por defecto), usd, eur, crcpricing.sales.based_onvalor_total (por defecto), m2, hapricing.sales.commission.value.commission_type (fixed_amount, months, percentaje) y opcionalmente .currency_typepricing.rents.price, .currency_type, .based_on, .commission.*pricing.sales.*location.mexican_statelocation.coords.lat, location.coords.lnglocation.street, location.neighborhood, location.municipalitylocation.ziplocation.number_ext, location.number_introoms, bathrooms, half_bathrooms, parking_spots, floorsapt_total_floors, apt_floorapt_floor ≤ apt_total_floorsconstruction_arealand.area, land.front, land.sideland.unitm2 (por defecto) o haconstruction_yearfirst_use o buildingmonthly_maintenance_feeinternal_keymonths_of_rent_depositexclusivity.has_exclusivity, .shared_commission, .shares_half0 o 1exclusivity.conditionsamenitiespet friendly,pool,gymother_links.youtube, other_links.matterportassigned_tocreated_by siempre es quien conectó| Campo | Requerido | Formato |
|---|---|---|
property_type | sí | Uno de los tipos (enumeraciones, abajo) |
operation_types | sí | sale, rental o ambos separados por coma: sale,rental. temporary_rental aún no se admite |
name | sí | 5 a 100 caracteres. No puede contener datos personales ni enlaces externos |
description | sí | 10 a 4096 caracteres |
pricing.sales.price | si hay sale | Número > 0 |
pricing.sales.currency_type | no | mxn (por defecto), usd, eur, crc |
pricing.sales.based_on | no | valor_total (por defecto), m2, ha |
pricing.sales.commission.value | no | 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.* | si hay rental | Igual que pricing.sales.* |
location.mexican_state | sí | Un estado (enumeraciones, abajo), en minúsculas y con guion bajo |
location.coords.lat, location.coords.lng | sí | Números; lat de −90 a 90, lng de −180 a 180 |
location.street, location.neighborhood, location.municipality | sí | 1 a 255 caracteres |
location.zip | sí | 1 a 10 caracteres |
location.number_ext, location.number_int | no | Hasta 10 caracteres |
rooms, bathrooms, half_bathrooms, parking_spots, floors | no | Enteros ≥ 0 |
apt_total_floors, apt_floor | no | Enteros ≥ 0, juntos o ninguno; apt_floor ≤ apt_total_floors |
construction_area | no | Número ≥ 0 (m²) |
land.area, land.front, land.side | no | Números ≥ 0 |
land.unit | no | m2 (por defecto) o ha |
construction_year | no | Un año (entero ≥ 0), first_use o building |
monthly_maintenance_fee | no | Número ≥ 0 (por defecto 0) |
internal_key | no | Hasta 16 caracteres |
months_of_rent_deposit | no | Entero de 0 a 6 |
exclusivity.has_exclusivity, .shared_commission, .shares_half | no | 0 o 1 |
exclusivity.conditions | no | Hasta 1000 caracteres |
amenities | no | Lista separada por comas, con espacios en lugar de guion bajo: pet friendly,pool,gym |
other_links.youtube, other_links.matterport | no | URL |
external_id | no (solo OAuth) | Texto de 1 a 255 caracteres; ver cargas idempotentes |
assigned_to | no (solo OAuth) | 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):
{"error": null, "invalid_images": 0, "details": null, "content": { …propiedad completa… }}invalid_images cuenta las fotos que no se pudieron procesar. La propiedad completa (content):
{
"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:
{"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 depricing.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 paraparking_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.
titlename)descriptionproperty_typebedrooms, bathrooms, half_bathrooms, parking_spaces, floors, floorconstruction_size, lot_size, lot_width, lot_length, expensesagefirst_use o buildinginternal_idoperations{type: "sale"|"rental", amount, currency, unit, commission: {value, type, currency}}. Si la mandas, reemplaza las operaciones: las que no incluyas dejan de existir. amount > 0location{latitude, longitude, street, exterior_number, interior_number, postal_code, name}; name es "Colonia, Municipio, Estado"features{"name": "pet friendly"}videosvirtual_tourexclusive, share_commission, shared_commission_percentage| Campo | Formato |
|---|---|
title | 5 a 100 caracteres (mismas reglas que name) |
description | 10 a 4096 caracteres |
property_type | Uno de los tipos |
bedrooms, bathrooms, half_bathrooms, parking_spaces, floors, floor | Enteros ≥ 0 |
construction_size, lot_size, lot_width, lot_length, expenses | Números ≥ 0 |
age | Año, first_use o building |
internal_id | Hasta 16 caracteres |
operations | 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 | {latitude, longitude, street, exterior_number, interior_number, postal_code, name}; name es "Colonia, Municipio, Estado" |
features | Lista de {"name": "pet friendly"} |
videos | Lista; se toma la primera (YouTube) |
virtual_tour | URL de Matterport |
exclusive, share_commission, shared_commission_percentage | 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 industrialVillalocation.mexican_state:
aguascalientesbaja_californiabaja_california_surcampechechiapaschihuahuaciudad_de_mexicocoahuilacolimadurangoestado_de_mexicoguanajuatoguerrerohidalgojaliscomichoacanmorelosnayaritnuevo_leonoaxacapueblaqueretaroquintana_roosan_luis_potosisinaloasonoratabascotamaulipastlaxcalaveracruzyucatanzacatecasEl 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_josealajuelacartagoherediaguanacastepuntarenaslimonamenities: 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_condocurrency_type/currency:mxn,usd,eur,crcbased_on/unit:valor_total,m2,hacommission_type:fixed_amount,months,percentaje(así se escribe)land.unit:m2,haconstruction_year: año,first_use,building
Fotos
- Una propiedad necesita entre 1 y 40 fotos, enviadas como partes de archivo del
multipart/form-datadelPOST. 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
imagesen la respuesta traeurl,nameyorder.namees el nombre de archivo (el último segmento de la URL) y es la única forma de direccionar una foto:orderes informativo, puede repetirse y deja huecos al borrar.
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.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": {…}}| Operación | Cómo |
|---|---|
| Añadir | 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 | 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 | 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.).
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.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). Responde200con la propiedad actualizada einvalid_imagescomo siempre. Si ninguna foto nueva se sube, las viejas se quedan y la respuesta eserror: "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_iden dos inmobiliarias distintas son dos propiedades distintas. 409si 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_tosolo cuenta al crear; un reenvío no reasigna la propiedad.
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.
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.
POST /properties, PUT, PATCH …/is_active, POST …/images, DELETE …/images/<name>GET /properties y GET /properties/<id>POST /oauth/tokenclient_id; además, authorization_code 10 por minuto por client_id, y refresh_token 10 por minuto por conexiónPOST /oauth/revokeclient_id| Ruta | Límite |
|---|---|
Escrituras: POST /properties, PUT, PATCH …/is_active, POST …/images, DELETE …/images/<name> | 1 por segundo y 30 por minuto, por ruta (cada ruta tiene su propia ventana) |
GET /properties y GET /properties/<id> | 10 por segundo y 500 por minuto |
POST /oauth/token | 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 | 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/*el429traeRetry-After(segundos) y cuerpo{"error": "rate_limit_exceeded", …}. - En
/v1/properties*el429llega con el cuerpo{"msg": "Rate limit exceeded. Please try again later.", "data": null}sin garantía deRetry-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.
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_idpisa los campos que mandas (pero nois_active), así que manda el formulario completo solo cuando quieras que tu sistema mande. - Guarda el
Request Idde los500para soporte: [email protected].
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).
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>. ElAuthorization: Bearerhace 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 los401de token inválido o vencido: sin?client_id=, el navegador no te deja leerlos.NeoJausAuth.fetch()lo agrega solo; si llamas con tu propiofetch, 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
POSTconapplication/x-www-form-urlencodedno 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
fetchconcredentials: "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.