# Alquila.do API REST de cuenta

> Base: `https://www.alquila.do/api/external/v1`. Auth: OAuth 2.0 Authorization Code + PKCE S256. `GET /search` es el marketplace en formato compacto; el resto opera sobre la cuenta autorizada.

OpenAPI: https://www.alquila.do/developers/openapi.yaml
Índice para agentes: https://www.alquila.do/developers/llms.txt

## Autenticación

1. Crea una app en `/developers/apps` y guarda `client_id` / `client_secret`.
2. Autoriza (el usuario entra con Alquila.do):

```json
{
  "method": "GET",
  "url": "https://www.alquila.do/oauth/authorize",
  "query": {
    "client_id": "{CLIENT_ID}",
    "redirect_uri": "{REDIRECT_URI}",
    "response_type": "code",
    "scope": "openid email profile offline_access listings_write",
    "code_challenge": "{S256_CHALLENGE}",
    "code_challenge_method": "S256",
    "state": "{CSRF}"
  }
}
```

3. Intercambia el `code`:

```
POST https://www.alquila.do/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code={CODE}&redirect_uri={REDIRECT_URI}&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&code_verifier={VERIFIER}
```

4. Llama la API:

```
GET https://www.alquila.do/api/external/v1/me
Authorization: Bearer {ACCESS_TOKEN}
```

Refresh: `grant_type=refresh_token&refresh_token={REFRESH}&client_id=...&client_secret=...`.
Sin el scope del recurso: `403 insufficient_scope`. Token inválido: `401 unauthorized`.

## Envelope

Éxito:

```json
{
  "success": true,
  "data": {
    "id": 1
  }
}
```

Error:

```json
{
  "success": false,
  "error_code": "insufficient_scope",
  "message": "Permiso insuficiente para este recurso."
}
```

Paginación: `page` (default 1), `per_page` (default 10, máx. 20). CORS `*` sin credentials.
Tokens de apps de desarrollador: 2 horas + `refresh_token` si pediste `offline_access`.

## Búsqueda del marketplace

### GET /search

Busca propiedades publicadas. Mismos filtros y ranking que el MCP (promocionadas primero). Máximo 8 resultados, sin paginación. Cada ítem: título, thumbnail, link y precios.

- Scope: `cualquiera (Bearer válido)`
- Query: `location`, `property_type`, `operation_type` (rent|sale), `min_price`, `max_price`, `bedrooms`, `bathrooms`
- Notas: Bearer válido, sin scope extra. Sin contacto, descripción ni IDs internos. Más resultados: `search_url`.

Output:

```json
{
  "success": true,
  "data": {
    "found": true,
    "location_recognized": true,
    "total_count": 120,
    "more_on_website": true,
    "search_url": "https://alquila.do/propiedades?neighborhood=Piantini",
    "properties": [
      {
        "title": "Apartamento en Piantini",
        "thumbnail": "https://alquila.do/rails/active_storage/representations/redirect/.../photo.jpg",
        "link": "https://alquila.do/propiedad/apartamento-en-piantini",
        "prices": {
          "rent": 25000,
          "currency": "DOP"
        }
      }
    ]
  }
}
```

## Perfil

### GET /me

Usuario resource owner y su perfil de propietario.

- Scope: `cualquiera (Bearer válido)`

Output:

```json
{
  "success": true,
  "data": {
    "id": 1,
    "email": "ana@example.com",
    "full_name": "Ana Pérez",
    "confirmed": true,
    "landlord_profile": {
      "id": 9,
      "slug": "ana-perez",
      "full_name": "Ana Pérez",
      "bio": null,
      "contact_number": null,
      "whatsapp_number": null,
      "show_contact_number": false,
      "show_whatsapp_number": false,
      "allow_only_id_verified_contacts": false
    }
  }
}
```

### PATCH /me

Actualiza nombre del usuario.

- Scope: `profile_write`
- Body: `full_name` o `first_name` / `last_name`

Output:

```json
{
  "success": true,
  "data": {
    "id": 1,
    "full_name": "Ana Pérez"
  }
}
```

### PATCH /me/landlord_profile

Actualiza el perfil de propietario (no RNC).

