# API Reference

Rentalot provides a REST API for programmatic access to your account. Use it to sync properties from your PMS, build custom integrations, connect CRM tools, or let AI bots manage your rental operations.

## Base URL

The API base URL is the configured server origin with `/api/v1` appended:

```
https://rentalot.ai/api/v1
```

Free Trial access is available on the production origin while the account trial is active. Use `https://rentalot.ai/api/v1` by default; an optional local development server can use `http://localhost:3000/api/v1`. Keep the same origin for web sign-in, Settings/key issuance, dashboard readback, and API requests.

## Authentication

All API requests require an API key passed in the `Authorization` header:

```
Authorization: Bearer your_api_key_here
```

Generate and manage API keys from **Settings** > **API Keys** in your dashboard. See [Authentication](/docs/api-reference/authentication) for details.

## Optional first CRM job

This is the smallest useful account-scoped integration check: create a private property and contact, read and update them, verify the same IDs in the web CRM, then revoke the key. It does not publish a listing, upload images, send messages, or run a workflow.

1. Choose the server origin before deriving the API base URL. Use `https://rentalot.ai` for paid or Free Trial accounts. For optional local Free Trial testing, use a development server origin such as `http://localhost:3000`.

   ```bash
   export RENTALOT_API_ORIGIN=https://rentalot.ai
   # For a Free Trial, use instead:
   # export RENTALOT_API_ORIGIN=http://localhost:3000
   ```

   Keep this origin for web sign-in, Settings/key issuance, dashboard readback, and every API request.
2. Create an account, verify the email, and sign in at `$RENTALOT_API_ORIGIN`.
3. Open **Settings > API Keys** at `$RENTALOT_API_ORIGIN/dash/settings?tab=api-keys`, create a key, and save the raw value immediately. It is shown only once. For a local Free Trial, this is `http://localhost:3000/dash/settings?tab=api-keys`.
4. Derive the API base URL only after selecting the origin:

   ```bash
   export RENTALOT_API_KEY=ra_your_key
   export RENTALOT_API_BASE_URL="$RENTALOT_API_ORIGIN/api/v1"
   ```

   If you change `RENTALOT_API_ORIGIN`, derive `RENTALOT_API_BASE_URL` again before making requests. Do not mix a local trial key with the production origin, or vice versa.
5. Create a private property with the canonical required fields. Do not include images or the development-only `propertyType` field in the trial path:

   ```bash
   property_json=$(curl -sS "$RENTALOT_API_BASE_URL/properties" \
     -H "Authorization: Bearer $RENTALOT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "title": "Example Private Studio",
       "address": "123 Example Street",
       "city": "Austin",
       "state": "TX",
       "zip": "78701",
       "monthlyRent": 1800,
       "bedrooms": 0,
       "bathrooms": 1,
       "features": ["hardwood floors"],
       "isPublic": false
     }')
   PROPERTY_ID=$(printf '%s' "$property_json" | jq -r '.data.id')
   ```

6. Create a contact with an email or phone, then save its ID:

   ```bash
   contact_json=$(curl -sS "$RENTALOT_API_BASE_URL/contacts" \
     -H "Authorization: Bearer $RENTALOT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "Ada Lovelace",
       "email": "ada@example.com",
       "role": "prospect",
       "source": "api"
     }')
   CONTACT_ID=$(printf '%s' "$contact_json" | jq -r '.data.id')
   ```

7. Read both records and update only canonical safe fields:

   ```bash
   curl -sS "$RENTALOT_API_BASE_URL/properties/$PROPERTY_ID" \
     -H "Authorization: Bearer $RENTALOT_API_KEY"
   curl -sS "$RENTALOT_API_BASE_URL/contacts/$CONTACT_ID" \
     -H "Authorization: Bearer $RENTALOT_API_KEY"

   curl -sS -X PATCH "$RENTALOT_API_BASE_URL/properties/$PROPERTY_ID" \
     -H "Authorization: Bearer $RENTALOT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"title":"Example Private Studio Updated","monthlyRent":1900}'
   curl -sS -X PATCH "$RENTALOT_API_BASE_URL/contacts/$CONTACT_ID" \
     -H "Authorization: Bearer $RENTALOT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"channelPreference":"email"}'
   ```

