Skip to main content
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.
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.
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.
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:
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.

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.
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.
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 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.
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.
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).