- Scope: `profile_write`
- Body: `landlord_profile`: full_name, bio, slug, contact_number, whatsapp_number, show_contact_number, show_whatsapp_number, allow_only_id_verified_contacts

Output:

```json
{
  "success": true,
  "data": {
    "id": 1,
    "landlord_profile": {
      "slug": "ana-perez"
    }
  }
}
```

## Propiedades propias

### GET /properties

Lista y busca propiedades del resource owner.

- Scope: `listings_write`
- Query: `q`, `status`, `rental_unit_type_id`, `page`, `per_page`

Output:

```json
{
  "success": true,
  "data": {
    "total": 1,
    "page": 1,
    "per_page": 10,
    "total_pages": 1,
    "properties": [
      {
        "id": 42,
        "slug": "apartamento-en-piantini",
        "title": "Apartamento en Piantini",
        "status": "unlisted",
        "property_type": "Apartamento",
        "rent_price": 25000,
        "selling_price": null,
        "currency": "DOP",
        "location": "Piantini, Santo Domingo",
        "photos_count": 3,
        "ready_to_publish": false,
        "quality": {
          "percent": 72,
          "threshold": 90,
          "suggestions": [
            "Agrega más fotos"
          ]
        },
        "url": "https://alquila.do/propiedad/apartamento-en-piantini",
        "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
        "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
        "message": "Propiedad creada como borrador."
      }
    ]
  }
}
```

### POST /properties

Crea un borrador (`unlisted`). Si hay precio sin moneda, se usa DOP.

- Scope: `listings_write`
- Body: Campos de RentalUnitPublisher (title, rental_unit_type_id, province_id, municipality_id, description, rent_price, selling_price, currency_id, bedrooms, bathrooms, amenity_ids, …)
- Notas: 201. Gates: identity_verification_required, listing_limit_reached.

Output:

```json
{
  "success": true,
  "data": {
    "id": 42,
    "slug": "apartamento-en-piantini",
    "title": "Apartamento en Piantini",
    "status": "unlisted",
    "property_type": "Apartamento",
    "rent_price": 25000,
    "selling_price": null,
    "currency": "DOP",
    "location": "Piantini, Santo Domingo",
    "photos_count": 3,
    "ready_to_publish": false,
    "quality": {
      "percent": 72,
      "threshold": 90,
      "suggestions": [
        "Agrega más fotos"
      ]
    },
    "url": "https://alquila.do/propiedad/apartamento-en-piantini",
    "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
    "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
    "message": "Propiedad creada como borrador."
  }
}
```

### GET /properties/:id

Detalle de una propiedad propia (id o slug).

- Scope: `listings_write`
- Notas: 404 si no es del resource owner (no se filtra existencia de terceros).

Output:

```json
{
  "success": true,
  "data": {
    "id": 42,
    "slug": "apartamento-en-piantini",
    "title": "Apartamento en Piantini",
    "status": "unlisted",
    "property_type": "Apartamento",
    "rent_price": 25000,
    "selling_price": null,
    "currency": "DOP",
    "location": "Piantini, Santo Domingo",
    "photos_count": 3,
    "ready_to_publish": false,
    "quality": {
      "percent": 72,
      "threshold": 90,
      "suggestions": [
        "Agrega más fotos"
      ]
    },
    "url": "https://alquila.do/propiedad/apartamento-en-piantini",
    "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
    "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
    "message": "Propiedad creada como borrador."
  }
}
```

### PATCH /properties/:id

Actualiza una propiedad propia.

- Scope: `listings_write`
- Body: Mismos campos que POST

Output:

```json
{
  "success": true,
  "data": {
    "id": 42,
    "slug": "apartamento-en-piantini",
    "title": "Apartamento en Piantini",
    "status": "unlisted",
    "property_type": "Apartamento",
    "rent_price": 25000,
    "selling_price": null,
    "currency": "DOP",
    "location": "Piantini, Santo Domingo",
    "photos_count": 3,
    "ready_to_publish": false,
    "quality": {
      "percent": 72,
      "threshold": 90,
      "suggestions": [
        "Agrega más fotos"
      ]
    },
    "url": "https://alquila.do/propiedad/apartamento-en-piantini",
    "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
    "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
    "message": "Propiedad creada como borrador."
  }
}
```