8. While signed in to the same account at `$RENTALOT_API_ORIGIN`, open `$RENTALOT_API_ORIGIN/dash/properties/$PROPERTY_ID` and `$RENTALOT_API_ORIGIN/dash/contacts/$CONTACT_ID` and confirm the dashboard shows the same IDs and updated values. For a local Free Trial, these resolve to `http://localhost:3000/dash/properties/$PROPERTY_ID` and `http://localhost:3000/dash/contacts/$CONTACT_ID`.
9. Revoke the key at `$RENTALOT_API_ORIGIN/dash/settings?tab=api-keys`. A subsequent request with that key must return `401 Unauthorized`.

For an MCP-shaped version of this job, use the [MCP setup guide](/docs/mcp-server). The current CLI can read the same IDs, but its property and contact write flags are drifted from the canonical API fields, so do not use CLI writes for this first job. See the [CLI guide](/docs/cli).

## Endpoints

### Properties

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/properties` | List all properties |
| `POST` | `/properties` | Create a property |
| `POST` | `/properties/bulk` | Bulk create properties (async, returns jobId) |
| `GET` | `/properties/bulk/:jobId` | Poll a bulk-create job |
| `GET` | `/properties/:id` | Get a property |
| `PATCH` | `/properties/:id` | Update a property |
| `DELETE` | `/properties/:id` | Delete a property (soft-delete) |

### Property Images

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/properties/:id/images` | List images for a property |
| `POST` | `/properties/:id/images/presign` | Get a presigned upload URL |
| `POST` | `/properties/:id/images/confirm` | Confirm an uploaded image |
| `POST` | `/properties/:id/images/presign-batch` | Get multiple presigned URLs |
| `POST` | `/properties/:id/images/confirm-batch` | Confirm multiple uploads |
| `POST` | `/properties/:id/images/import` | Import images from URLs (async) |
| `GET` | `/properties/:id/images/import/:jobId` | Poll an import job |
| `DELETE` | `/properties/:id/images` | Delete images by ID (batch body) |
| `PATCH` | `/properties/:id/images/reorder` | Reorder images |

### Contacts

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/contacts` | List all contacts |
| `POST` | `/contacts` | Create a contact |
| `GET` | `/contacts/:id` | Get a contact |
| `PATCH` | `/contacts/:id` | Update a contact |
| `DELETE` | `/contacts/:id` | Delete a contact (soft-delete) |

Contacts are also created automatically when prospects interact with your workflows.

### Showings

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/showings` | List all showings |
| `POST` | `/showings` | Schedule a showing |
| `GET` | `/showings/:id` | Get a showing |
| `PATCH` | `/showings/:id` | Update a showing |
| `DELETE` | `/showings/:id` | Cancel a showing |
| `GET` | `/showings/availability` | Check available time slots |

### Conversations

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/conversations` | List all conversations |
| `GET` | `/conversations/:id/messages` | Get messages in a conversation |
| `GET` | `/conversations/search` | Search across message content |

Read-only access for CRM sync and reporting.

### Events

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/events` | List all calendar events |

Read-only view of your full calendar (showings, synced Google Calendar events, Cal.com events).

### Workflows

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/workflows` | List workflow templates |
| `GET` | `/workflows/:id` | Get a template |
| `GET` | `/workflows/runs` | List workflow runs |
| `POST` | `/workflows/runs` | Trigger a workflow run |
| `GET` | `/workflows/runs/:id` | Get a run |

### Messages

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/messages` | Send a message to a contact |

### Drafts

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/drafts` | List draft messages |
| `POST` | `/drafts` | Create a draft |
| `GET` | `/drafts/:id` | Get a draft |
| `PATCH` | `/drafts/:id` | Update a draft |
| `DELETE` | `/drafts/:id` | Delete a draft |
| `POST` | `/drafts/:id/send` | Send a draft |

### Follow-ups

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/followups` | List scheduled follow-ups |
| `POST` | `/followups` | Schedule a follow-up |
| `GET` | `/followups/:id` | Get a follow-up |
| `DELETE` | `/followups/:id` | Cancel a follow-up |

