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

# For CRMs

> The recommended flow for a robust synchronization between your CRM and Housera.

This page is for those who automatically synchronize an entire inventory: real estate CRMs, agency ERPs, scripts that publish from a database. The template below keeps Housera up to date with your system, withstands network failures, and never overwrites the work of agents done directly in the portal.

## The recommended flow

<Steps>
  <Step title="You publish and update by your id">
    For each property in the CRM, you send `PUT /listings/ref/{id-ul tău}` with all the fields you know. There's no need to remember if the listing already exists: the first time it gets created (`201`), then it's updated (`200` or `202`).

    ```bash theme={null}
    curl -X PUT https://housera.ro/api/v1/listings/ref/CRM-10293 \
      -H "Authorization: Bearer $HOUSERA_KEY" \
      -H "Accept: application/json" \
      -H "Content-Type: application/json" \
      -H 'If-Match: "9b2f6c1e4a7d3b8f0c5e2a1d6f4b7c9e"' \
      -d @CRM-10293.json
    ```

    Keep in the CRM, next to each property, `code` and `etag` from the last response. When creating a new listing, send without `If-Match`.
  </Step>

  <Step title="You send the pictures">
    Once the listing exists, `PUT /listings/{code}/photos` with the complete list, in order. Resend the list only when something has changed with the pictures in the CRM: the same URLs don't get downloaded again, but each list sent for a published listing goes into moderation.
  </Step>

  <Step title="You listen to the webhooks">
    You configure a webhook for all events. You update in the CRM the status of the listing to `listing.published`, `listing.rejected`, `listing.paused`, `listing.reactivated`, `listing.sold`, and `listing.withdrawn`, and at `listing.edited_in_portal` you apply your conflict rule (below).
  </Step>

  <Step title="You recover what was missed">
    When the service starts and every few minutes, you run `GET /events?after=<ultimul id procesat>`. A lost webhook while your server was down is no longer a lost change.
  </Step>

  <Step title="You reconcile daily">
    Once a day you go through `GET /account/listings?fields=code,external_ref,etag,updated_at,status` and compare with the CRM: `etag` different means a change on Housera, a missing id means a listing to resend. The details are in [Identifiers](/en/concepts/identifiers#reconcilierea).
  </Step>
</Steps>

## Edits made in the portal

Listings published via API remain editable on housera.ro: an agent can correct the price or description directly from their account. When this happens:

1. You receive the webhook `listing.edited_in_portal`, with the new `etag`.
2. The `etag` you have in the CRM is no longer the current one.
3. Your next `PUT` or `PATCH` with old `If-Match` receives `412 listing_changed_meanwhile` and does not write anything.

What you do at `412` is your decision. Common options:

<Tabs>
  <Tab title="The portal wins">
    You read the listing with `GET /account/listings/{code}`, update the CRM with the values from Housera and save the new `etag`. Appropriate when agents work directly in the portal.
  </Tab>

  <Tab title="The CRM wins">
    You read the listing to get the new `etag` and resend your body with it in `If-Match`. Appropriate when the CRM is the only source of truth. Do it consciously: the agent's correction in the portal is lost.
  </Tab>

  <Tab title="Decide on a person">
    You mark the property in the CRM as "in conflict" and let the agent choose. The safest option for sensitive fields, like price.
  </Tab>
</Tabs>

<Warning>
  Without `If-Match`, `PUT` and `PATCH` apply across any version. For automatic synchronization, always send `If-Match` on existing listings.
</Warning>

## Fields you don’t know

`PUT /listings/ref/{ref}` on an existing listing acts like a patch: fields you do not send **retain their value**. Send only the fields that your CRM actually manages. If an agent fills in a field in the portal that the CRM doesn’t have, your synchronization does not delete it.

To explicitly clear a field, you send it with `null`.

## Resubmissions without changes

An identical body to the last successful write does not write anything and does not go to moderation: you receive `200` with the header `X-Housera-Unchanged: true`. You can thus resend the entire inventory at every synchronization. To take advantage of this, serialize the body the same way every time (the same field order, the same number format).

## Robustness

### Retries

| Response | Retry? | How |
| - | - | - |
| `409 listing_busy` | Yes | After 1–2 seconds, a few times. |
| `429` | Yes | After how many seconds `Retry-After` says. |
| `5xx`, timeout, connection interrupted | Yes | With increasingly larger pauses (1 s, 2 s, 4 s, 8 s…), plus a small random variation, up to a ceiling. |
| `412 listing_changed_meanwhile` | Do not blindly | You apply the conflict rule above. |
| `409 listing_action_refused` | No | You read `detail`: the reason does not disappear on a new attempt. |
| `422` | No | You fix the data. The codes are in [Errors](/en/errors). |
| `401`, `403` | No | You check the key and its rights. |
| `404` | No | The listing does not exist, is not yours, or the action is not possible from its state. |

### Idempotence

* `PUT /listings/ref/{ref}` is safe to repeat: the same id never creates two listings.
* For `POST /listings`, always send `Idempotency-Key` and reuse the same value on retries of the same operation.
* `PATCH` and `PUT /listings/{code}/photos` yield the same result if repeated. With `If-Match`, a repeat after a successful write receives `412`, because the version changed right through the first write: you read the listing and check if it already has your values.
* A transition (`/pause`, `/resume`, `/sold`) repeated after it succeeded receives `404`, because the listing is no longer in the departing state. Treat this as success if the current state is the desired one.

### Rate

The limit is 300 requests per minute for each key. Monitor `X-RateLimit-Remaining` and throttle when it approaches zero, instead of waiting `429`. At this limit, 3,000 listings are resent in about 10 minutes, including unchanged ones. See [Limits](/en/limits).

### Validation before launch

Before the first real synchronization, pass the entire inventory through `POST /listings/validate` (in sandbox or in production, it does not write anything) and clean both `errors[]` and `warnings[]`. The error codes are stable, so you can turn them into clear messages for agents filling in data in the CRM.

## Checklist

<AccordionGroup>
  <Accordion title="Before the launch">
    * Sandbox key with `listings:read`, `listings:write`, `media:write`.
    * The entire inventory passes `POST /listings/validate`, without `warnings[]`.
    * Webhook configured, signature verified, with a 5-minute tolerance.
    * You treat `412`, `409`, `429`, and `5xx` as above.
    * `GET /events` run periodically, with the last id saved.
  </Accordion>

  <Accordion title="At the transition to production">
    * New `hsr_live_` key from the account on housera.ro.
    * The webhook created again in production, with its secret.
    * The base address changed to `https://housera.ro/api/v1`.
    * A reminder in the calendar before the key expires.
  </Accordion>
</AccordionGroup>


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