### DELETE /properties/:id

Elimina una propiedad no publicada.

- Scope: `listings_write`
- Notas: Si está publicada: despublica antes.

Output:

```json
{
  "success": true,
  "data": {
    "message": "Propiedad eliminada.",
    "id": 42
  }
}
```

### POST /properties/:id/publish

Publica si pasa `ready_to_publish?`.

- Scope: `listings_write`

Output:

```json
{
  "success": true,
  "data": {
    "id": 42,
    "slug": "apartamento-en-piantini",
    "title": "Apartamento en Piantini",
    "status": "available",
    "property_type": "Apartamento",
    "rent_price": 25000,
    "selling_price": null,
    "currency": "DOP",
    "location": "Piantini, Santo Domingo",
    "photos_count": 3,
    "ready_to_publish": false,
    "quality": {
      "percent": 72,
      "threshold": 90,
      "suggestions": [
        "Agrega más fotos"
      ]
    },
    "url": "https://alquila.do/propiedad/apartamento-en-piantini",
    "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
    "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
    "message": "Propiedad publicada exitosamente.",
    "published": true
  }
}
```

### POST /properties/:id/unpublish

Despublica. Requiere motivo.

- Scope: `listings_write`
- Body: `unpublish_reason`: sold_or_rented_on_alquila_do | sold_or_rented_elsewhere | not_yet_sold_or_rented | prefer_not_to_say

Output:

```json
{
  "success": true,
  "data": {
    "id": 42,
    "slug": "apartamento-en-piantini",
    "title": "Apartamento en Piantini",
    "status": "unlisted",
    "property_type": "Apartamento",
    "rent_price": 25000,
    "selling_price": null,
    "currency": "DOP",
    "location": "Piantini, Santo Domingo",
    "photos_count": 3,
    "ready_to_publish": false,
    "quality": {
      "percent": 72,
      "threshold": 90,
      "suggestions": [
        "Agrega más fotos"
      ]
    },
    "url": "https://alquila.do/propiedad/apartamento-en-piantini",
    "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
    "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
    "message": "Propiedad despublicada."
  }
}
```

### GET /properties/:id/quality

Índice de calidad del anuncio propio.

- Scope: `listings_write`

Output:

```json
{
  "success": true,
  "data": {
    "id": 42,
    "slug": "apartamento-en-piantini",
    "title": "Apartamento en Piantini",
    "status": "unlisted",
    "property_type": "Apartamento",
    "rent_price": 25000,
    "selling_price": null,
    "currency": "DOP",
    "location": "Piantini, Santo Domingo",
    "photos_count": 3,
    "ready_to_publish": false,
    "quality": {
      "percent": 72,
      "threshold": 90,
      "suggestions": [
        "Agrega más fotos"
      ]
    },
    "url": "https://alquila.do/propiedad/apartamento-en-piantini",
    "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
    "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
    "message": "Índice de calidad del anuncio."
  }
}
```

### POST /properties/:property_id/photos

Sube fotos (máx. 10, 5 MB, jpeg/png/webp/gif).

- Scope: `listings_write`
- Body: multipart `photos[]` o JSON `{ "photos": [{ "url": "https://..." }] }`
- Notas: 201. Throttle: 5 POST /photos por min / usuario.

Output:

```json
{
  "success": true,
  "data": {
    "photos_count": 3,
    "message": "Fotos subidas correctamente.",
    "quality": {
      "percent": 80
    }
  }
}
```

### DELETE /properties/:property_id/photos/:id

Elimina una foto.

- Scope: `listings_write`

Output:

```json
{
  "success": true,
  "data": {
    "photos_count": 2,
    "message": "Foto eliminada."
  }
}
```

## Agencias

### GET /agencies

Agencias donde el usuario es owner o miembro active.

- Scope: `agencies_read`

Output:

```json
{
  "success": true,
  "data": [
    {
      "id": 3,
      "name": "Agencia Demo",
      "slug": "agencia-demo",
      "status": "active",
      "role": "owner"
    }
  ]
}
```

### GET /agencies/:id

Detalle de una agencia accesible.

- Scope: `agencies_read`
- Notas: Agencia ajena: 404.

