# Inmobalia

> Connect Inmobalia to search your Inmobalia listings and move your contacts, history and sales into Fondaro with an import you can undo.

Connecting Inmobalia does two things. Your Inmobalia property catalogue appears in Fondaro's search beside your own listings, and you can move your Inmobalia CRM across: contacts become leads with their activity history and tasks, and sales become deals with their commission split. Fondaro only reads from Inmobalia and never writes back. Every import can be previewed first and undone afterwards.

## Set it up

1. In Inmobalia, open **Settings › Users ›** your user **› Connections › Personal access tokens** and create a token with the read scopes `contacts:read`, `activities:read`, `users:read` and `sales:read`. Add `events:read` and `enquiries:read` to bring calendar events and enquiries too.
2. In Fondaro, an admin pastes it in **Settings › Integrations › Inmobalia**, or saves it with `PUT /integrations/credentials/inmobalia/api_token` (`{ "value": "<token>" }`). The save checks the token and returns its `connection` state.
3. To move your CRM across, follow the steps below: scan, choose, map, preview, import contacts, then import sales.

Every `/inmobalia/import` route takes a signed-in organization admin session (`Authorization: Bearer <session token>`). A property API key is refused.

## Check the connection

### `GET /inmobalia/status`

Any member. Whether the saved token passed its latest live check, cached for 6 hours after a pass and 5 minutes after a failure.

```json
{ "connected": true, "connection": { "status": "connected", "checkedAt": "2026-10-02T10:15:00.000Z" } }
```

### `GET /inmobalia/import/status`

Admins. The token's expiry and the last import, with one live call to Inmobalia.

```json
{
  "connected": true,
  "expiresAt": "2026-12-31T00:00:00.000Z",
  "expired": false,
  "lastBatch": {
    "id": "9f1c2a3b-...",
    "status": "committed",
    "counts": { "contacts": 120, "leadsCreated": 118, "leadsSkipped": 2, "notes": 540, "tasks": 33, "tasksUndated": 4, "tasksSkipped": 0, "errors": 0 }
  }
}
```

## Search your Inmobalia listings

