API REST OAuth de propiedades y cuenta

Base URL de producción: https://www.alquila.do/api/external/v1. En desarrollo usa tu host local.

Autenticación

  1. Crea una app en Aplicaciones y guarda client_id y client_secret.
  2. Redirige al usuario a autorizar (OAuth 2.0 Authorization Code + PKCE S256):
GET https://www.alquila.do/oauth/authorize
  ?client_id={CLIENT_ID}
  &redirect_uri={REDIRECT_URI}
  &response_type=code
  &scope=openid%20email%20profile%20offline_access%20listings_write
  &code_challenge={S256_CHALLENGE}
  &code_challenge_method=S256
  &state={CSRF}

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}

Usa el access token (2 horas). Pide offline_access para refresh:

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

Refresh: grant_type=refresh_token. Sin el scope del recurso: 403 insufficient_scope. Token inválido: 401. Cuenta suspendida: 403.

Envelope

Éxito

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

Error

{
  "success": false,
  "error_code": "not_found",
  "message": "Recurso no encontrado."
}

Paginación: 10 por página, máximo 20. CORS * sin credenciales. Cabecera X-Request-Id en cada respuesta. Throttle: ver 429 + Retry-After.

GET /search busca el marketplace con los mismos filtros que el MCP; cada resultado solo trae título, thumbnail, link y precios. El resto de la API opera sobre la cuenta autorizada. Mutar propiedades de otro miembro del equipo no entra en v1.

Búsqueda del marketplace

Perfil

GET /me

Usuario resource owner y su perfil de propietario.

Scope: Bearer válido (cualquier scope de cuenta)

Output

{
  "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

{
  "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

{
  "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

{
  "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, …)

201. Gates: identity_verification_required, listing_limit_reached.

Output

{
  "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

404 si no es del resource owner (no se filtra existencia de terceros).

Output

{
  "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

{
  "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

Si está publicada: despublica antes.

Output

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

POST /properties/:id/publish

Publica si pasa `ready_to_publish?`.

Scope: listings_write

Output

{
  "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

{
  "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

{
  "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://..." }] }`

201. Throttle: 5 POST /photos por min / usuario.

Output

{
  "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

{
  "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

{
  "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

Agencia ajena: 404.

Output

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

201. Cupo del plan de agencia aplica. No hay CSV/bulk.

Output

{
  "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

{
  "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

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

GET /me/agency_invitations

Invitaciones pending enviadas al resource owner.

Scope: invitations_write

Output

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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)

No hay DELETE. No cubre LeadRequest del marketplace.

Output

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

POST /leads/:lead_id/comments

Agrega un comentario.

Scope: leads_write

Body: `message` o `comment`

Output

{
  "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: Bearer válido (cualquier scope de cuenta)

Output

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

GET /countries

Países.

Scope: Bearer válido (cualquier scope de cuenta)

Output

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

GET /currencies

Monedas.

Scope: Bearer válido (cualquier scope de cuenta)

Output

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

GET /amenities

Amenidades activas.

Scope: Bearer válido (cualquier scope de cuenta)

Output

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

GET /locations

Sectores (neighborhoods) para crear propiedades.

Scope: Bearer válido (cualquier scope de cuenta)

Query: `q`, `page`, `per_page`

Output

{
  "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
    }
  }
}