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

# Connect with Housera

> How a CRM publishes for its agencies without any agency having to hand it a key.

If you publish for multiple agencies, do not ask them to create a key and send it to you. Integrate the **Connect with Housera** button: the agent logs in on housera.ro, sees what your application requests and accepts, and you receive his account key directly, server to server. The agency sees your connection in their account and can disconnect it at any time.

The flow is OAuth 2.0 with authorization code and PKCE ([RFC 6749](https://www.rfc-editor.org/rfc/rfc6749), [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636)), so any OAuth library can handle it. The steps below show it without a library.

## First of all: approval

The integrator access is granted by Housera. Write to us at **[office@housera.ro](mailto:office@housera.ro)** with:

* the name of the application, as you want the agencies to see it;
* the website;
* **return addresses** (`redirect_uri`), exactly as you will send them. We compare character by character, with no wildcard. We only accept `https`, plus `http://localhost` for development;
* rights you need (`listings:read`, `listings:write`, `media:write`);
* optionally, a `https` webhook address for events of connected accounts.

You receive `client_id` (`hsrc_…`), `client_secret` (`hsrs_…`) and, if you requested a webhook, its secret (`whsec_…`). You receive them only once; keep them only on the server. For the sandbox, you get separate data, on `staging.housera.ro`.

<Warning>
  `client_secret` never reaches the browser, a mobile application, or a code repository. Anyone who has it can change codes on your behalf.
</Warning>

## The Flow

<Steps>
  <Step title="You are preparing the request">
    For each connection you generate two random values and keep them in the agent's session, on your server:

    * `state`: protects against forged requests; you check it on return;
    * `code_verifier`: 43–128 characters from `A-Z a-z 0-9 - . _ ~`. From it you calculate `code_challenge = BASE64URL(SHA256(code_verifier))`, without `=` at the end.

    <CodeGroup>
      ```php PHP theme={null}
      $verifier = rtrim(strtr(base64_encode(random_bytes(48)), '+/', '-_'), '=');
      $challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
      $state = bin2hex(random_bytes(16));
      ```

      ```javascript Node.js theme={null}
      import { createHash, randomBytes } from 'node:crypto';

      const verifier = randomBytes(48).toString('base64url');
      const challenge = createHash('sha256').update(verifier).digest('base64url');
      const state = randomBytes(16).toString('hex');
      ```

      ```python Python theme={null}
      import base64, hashlib, secrets

      verifier = secrets.token_urlsafe(48)
      challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=').decode()
      state = secrets.token_hex(16)
      ```
    </CodeGroup>
  </Step>

  <Step title="You send the agent to the consent page">
    You redirect the agent's browser to:

    ```text theme={null}
    https://housera.ro/setari/api/conectare
      ?response_type=code
      &client_id=hsrc_…
      &redirect_uri=https%3A%2F%2Fcrm.exemplu.ro%2Fhousera%2Fcallback
      &scope=listings%3Aread%20listings%3Awrite%20media%3Awrite
      &state=…
      &code_challenge=…
      &code_challenge_method=S256
    ```

    In the sandbox, the address is `https://staging.housera.ro/setari/api/conectare`. Staging is not open to the public: for your tests, we provide, upon request, a test account with access to it. The English version, `/en/settings/api/connect`, is equivalent.

    `scope` is optional: without it you request all rights that Housera has approved for you. You cannot request more than that, and the platform catalog is never granted through connection.
  </Step>

  <Step title="The agent accepts or declines">
    If the agent is not authenticated, housera.ro first asks him to log in. Then he sees the name of your application, the account or agency for which access is granted, the rights, and the domain to which he returns.

    The browser goes back to your return address:

    * to consent: `https://crm.exemplu.ro/housera/callback?code=…&state=…`;
    * to decline: `…?error=access_denied&state=…`.

    You check that `state` is the one from the session. If it is not, you stop the flow.
  </Step>

  <Step title="You change the code on the key, from the server">
    The code expires in **60 seconds** and is used only once. You send it immediately to `POST /oauth/token`:

    ```bash theme={null}
    curl https://housera.ro/api/v1/oauth/token \
      -d grant_type=authorization_code \
      -d code="$CODE" \
      -d redirect_uri=https://crm.exemplu.ro/housera/callback \
      -d code_verifier="$VERIFIER" \
      -d client_id="$HOUSERA_CLIENT_ID" \
      -d client_secret="$HOUSERA_CLIENT_SECRET"
    ```

    ```json theme={null}
    {
      "access_token": "1234|hsr_live_…",
      "token_type": "Bearer",
      "expires_in": 31536000,
      "scope": "listings:read listings:write media:write",
      "connection_id": 1234
    }
    ```

    You can also send `client_id` and `client_secret` via HTTP Basic instead of the body.
  </Step>

  <Step title="You work with the key, like any key">
    You keep `access_token` and `connection_id` next to the agency in your CRM and use the key just like in the rest of the documentation: `Authorization: Bearer …`. The same rules, the same limits, the same moderation.
  </Step>
</Steps>

## What happens with listings

* The listings you publish are **the agency's**. Agents can also edit them from the portal; then you receive `listing.edited_in_portal` and the new `etag`.
* Your ids (`PUT /listings/ref/{ref}`) are only yours: two CRMs connected to the same agency do not interfere, even if they use the same ids.
* In the requests log, the agency sees every request made through your connection, with the name of the application.

## Events

If you declared a webhook for approval, you receive on it the events of **all** connected accounts, signed with the webhook secret (see [Webhooks](/en/webhooks)). Each delivery additionally includes the `connection` field, equal to `connection_id` from the response of the code exchange:

```json theme={null}
{
  "id": "01JZ8K3V7Q2W4E6R8T0Y1U3I5O",
  "type": "listing.published",
  "created_at": "2026-10-19T09:41:00+00:00",
  "data": { "code": "HEWPUTA8" },
  "connection": 1234
}
```

Without a webhook, you retrieve events of each account with `GET /events?after=…`, using its key.

## Expiration and disconnection

* The key expires after **365 days** (`expires_in`). Before the deadline, send the agent through the consent page again: the new key replaces the old one.
* A new connection of the same account gives you a new key and invalidates the old one.
* The agency can disconnect you from **Settings → API → Connected Applications**. From that moment on, the key receives `401 unauthenticated`, and the webhook no longer receives the events of the account. Treat `401` as a disconnection and suggest to the agent to reconnect.
* If Housera disables your access, all your keys receive `401` until reactivation.

## Errors

On the consent page, as long as the application or return address are not confirmed, the error remains on housera.ro and does not reach you:

| Situation | What the agent sees |
| - | - |
| `client_id` unknown or application disabled | “The application is not approved by Housera or has been disabled.” |
| `redirect_uri` undeclared | “The address to which the application wants to return you is not declared at Housera.” |

Once confirmed, the errors return to your return address, with `error` and `state`:

| `error` | Cause |
| - | - |
| `access_denied` | The agent declined. |
| `invalid_request` | Missing `state`, `code_challenge` is not a SHA-256 in base64url or `code_challenge_method` is not `S256`. |
| `invalid_scope` | You requested a right that we did not approve for you. |
| `unsupported_response_type` | `response_type` is not `code`. |

At `POST /oauth/token`, the errors take the form of RFC 6749, `{"error": "…", "error_description": "…"}`:

| Status | `error` | Cause |
| - | - | - |
| `401` | `invalid_client` | `client_id` or `client_secret` incorrect, or application disabled. |
| `400` | `invalid_grant` | Unknown, expired or already used code, other `redirect_uri` than in the request, `code_verifier` incorrect. A code used a second time revokes also the key it gave. |
| `400` | `unsupported_grant_type` | `grant_type` is not `authorization_code`. |


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