### Sessions (Pre-Screening)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/sessions` | List pre-screening sessions |
| `GET` | `/sessions/:id` | Get a session |
| `PATCH` | `/sessions/:id/review` | Approve or deny a session |

View and manage prospect submissions from public chat workflows.

### Webhooks

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/webhooks` | List webhook subscriptions |
| `POST` | `/webhooks` | Create a webhook |
| `GET` | `/webhooks/:id` | Get a webhook |
| `PATCH` | `/webhooks/:id` | Update a webhook |
| `DELETE` | `/webhooks/:id` | Delete a webhook |
| `POST` | `/webhooks/:id/test` | Send a test ping |
| `POST` | `/webhooks/:id/rotate-secret` | Rotate the signing secret |

### Settings

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/settings` | Get all agent settings |
| `PATCH` | `/settings` | Update agent settings |
| `GET` | `/settings/followups` | Get follow-up settings |
| `PATCH` | `/settings/followups` | Update follow-up settings |

## Idempotency

POST endpoints that support idempotency accept an optional `Idempotency-Key` header. The key is claimed atomically before execution, so concurrent first attempts using the same key cannot both execute. A completed response for the same key and request body is replayed within 24 hours with an `X-Idempotent-Replayed: true` header. A different request body returns `422`.

While a request has no completed response, repeats return `409` with `Retry-After: 1`. Retry the same key later, not a new key for the same action. A crash or failure to save the response can leave a pending claim until cleanup; reconcile the outcome before authorizing another action. The 24-hour claim is not an unlimited exactly-once guarantee.

Validation, ownership, plan-limit, and other known failures before the requested action release the pending claim, so the same key can be retried after the problem is resolved. Provider-send and queue-acceptance failures can be ambiguous: their returned error responses are retained and replayed instead of permitting a duplicate action. Unknown thrown outcomes remain pending.

Rate-limit accounting is charge-on-attempt. A failed request can consume quota, and replaying a cached response does not refund quota.

```
Idempotency-Key: your-unique-key-here
```

## Access by Plan

| Plan | API Access | Details |
|------|-----------|---------|
| **Free Trial** | Core CRUD | Private, image-free properties and contacts only while the account trial is active. |
| **Starter** | Read-only | `GET` on the supported API resources. Write operations are not included in Starter API authority. |
| **Pro** | Paid full authority | Read and write access to the paid API surface, including property/contact writes and webhooks, subject to endpoint limits. |
| **Scale** | Paid full authority plus priority limits | Pro write authority with higher limits and the Scale priority API feature. |

The API key determines the account and tier. Do not infer API write authority from a web CRM or management-chat control.

## Surface and tier boundaries

| Surface | Reads | Writes and current boundary |
|---------|-------|-----------------------------|
| **Web CRM** | Account-owned property/contact, conversation, calendar, workflow, and other dashboard reads | Account-owned dashboard mutations; this is the source of truth for same-record readback in the first job. |
| **Management chat** | Property/contact/conversation/calendar/workflow tools | Account-scoped writes with management limits. Destructive or externally consequential operations may return a confirmation preview. This interactive confirmation is not inherited by API-key clients. |
| **v1 API** | Account-owned resources through Bearer API keys | Starter is read-only. Pro/Scale have paid write authority. Free Trial writes are limited to private image-free properties and contacts while the account trial is active. |
| **Rentalot CLI** | `properties list/get` and `contacts list/get` are usable reads | Current property/contact write payloads drift from the API (`name`/`type`/`rent` versus canonical fields). Use MCP or direct API for the first write job until the sibling client sync lands. |
| **Rentalot MCP** | Registered list/get tools follow the v1 API | Canonical property/contact tool inputs are usable with an eligible key, but omit drifted title/status variants from the first job and include email or phone for contact creation. Other registered tools remain tier and endpoint restricted. |

