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.
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 theleads: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, withPOST /leads/duplicate-check, sending name, phone and, optionally, board, as for POST /leads:
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=truereturns 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}/takegives you the request. If a colleague took it first, you get409 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, andmeta.merged_fromis 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 gets422 agent_not_member, and a deactivated colleague422 agent_deactivated.
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 gets403 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.
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 get409 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/readmarks 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.
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.
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.
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 getserased: 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.