Output:

```json
{
  "success": true,
  "data": {
    "id": 3,
    "name": "Agencia Demo",
    "slug": "agencia-demo",
    "status": "active",
    "role": "owner",
    "email": "agencia@example.com",
    "phone": "8090000000",
    "website": null,
    "description": null,
    "address": null
  }
}
```

### PATCH /agencies/:id

Actualiza datos de agencia (solo owner/partner).

- Scope: `agencies_write`
- Body: `agency`: name, address, phone, email, website, description, redes, auto_detect_enabled

Output:

```json
{
  "success": true,
  "data": {
    "id": 3,
    "name": "Agencia Demo"
  }
}
```

### GET /agencies/:id/properties

Inventario del equipo (unión de miembros active + owner). Solo lectura.

- Scope: `agencies_read`
- Query: `q`, `status`, `rental_unit_type_id`, `page`, `per_page`

Output:

```json
{
  "success": true,
  "data": {
    "properties": [
      {
        "id": 42,
        "slug": "apartamento-en-piantini",
        "title": "Apartamento en Piantini",
        "status": "unlisted",
        "property_type": "Apartamento",
        "rent_price": 25000,
        "selling_price": null,
        "currency": "DOP",
        "location": "Piantini, Santo Domingo",
        "photos_count": 3,
        "ready_to_publish": false,
        "quality": {
          "percent": 72,
          "threshold": 90,
          "suggestions": [
            "Agrega más fotos"
          ]
        },
        "url": "https://alquila.do/propiedad/apartamento-en-piantini",
        "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
        "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
        "message": "Propiedad creada como borrador."
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 10,
      "total": 1,
      "total_pages": 1
    }
  }
}
```

### GET /agencies/:id/properties/:property_id

Una propiedad del inventario de la agencia.

- Scope: `agencies_read`

Output:

```json
{
  "success": true,
  "data": {
    "id": 42,
    "slug": "apartamento-en-piantini",
    "title": "Apartamento en Piantini",
    "status": "unlisted",
    "property_type": "Apartamento",
    "rent_price": 25000,
    "selling_price": null,
    "currency": "DOP",
    "location": "Piantini, Santo Domingo",
    "photos_count": 3,
    "ready_to_publish": false,
    "quality": {
      "percent": 72,
      "threshold": 90,
      "suggestions": [
        "Agrega más fotos"
      ]
    },
    "url": "https://alquila.do/propiedad/apartamento-en-piantini",
    "view_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini",
    "edit_url": "https://alquila.do/propietario/propiedades/apartamento-en-piantini/edit",
    "message": "Propiedad creada como borrador."
  }
}
```

## Invitaciones de agencia

### GET /agencies/:id/invitations

Membresías/invitaciones de esa agencia (owner/partner).

- Scope: `invitations_write`

Output:

```json
{
  "success": true,
  "data": {
    "invitations": [
      {
        "id": 8,
        "email": "agente@example.com",
        "role": "agent",
        "status": "pending",
        "source": "manual",
        "user_id": 11,
        "created_at": "2026-10-28T12:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 10,
      "total": 1,
      "total_pages": 1
    }
  }
}
```

### POST /agencies/:id/invitations

Invita un email o user_id (un destinatario). Si no existe, Devise invite.

- Scope: `invitations_write`
- Body: `{ "email": "a@b.com", "role": "agent" }` o `{ "user_id": 11, "role": "manager" }` — roles: agent, manager, secretary, partner
- Notas: 201. Cupo del plan de agencia aplica. No hay CSV/bulk.

Output:

```json
{
  "success": true,
  "data": {
    "id": 8,
    "email": "a@b.com",
    "role": "agent",
    "status": "pending"
  }
}
```

### POST /agencies/:id/invitations/:invitation_id/resend

Reenvía una invitación pending.

- Scope: `invitations_write`

Output:

```json
{
  "success": true,
  "data": {
    "id": 8,
    "status": "pending",
    "message": "Invitación reenviada."
  }
}
```

### DELETE /agencies/:id/invitations/:invitation_id

Cancela una invitación pending.

- Scope: `invitations_write`

Output:

```json
{
  "success": true,
  "data": {
    "message": "Invitación cancelada."
  }
}
```