Each endpoint page in this reference documents its request fields and responses. For client-specific coverage, see [MCP Server](/docs/mcp-server#tool-coverage) and [CLI](/docs/cli).

## Rate Limits

Paid-tier rate limits are per API key and vary by plan. Trial limits are durable and account-wide across all active keys:

| Plan | Global RPM | Daily Requests |
|------|-----------|----------------|
| Free Trial | 20/min per account | 200/day, 700/trial |
| Starter | 30/min | 5,000/day |
| Pro | 120/min | 50,000/day |
| Scale | 600/min | 500,000/day |

For the trial, properties and contacts each allow 10 reads/minute, 5 writes/minute, 20 writes/day, and 50 writes over the trial lifetime. Trial list pages are capped at 20 items. Trial property mutations are private and image-free, and do not trigger geocoding or outbound webhook/notification side effects. Image endpoints, image URL imports, bulk operations, messages, workflows, showings, webhooks, provider integrations, and AI/outbound actions are not part of the trial surface.

Write operations on paid tiers have additional per-resource daily and monthly caps. See your plan details for specifics.

Rate limit headers are included in every response:

```
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1707667200
X-RateLimit-Resource: properties
Retry-After: 3                    (429 responses only)
```

Daily trial total and resource-write exhaustion returns `Retry-After` seconds until the next UTC midnight. Trial lifetime exhaustion has no timed retry: upgrade to continue, and rotating keys does not reset it. A transient trial usage-store failure fails closed with a finite five-second retry interval.

## Pagination

All list endpoints return paginated results:

```json
{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 142,
    "totalPages": 8
  }
}
```

Page size is capped by your plan tier (Free Trial: 20, Starter: 50, Pro: 100, Scale: 200).

## Trial signup and account controls

Free Trial API access is a released, bounded entitlement, not a production signup-abuse control. Signup requires email verification. Production Better Auth configuration applies a 5/minute sign-up rule and a 100/minute general rule; development disables Better Auth's built-in limiter. The current account schema/control plane has no account-suspension authority and no cross-account identity/Sybil prevention guarantee.

## Errors

Errors follow [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457). Responses use `Content-Type: application/problem+json`:

```json
{
  "type": "https://rentalot.ai/problems/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Property not found"
}
```

Validation errors include a per-field `errors` array:

```json
{
  "type": "https://rentalot.ai/problems/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "Request validation failed",
  "errors": [
    { "field": "monthlyRent", "message": "Expected number, received string" }
  ]
}
```

| Status | `type` (suffix of `https://rentalot.ai/problems/`) | Meaning |
|--------|----------------------------------------------------|---------|
| 401 | `unauthorized` | Missing or invalid API key |
| 403 | `forbidden` | Plan doesn't allow this operation |
| 404 | `not-found` | Resource not found |
| 409 | `conflict` | Resource already exists |
| 410 | `gone` | Resource has expired |
| 422 | `validation-error` / `unprocessable-entity` | Invalid request body, parameters, or business-rule violation |
| 429 | `rate-limited` | Rate limit exceeded — check `Retry-After` header |
| 500 | `internal-error` | Server error — safe to retry with backoff |

## Developer Tools

### MCP Server

Connect an AI assistant with the [Rentalot MCP server](https://github.com/Rentalot-ai/rentalot-mcp). The package registers 65 tools, but current API tier restrictions and client contract drift still apply. The [MCP setup guide](/docs/mcp-server) includes the correct `RENTALOT_API_KEY` and `RENTALOT_BASE_URL` configuration, the private first CRM job, and current limitations.

The MCP client's `RENTALOT_BASE_URL` is the server origin, such as `https://rentalot.ai` or a local development origin. Do not append `/api/v1` to that variable.

```bash
claude mcp add rentalot \
  -e RENTALOT_API_KEY=ra_your_key \
  -e RENTALOT_BASE_URL=https://rentalot.ai \
  -- npx -y @rentalot/mcp-server
```

### CLI

Install the [Rentalot CLI](https://github.com/Rentalot-ai/rentalot-cli) for terminal reads and the current partial resource surface:

```bash
go install github.com/Rentalot-ai/rentalot-cli/cmd/rentalot@latest
rentalot config set api_key ra_your_key
rentalot config set base_url https://rentalot.ai
rentalot --help
```

`base_url` and `RENTALOT_BASE_URL` also use the server origin, not `/api/v1`. The [CLI guide](/docs/cli) documents why its current property/contact write flags are not a canonical write path.

## OpenAPI Spec

A machine-readable OpenAPI 3.0 spec is available at:

```
GET /api/v1/openapi.json
```

Use it to auto-generate client libraries in any language.
