> ## Documentation Index
> Fetch the complete documentation index at: https://api.housera.ro/llms.txt
> Use this file to discover all available pages before exploring further.

# Client requests

> Messages, call requests, and viewing requests from clients: read them with leads:read and work on them as in the portal with leads:write.

When a client sends a message about a listing, asks to be called, or asks for a viewing, the request lands in the Housera portal inbox. With the `leads:read` permission, you can read it through the API, with the client's contact details, and bring it into your CRM.

## Who has access

An API key sees the requests the account sees in the portal within the key's company, plus the account's requests on listings published outside any company:

* the agency owner and administrators see the whole company's requests;
* a branch manager sees their branch's requests (for a property developer, the requests for its projects);
* an agent sees the requests they are handling;
* an account without a company sees its own requests.

Requests from another company never appear, even for someone who works in both.

While the company is suspended, the key sees only the requests the account is handling, as in the portal. Colleagues' requests no longer appear, and neither do events about them; they come back after reactivation. When the company closes, the key keeps only the account's requests from outside the company.

For your own key, you tick `leads:read` yourself in Settings → API. An integrator gets it only if Housera has approved it, and the consent page tells the account that the integrator will receive clients' contact details. Without this permission, calls to `/leads` get `403 missing_ability`.

## List of requests

`GET /leads` returns requests from oldest to newest, with cursor pagination.

```bash theme={null}
curl "https://housera.ro/api/v1/leads?since=2026-11-01T00:00:00Z&per_page=50" \
  -H "Authorization: Bearer $HOUSERA_KEY" \
  -H "Accept: application/json"
```

| Parameter | What it does |
| - | - |
| `listing` | Only requests for the listing with this code. |
| `external_ref` | Only requests for the listing with this ID from your system. |
| `since` | Only requests received or changed after this point in time (ISO 8601): a new one, one the same client came back to, or one whose status changed in the portal. |
| `status` | Only requests at this stage: `new`, `contacted`, `viewing`, `offer` or `closed`. |
| `source` | Only requests that came in on this channel: `message`, `phone`, `viewing_request`, `referral` or `other`. |
| `agent` | Only the requests held by the colleague with this email. An email that belongs to nobody gets you an empty list. |
| `office` | Only the requests of the office with this id, the one in the request's `office.id`. |
| `complex` | Only the requests of a developer's project, by the development's code: those on the development and those on its units. |
| `q` | Searches like the portal's search bar: the name, the note, the start of the email and the phone. Write the phone any way you like: `0740 111 222` finds `+40740111222`. The owner, admins and managers also find requests by the name of the colleague who holds them. |
| `per_page` | How many requests per page, from 1 to 100 (default 50). |
| `cursor` | The cursor for the next page, taken from `meta.next_cursor` and sent exactly as you received it. Any other value gets `422 pagination_cursor`. |

If Housera cannot find the code or ID, you get an empty list. The filters combine with each other and with `inbox=true`, and a value outside their list (for example `status=unassigned`) gets `422`.

`since` is compared with `updated_at`, so a request you already imported can show up again. Use its `id` to tell that you already have it.

## One request

`GET /leads/{id}` returns a single request. For a request the key can't access, the API returns `404`, just as for one that does not exist.

```json theme={null}
{
  "data": {
    "id": 4182,
    "source": "message",
    "status": "new",
    "listing": { "code": "HEWPUTA8", "external_ref": "CRM-10293" },
    "complex": null,
    "contact": { "name": "Andrei Ionescu", "phone": "+40740123456", "email": "andrei@example.com" },
    "message": "Bună ziua, mai este disponibil apartamentul? Aș putea veni sâmbătă la vizionare.",
    "note": null,
    "price": null,
    "agent": { "name": "Maria Popescu", "email": "maria.popescu@agentia.ro", "display_name": "Maria Popescu" },
    "requests": { "callback_at": null, "viewing_at": "2026-11-02T08:14:03+00:00" },
    "office": { "id": 12, "name": "Tătărași" },
    "inbox": null,
    "created_at": "2026-11-02T08:14:03+00:00",
    "updated_at": "2026-11-02T08:14:03+00:00"
  }
}
```