Use the `inmobalia` source of [Property sources](https://www.fondaro.com/docs/api/properties/sources.md) with an API key that has `sources:read`. Inmobalia has no location filter; it filters by status and country and sorts.

```bash
curl -X POST https://api.fondaro.com/property-sources/inmobalia/search \
  -H "X-API-Key: fondaro_pk_abc123" \
  -H "Content-Type: application/json" \
  -d '{"limit":20}'
```

`POST /inmobalia/mls/search` is the dashboard's deprecated route for the same search.

## 1. Scan Inmobalia

Inmobalia cannot list contacts by owner, so Fondaro first copies a snapshot of every contact (with its owners) and every sale (with its responsible user). Your choices are then made over that snapshot. A sandbox shares its parent organization's snapshot, which expires 14 days after the last completed scan.

### `POST /inmobalia/import/snapshot/scan`

| Field | Values |
|---|---|
| `mode` | `full` (default: everything, and records gone from Inmobalia leave the snapshot), `refresh` (only what changed since the last scan) or `resume` (continue a failed or stalled scan) |

Returns `202` with `{ "batchId": "...", "resumed": false }`. If a scan is already running, its id is returned instead. Refused with `400` when the token has expired.

### `GET /inmobalia/import/snapshot`

The latest scan and the snapshot's size.

```json
{
  "sandbox": false,
  "scan": {
    "id": "3b0e5c7d-...",
    "status": "committing",
    "mode": "full",
    "phase": "contacts_detail",
    "stalled": false,
    "counts": { "contactsListed": 4210, "contactsTotal": 4210, "contactsDetailed": 1830, "salesListed": 0, "detailErrors": 2 }
  },
  "snapshot": { "contacts": 4210, "contactsWithDetail": 1830, "sales": 0, "scannedAt": null, "expiresAt": null }
}
```

A scan moves through `contacts_list`, `contacts_detail`, `sales_list` and `done`. `stalled` is `true` when it has not moved for 5 minutes; resume it. Choose records once `snapshot.scannedAt` is set. `GET /inmobalia/import/snapshot/scans/:id` reads one scan.

## 2. Choose what to import

### `POST /inmobalia/import/snapshot/selection`

The body is a `selection`, the same object preview and commit take. Every field is optional; `{}` selects everything.

| Field | Meaning |
|---|---|
| `users` | Inmobalia usernames, plus `__unassigned__` (up to 500). A contact matches on any owner, a sale on its responsible user |
| `from`, `to` | Created between these instants (`from` inclusive, `to` exclusive) |
| `contactTypes` | `buyer`, `owner`, `tenantLong`, `tenantShort`, `collaborator`, `developer`, `lawyer`, `serviceco` (contacts only) |
| `includeArchived` | Include archived contacts (default `false`) |
| `sinceLastImport` | Only contacts created after their owner's last full import |
| `activities`, `tasks` | Bring activity logs as notes and tasks (default `true`) |
| `events`, `enquiries` | Bring calendar events and enquiries as notes (default `false`) |
| `files` | Bring contact and sale files as [Documents](https://www.fondaro.com/docs/api/documents.md) (PDF only, up to 25 MB; default `false`) |
| `fileVisibility` | `private` (admins and you, the default) or `organization` |
| `limit` | At most this many records (1 to 100,000) |

```json
{
  "users": [{ "username": "jdoe", "contacts": 812, "sales": 41, "lastImportedAt": "2026-09-10T08:00:00.000Z" }],
  "unassigned": { "contacts": 96, "sales": 3, "lastImportedAt": null },
  "selected": { "contacts": 50, "sales": 41, "contactsAlreadyImported": 12, "salesAlreadyImported": 0 },
  "contactsPendingDetail": 0
}
```

Per-user counts apply every filter except `users`, so they show what ticking each user would add.

## 3. Map Inmobalia to Fondaro

These reads give the values each mapping is keyed by.

| Route | Returns | Used by |
|---|---|---|
| `GET /inmobalia/import/user-mapping` | Inmobalia users, your members, and an `autoMatch` by email | `userMapping` |
| `GET /inmobalia/import/lead-statuses` | Your contact lead statuses | `statusMapping` (required) |
| `GET /inmobalia/import/sources` | Your contact sources | `sourceMapping` |
| `GET /inmobalia/import/contact-tags` | Your contact tags | `importContactTags` |
| `GET /inmobalia/import/activity-types` | Your activity types | `viewingActivityTypes` |
| `GET /inmobalia/import/sale-stages` | Your sale stages | `stageMapping` (required) |

```json
{
  "inmobaliaUsers": [{ "username": "jdoe", "email": "j@agency.com", "name": "Jane Doe" }],
  "fondaroMembers": [{ "userId": "user_123", "email": "j@agency.com", "name": "Jane Doe" }],
  "autoMatch": { "jdoe": "user_123" },
  "sandbox": false
}
```

## 4. Preview and import contacts

### `POST /inmobalia/import/preview`

Classifies the selected contacts without writing anything: `{ "selection": { ... } }`.

```json
{
  "total": 120,
  "toCreate": 110,
  "existing": 8,
  "invalidCount": 2,
  "unreachableCount": 1,
  "invalid": [{ "contactId": 42, "reasons": ["Missing phone number"] }],
  "typeBreakdown": { "buyer": 90, "owner": 30 },
  "assignmentSummary": { "inmobaliaUsers": 4, "autoMatched": 3, "unmatched": 1 }
}
```

### `POST /inmobalia/import/commit`

Starts the import in the background and returns `202` with `{ "batchId": "..." }`.

| Field | Rules |
|---|---|
| `selection` | As above |
| `userMapping` | Required. Inmobalia username to Fondaro user id, or `null` |
| `statusMapping` | Required. Each lead status plus `DEFAULT` to a Fondaro lead status |
| `missingFieldStrategy` | Required. `skip` contacts missing a required field, import them with a `placeholder`, or `skip_unreachable`: skip only those with neither an email nor a phone and import the rest with placeholders (`unreachableCount` in the preview counts them) |
| `contactOverrides` | Per contact id, a `firstName`, `email` or `phoneNumber` to use instead |
| `skipContactIds` | Contact ids to leave out |
| `sourceMapping` | Source name (or `__default__`) to `{ leadSource, tagName? }`; `leadSource` is `meta`, `google`, `client_web`, `manual`, `csv_import`, `zapier`, `james_edition`, `inmobalia` or `n8n` |
| `typeMapping` | Contact type to lead type (`buyer`, `seller`, `tenant`, `collaborator`); owners become sellers by default |
| `importContactTags` | Turn each contact's tags into Fondaro tags (default `false`) |
| `viewingActivityTypes` | Activity types that become [viewings](https://www.fondaro.com/docs/api/properties/activity.md) when they name exactly one of your listings |

```bash
curl -X POST https://api.fondaro.com/inmobalia/import/commit \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "selection": { "users": ["jdoe"], "from": "2026-01-01T00:00:00Z" },
    "userMapping": { "jdoe": "user_123" },
    "statusMapping": { "HOT": "client", "WARM": "potential", "DEFAULT": "lead" },
    "missingFieldStrategy": "skip"
  }'
```

Running it again skips contacts already imported, and adds the viewings, notes and files an earlier run did not bring. Each lead keeps the contact's creation date as its start, and an owner you did not map is named in a note on the lead.

## 5. Preview and import sales

Run this after contacts: each sale attaches to its buyer's (else seller's) imported lead.

### `POST /inmobalia/import/sales/preview`

`{ "status": "WON", "selection": { ... } }`, with `status` `IN_PROGRESS`, `WON` or `LOST`.

```json
{ "totalSales": 60, "toCreate": 52, "existing": 3, "skippedNoLead": 5, "unlinkedParties": 7, "byStage": { "CLOSED": 40, "OFFERING": 12 } }
```

### `POST /inmobalia/import/sales/commit`

Returns `202` with `{ "batchId": "..." }`.

| Field | Rules |
|---|---|
| `stageMapping` | Required. Each sale stage plus `DEFAULT` to `qualified`, `viewing`, `offer`, `reserved` or `under_contract` |
| `userMapping` | As for contacts |
| `status`, `selection` | As for the preview |

Each deal gets its buyer, seller, lawyers, collaborators and internal agents as participants with their commission shares, the listing when the sale's reference matches exactly one of yours, and the date its stage was reached. Running it again skips sales already imported.

## Follow and undo an import

### `GET /inmobalia/import/batches/:id`

`GET /inmobalia/import/batches` lists the 20 most recent. `status` moves from `committing` to `committed` (or `failed`), and from `rolling_back` to `rolled_back`.

```json
{
  "id": "a2d7c4e1-...",
  "kind": "sales",
  "status": "committed",
  "counts": { "totalSales": 60, "dealsCreated": 52, "dealsSkipped": 8, "participants": 190, "notes": 12, "errors": 0, "partiesDropped": 2, "listingsLinked": 31, "referrersSet": 9 },
  "committedAt": "2026-10-02T10:05:00.000Z",
  "rolledBackAt": null,
  "error": null,
  "droppedParties": [{ "saleId": 81, "role": "lawyer", "ref": "4410", "reason": "contact_not_imported" }]
}
```

`skippedObjects` names an object the token has no scope for (for example `{ "events": "insufficient_scope" }`), and `filesSkippedByReason` counts files that did not become documents (`not_pdf`, `too_large`, `no_url`, `download_failed`, `upload_failed`, `not_entitled`).

### `POST /inmobalia/import/batches/:id/rollback`

Deletes what the batch created, in one transaction: leads, notes, tasks and unused tags for a contacts batch; deals and their participants for a sales batch; and the viewings and documents either one added.

| Field | Meaning |
|---|---|
| `force` | Delete even if someone has worked on the imported records since |

```json
{ "status": "rolled_back", "deleted": { "leads": 118, "notes": 540, "tasks": 33, "deals": 0, "participants": 0, "tags": 4, "documents": 12, "viewings": 7 }, "forced": false }
```

Without `force`, an undo is refused with `409 inmobalia_rollback_touched` when the imported records were worked on since (calls, emails, notes, status changes, deals, edits), listing up to 200 of them:

```json
{
  "statusCode": 409,
  "code": "inmobalia_rollback_touched",
  "touchedTotal": 2,
  "touched": [{ "leadId": 5012, "name": "Ada Lovelace", "touches": ["call", "status_change"] }]
}
```

A contacts batch whose leads still carry a later sales batch's deals is refused with `409 inmobalia_rollback_dependent_batch`; undo the sales batch first. `force` overrides neither that nor a running import.

## Other tools

| Route | Purpose |
|---|---|
| `POST /inmobalia/import/assignees/update` | Add the people your current `userMapping` resolves to every imported contact and task. Runs in the background (`202`, `{ "batchId" }`); safe to repeat |
| `GET /inmobalia/import/assignees/runs/latest`, `/assignees/runs/:id` | Follow that run: `status` `running`, `done` or `failed` |
| `POST /inmobalia/import/lead-type-backfill` | Apply the saved type mapping to leads imported before lead types existed; only changes leads still at `buyer`. A dry run unless `{ "apply": true }` |
| `GET /inmobalia/import/sandbox-profile` | From the parent organization: the mappings of your sandbox's latest import, to reuse for the real run. People mapped to sandbox-only members are listed in `usersToMap` |

## Errors

| Status | Code | When |
|---|---|---|
| `400` | | The token has expired, a mapping is incomplete or invalid, or a batch cannot be undone (already undone, still running, or a scan) |
| `403` | | Not an admin |
| `422` | `inmobalia_insufficient_scope` | The token lacks a required scope; `requiredScopes` lists them |
| `422` | `inmobalia_unauthorized` | Inmobalia rejected the token |
| `409` | `inmobalia_commit_in_progress` | Another import of this organization is running or being undone |
| `409` | `inmobalia_crm_wipe_in_progress` | The organization is deleting all its CRM data |
| `409` | `inmobalia_rollback_touched`, `inmobalia_rollback_dependent_batch` | See above |
| `502` | `inmobalia_api_error` | Inmobalia failed to answer; try again |

Source: https://www.fondaro.com/docs/api/inmobalia