### GET /me/agency_invitations

Invitaciones pending enviadas al resource owner.

- Scope: `invitations_write`

Output:

```json
{
  "success": true,
  "data": [
    {
      "id": 8,
      "agency_id": 3,
      "agency_name": "Agencia Demo",
      "role": "agent",
      "status": "pending",
      "created_at": "2026-10-28T12:00:00Z"
    }
  ]
}
```

### POST /me/agency_invitations/:id/accept

Acepta una invitación recibida.

- Scope: `invitations_write`

Output:

```json
{
  "success": true,
  "data": {
    "id": 8,
    "status": "active",
    "message": "Te uniste a Agencia Demo."
  }
}
```

### POST /me/agency_invitations/:id/decline

Rechaza (elimina) una invitación recibida.

- Scope: `invitations_write`

Output:

```json
{
  "success": true,
  "data": {
    "message": "Rechazaste la invitación de Agencia Demo."
  }
}
```

## Leads

### GET /leads

Leads de listings propios. Con `agency_id` + `agencies_read`, leads agregados de esa agencia.

- Scope: `leads_read`
- Query: `agency_id`, `page`, `per_page`

Output:

```json
{
  "success": true,
  "data": {
    "leads": [
      {
        "id": 5,
        "name": "Luis",
        "email": "luis@example.com",
        "phone": "8091112222",
        "status": "new",
        "message": "Me interesa",
        "rental_unit_id": 42,
        "created_at": "2026-10-28T12:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 10,
      "total": 1,
      "total_pages": 1
    }
  }
}
```

### GET /leads/:id

Detalle y comentarios.

- Scope: `leads_read`

Output:

```json
{
  "success": true,
  "data": {
    "lead": {
      "id": 5,
      "name": "Luis",
      "status": "new",
      "source": "web",
      "updated_at": "2026-10-28T12:00:00Z"
    },
    "comments": [
      {
        "id": 1,
        "message": "Llamar mañana",
        "user": {
          "id": 1,
          "full_name": "Ana Pérez"
        },
        "created_at": "2026-10-28T12:00:00Z"
      }
    ]
  }
}
```

### PATCH /leads/:id

Cambia status. `comment` opcional crea un comentario.

- Scope: `leads_write`
- Body: `status` (requerido), `comment` (opcional)
- Notas: No hay DELETE. No cubre LeadRequest del marketplace.

Output:

```json
{
  "success": true,
  "data": {
    "id": 5,
    "status": "contacted"
  }
}
```

### POST /leads/:lead_id/comments

Agrega un comentario.

- Scope: `leads_write`
- Body: `message` o `comment`

Output:

```json
{
  "success": true,
  "data": {
    "id": 2,
    "message": "Llamar mañana",
    "created_at": "2026-10-28T12:00:00Z"
  }
}
```

## Catálogos

### GET /rental_unit_types

Tipos de propiedad.

- Scope: `cualquiera (Bearer válido)`

Output:

```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Apartamento",
      "description": null
    }
  ]
}
```

### GET /countries

Países.

- Scope: `cualquiera (Bearer válido)`

Output:

```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "República Dominicana",
      "code": "DO"
    }
  ]
}
```

### GET /currencies

Monedas.

- Scope: `cualquiera (Bearer válido)`

Output:

```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "code": "DOP",
      "name": "Peso dominicano"
    }
  ]
}
```

### GET /amenities

Amenidades activas.

- Scope: `cualquiera (Bearer válido)`

Output:

```json
{
  "success": true,
  "data": [
    {
      "id": 4,
      "name": "Piscina",
      "code": "pool"
    }
  ]
}
```

### GET /locations

Sectores (neighborhoods) para crear propiedades.

- Scope: `cualquiera (Bearer válido)`
- Query: `q`, `page`, `per_page`

Output:

```json
{
  "success": true,
  "data": {
    "locations": [
      {
        "id": 10,
        "name": "Piantini",
        "municipality": "Santo Domingo",
        "province": "Distrito Nacional"
      }
    ],
    "pagination": {
      "page": 1,
      "per_page": 10,
      "total": 1,
      "total_pages": 1
    }
  }
}
```