| Field | What it contains |
| - | - |
| `source` | How the request came in: `message`, `phone` (call request), `viewing_request`, `referral`, or `other`. |
| `status` | Where the request stands in the portal: `new`, `contacted`, `viewing`, `offer`, or `closed`. |
| `listing` | The listing the request is about, with the Housera code and your ID (`external_ref`, `null` if you didn't set it). `null` for a request not tied to a listing. |
| `complex` | At a developer, the request's project, with `code`, `title` and `url`: the development the request came on, or the one the unit is part of. `null` outside a project. |
| `contact` | The client's name, phone, and email. |
| `message` | What the client wrote. `null` if they wrote nothing, for example with a call request. |
| `note` | The account's note on the request, as in the portal. For a request sent from the agency's page, the note starts with the client's text. |
| `price` | The amount the client offered, in whole euros. `null` when none is set. |
| `agent` | Who took the request in the portal: `name` is the account's name, `email` its email, and `display_name` the approved public name the portal shows next to the request. The two names differ when the colleague has a different public name approved. `email` is `null` when we have no real address for the colleague, for example one who came from a CRM with no email on their team record. `agent` is `null` while the request is waiting in the company's inbox. |
| `requests` | When the client last asked to be called (`callback_at`) and to come to a viewing (`viewing_at`), as in the portal. `null` if they never asked. `source` only gives the channel of the first request, so a client who wrote to you and then asked for a call has `source: message` and a `callback_at`. |
| `office` | The request's office, with `id` and `name`. The head office is named in the language of the response: “Head office” in English, “Sediu” in Romanian. `null` for a request outside a company. |
| `inbox` | Only for a request in “To take”: `reason` says why it waits, and `since` when the request came in. `null` while someone holds the request. |

The values of `inbox.reason` are the portal's:

* `no_listing`: the client wrote from the company's page, so the request has no listing;
* `left`: the agent who held the request or the listing left the company or was deactivated, and their work went to “To take”;
* `no_agent`: the listing has no active agent.

## Working on requests

With the `leads:write` permission you do through the API what you do with a request in the portal: change its stage, note and amount, delete it, add contacts by hand and take the requests nobody is responsible for yet. The rules are the portal's, and the change shows up there right away.

`leads:write` works only together with `leads:read`, because every response carries the client's contact details. On your own key you tick it in Settings → API. An integrator gets it only if Housera has approved it, and the account accepts it separately on the consent page. A key or connection that only has `leads:read` can't write: it gets `403 missing_ability`.

### Stage, note and contact details

`PATCH /leads/{id}` changes the fields you send and leaves the others as they are. A field sent as `null` is cleared, except the name and the stage, which can't be empty.

```bash theme={null}
curl -X PATCH "https://housera.ro/api/v1/leads/4182" \
  -H "Authorization: Bearer $HOUSERA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "viewing", "note": "Vine sâmbătă la 11.", "price": 91000}'
```

| Field | What it changes |
| - | - |
| `status` | The stage: `new`, `contacted`, `viewing`, `offer`, or `closed`. Any stage can follow any other. |
| `note` | The account's note, up to 500 characters. |
| `price` | The offered amount, in whole euros. |
| `name`, `phone`, `email` | The contact details, with the same checks as in the portal. The phone is stored in international format (`+40…`). |
| `source` | The channel: `message`, `phone`, `viewing_request`, `referral`, or `other`. |

The response holds the request with the new values. If another contact of the same client on the same listing already has the phone or email you sent, you get `422` with the `lead_duplicate` code on `/phone`.

`DELETE /leads/{id}` deletes the request, as in the portal. The response is `204`, and the request disappears from the lists and from `GET /leads/{id}`.

A request the key doesn't see, a colleague's or another company's, gets `404`, like one that doesn't exist.

### A contact added by hand

`POST /leads` adds a contact, like the “Add” button in the portal, with you as the person responsible. Only `name` is required, and without `status` the contact starts at the `new` stage. Send a different `Idempotency-Key` for each new contact and the same one when you retry a request: within 10 minutes, the same value answers with the contact added the first time, with `200` and the same `meta.possible_duplicate`, and adds no other.

The owner, admins and managers add the contact to the company's requests. If you want it to stay only in your own requests, send `"board": "personal"`. For an agent, the contact always goes to their own requests, and `"board": "firm"` gets `403`.

#### Is the contact already there?

Before saving, the portal checks whether you already have the contact: it looks in the same requests (the company's or yours) for the latest contact with the same phone or exactly the same name. Through the API you run the same check, among the requests the key sees, with `POST /leads/duplicate-check`, sending `name`, `phone` and, optionally, `board`, as for `POST /leads`:

```bash theme={null}
curl -X POST "https://housera.ro/api/v1/leads/duplicate-check" \
  -H "Authorization: Bearer $HOUSERA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Andrei Ionescu", "phone": "0740 123 456"}'
```

If it finds the contact, you get the request in `data`, as from `GET /leads/{id}`, and in `meta.matched_on` what matched: `phone`, `name` or both. If not, you get `"data": null` and an empty `meta.matched_on`. The check changes nothing and needs the same permissions as adding a contact, `leads:read` and `leads:write`.

Two people can share a phone, for example an agency's line, so the check only warns you. You decide whether to add the contact. `POST /leads` adds it anyway and tells you in `meta.possible_duplicate` the contact the same check finds, with `id` and `matched_on`, or `null`.

### Requests nobody is responsible for

When a company listing has no active agent, or the client writes from the agency's page, the request goes to “To take” and anyone in the branch can take it. Through the API:

* `GET /leads?inbox=true` returns the requests in “To take” that the account can take. An agent sees only their branch's, while the owner and admins see all of them. `GET /leads/{id}` reads them too.
* `POST /leads/{id}/take` gives you the request. If a colleague took it first, you get `409 lead_already_taken`. If you already have a contact with the same client, without a listing, the request joins it, as in the portal: the response is your contact, and `meta.merged_from` is the id of the joined request, which no longer exists.
* `POST /leads/{id}/assign`, with `{"agent_email": "colleague@agency.ro"}`, hands the request to a colleague on your team. It works for the owner, admins and managers, and the colleague gets the portal's notification. An email that isn't a colleague's on the team gets `422 agent_not_member`, and a deactivated colleague `422 agent_deactivated`.

Once a request is taken, it shows up in the responsible person's `GET /leads`, with `agent` filled in.

## The conversation with the client

When the client writes to you about a listing, the request has a conversation, the same one you see in Messages in the portal. Through the API you read it and reply to the client, as from the portal.

Only the account in charge of the listing reads the conversation and replies to the client, as in the portal. An owner or manager who sees an agent's request gets `403 conversation_not_held` on its conversation. A request with no conversation (a call request, a contact added by hand, a message from the agency's page) returns an empty list.

### Messages

`GET /leads/{id}/messages` returns the messages oldest first, with cursor pagination. With `after` you get only the messages after one you already have. Reading through the API doesn't mark messages as read, so it doesn't hide them from whoever works in the portal.

```json theme={null}
{
  "data": [
    {
      "id": 90211,
      "direction": "in",
      "author": { "name": "Andrei Ionescu" },
      "body": "Bună ziua, mai este disponibil apartamentul?",
      "kind": null,
      "deleted": false,
      "edited_at": null,
      "read_at": "2026-11-02T08:20:11+00:00",
      "created_at": "2026-11-02T08:14:03+00:00"
    }
  ]
}
```

| Field | What it contains |
| - | - |
| `direction` | `in` for a message from the client, `out` for one sent from the listing's side, by you or by a colleague who handled the listing before. |
| `author` | The author's name, as the portal's conversation shows it. |
| `body` | The text. `null` for a deleted message, which keeps its place in the conversation. |
| `kind` | `viewing_request` for the message with which the client asked for a viewing, otherwise `null`. |
| `read_at` | When the other side read the message. `null` until they read it. |

### Replying

`POST /leads/{id}/messages` sends a message to the client, as from the portal. The client sees it in the conversation and gets the same notifications as for a message written in the portal.

```bash theme={null}
curl -X POST "https://housera.ro/api/v1/leads/4182/messages" \
  -H "Authorization: Bearer $HOUSERA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2b0e-8a4d-4c3e-9b7a-2d5f1e0c9a11" \
  -d '{"body": "Bună ziua! Da, e disponibil. Vă convine sâmbătă la 11?"}'
```

The response is `201`, with the message. Send a different `Idempotency-Key` for each new message and the same one when you retry a message: within 10 minutes, a request with the same value returns the message sent the first time, with `200`, and sends nothing else. The text has at most 2,000 characters.

The reply is refused with `409` when the request has no conversation (`no_conversation`), when the conversation is read only because the listing or the client's account was deleted (`conversation_closed`), and when one side blocked the other (`conversation_blocked`). Replies through the API and from the portal share the same limit, 20 per minute and 200 per hour per account.

### Editing, deleting and reading

* `PATCH /leads/{id}/messages/{message}`, with `{"body": "…"}`, changes the text of a message you wrote, within the first 15 minutes and while the conversation is open. After 15 minutes you get `409 edit_window_closed`. The client sees the message marked as edited.
* `DELETE /leads/{id}/messages/{message}` deletes a message you wrote, at any time. The message stays in the conversation as “Message deleted”, with no text.
* A client's message can't be edited or deleted: you get `403 message_not_yours`.
* `POST /leads/{id}/messages/read` marks the client's messages as read, as opening the conversation in the portal does. The client isn't told.

### The `lead.message_received` event

When the client writes again in the conversation, the account in charge of the listing gets the `lead.message_received` event, on its webhook and in `GET /events`, if it has a key with `leads:read`. The event has `lead_id` and `message_id`, without the message's text, which you read with `GET /leads/{id}/messages?after=…`. The client's first message, the one that opens the conversation, comes as `lead.created`, or as `lead.updated` if the client already had a request on the listing, so it doesn't also produce `lead.message_received`.

> [!WARNING]
>
> Clients' messages are personal data, like their contact details. Keep them only as long as you need them to respond to the request.

## The `lead.created` event

When a client sends their first request about a listing, you get a `lead.created` event on your webhook and in `GET /events`. The event does not include the client's data. It has `lead_id` and `source`, and you read the details with `GET /leads/{id}`. In `GET /events` you see it with any key that has `leads:read`, even without `listings:read`.

Only accounts that see the request in their inbox and have a key with `leads:read` get the event. From the day we announce to you, a request that lands in “To take” also produces `lead.created` for whoever may take it, that is, for everyone in the office the portal tells, agents included. A webhook gets it only if it has `leads:read` behind it. See [Webhooks](https://api.housera.ro/en/webhooks.md) for details.

## The `lead.assigned` and `lead.removed` events

From the day we announce to you, you also learn what happens to a request after it arrives.

`lead.assigned` comes when the request has a new holder. `by` says how: `take` (someone took it from “To take”), `assign` (the owner, an admin or a manager gave it to a colleague) or `handover` (it went to a colleague along with the work of someone who left the company or was deactivated, or with a listing moved to another agent). `agent` is the new holder and `previous_agent` the one before, each with `name` and `email`; `previous_agent` is `null` when the request was waiting in “To take” or when the previous colleague's account was deleted. Whoever sees the request now gets it.

`lead.removed` comes when the request is no longer yours, with `reason`:

* `deleted`: the request was deleted, in the portal or through the API;
* `merged`: the request was added to the same client's request that the colleague who took it already had;
* `out_of_reach`: the request is no longer within your reach, for example a colleague took it from “To take” or you handed it on.

Whoever got the request's events until then gets it. After `lead.removed`, `GET /leads/{id}` answers `404`, but the event stays in `GET /events`. Delete the request from your system, or mark it as out of your reach.

Both events have `lead_id`, like `lead.created`, and need the `leads:read` permission. Any webhook that gets `lead.created` also gets them, without ticking them.

## The `lead.updated` event

When the same client comes back to the same listing with a new request, for example asking you to call after they sent a message, the new request is added to the one you already have. No separate request appears, but you get a `lead.updated` event with the same `lead_id`. The event's `source` is the channel of the new request (`phone` here), while `source` in `GET /leads/{id}` stays the channel of the first one. The event's `created_at` is when the new request came in. The request's `updated_at` moves to that moment too, so `GET /leads?since=` returns it again. You also get `lead.updated` when the client writes again from the company's page after a colleague took their request, and when a request from "To take" joins, on taking, the contact you already have with the same client.

`lead.updated` goes to the same accounts and webhooks as `lead.created` and also needs `leads:read`. On a webhook you set up in Settings → API, you tick it separately.

> [!WARNING]
>
> Clients' contact details are personal data. Keep them only as long as you need them to respond to the request, and don't use them for anything else.

## Erasing client data

Housera erases a client's data in two cases, and you must erase it from your system too.

When a client deletes their account, their requests stay in the portal without their data: the name is anonymized, and the phone, email, message and note are cleared. The request gets `erased: true`, and `updated_at` becomes the time of the deletion, so `GET /leads?since=` returns it again. You also receive the `lead.erased` event, with `reason: account_deleted`, even for a request you already deleted in the portal.

A request with no activity for 24 months, or one deleted in the portal more than 30 days ago, is deleted for good. Just before that, you receive `lead.erased` with `reason: retention`. After that, `GET /leads/{id}` returns `404`, and the event stays in `GET /events` for 35 days.

`lead.erased` goes to the same accounts as `lead.created` and also needs `leads:read`. Every webhook that receives `lead.created` receives it too; it does not appear in the webhook's event list and you do not tick it.

> [!WARNING]
>
> When you receive `lead.erased` or see a request with `erased: true`, delete the client's name, phone, email, message and note for that request from your system. You received the data from Housera, so its erasure on our side applies to you as well (GDPR, Article 17(2) and Article 19).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.