# Integration reporting

> Read a lead's calls and activity, viewings and meaning-based lead search, and receive signed webhooks, with a scoped integration key.

The integrations API (`/integrations/v1`) lets your own automation read and write your organization's CRM: leads, deals, tasks, notes and tags, call and activity history, viewings, and webhooks for what changes. It is the API behind the n8n node, and any HTTP client can call it. For access as one person, use the [REST API](https://www.fondaro.com/docs/api/overview.md) with an API key; this API stays for agency-wide automation and webhooks.

## Authenticate

Send an integration key as a Bearer token:

```bash
curl https://api.fondaro.com/integrations/v1/whoami \
  -H "Authorization: Bearer $FONDARO_INTEGRATION_KEY"
```

```json
{
  "organizationId": "5b1f0c1e-8a54-4f1d-9d7e-3f2a1c9b7e21",
  "organizationName": "Costa Homes",
  "keyId": "0c9e1d2a-7b3f-4e8a-9d61-2b7a1c9e4d10",
  "keyName": "n8n production",
  "scopes": ["leads:read", "leads:write"]
}
```

An integration key starts with `fdr_n8n_`. An organization admin creates it in **Settings > Integrations > n8n**; the full key is shown once. It acts for the whole organization within its scopes, and works until it is revoked or expires. A disabled organization's keys stop working. Property API keys (`fondaro_pk_`) and MCP credentials do not open these routes, and an integration key opens nothing else. See [Authentication](https://www.fondaro.com/docs/api/authentication.md).

### Scopes

| Scope | Allows |
|-------|--------|
| `leads:read` | Read, find, search and list leads, their calls and activities, the option lists, and meaning-based lead search |
| `leads:write` | Create leads, change contact details and status, add assignees |
| `deals:read` | Read deals and a lead's deals |
| `deals:write` | Create deals, change stage, mark won or lost |
| `tasks:write` | Create a task on a lead |
| `notes:write` | Create a note on a lead |
| `tags:read` | Read a lead's tags and a tag |
| `tags:write` | Add tags to a lead |
| `triggers:subscribe` | Manage webhook subscriptions and poll for new leads |
| `users:read` | Resolve a team member's id to their name, email and role |
| `viewings:read` | Read viewings. Only granted when chosen explicitly |

A key created without a scope list gets every scope except `viewings:read`. A request without the scope its route needs returns `403`.

## Endpoints

| Method and path | Scope | Purpose |
|-----------------|-------|---------|
| `GET /integrations/v1/whoami` | none | The key's organization, name and scopes |
| `POST /integrations/v1/leads` | `leads:write` | Create a lead |
| `GET /integrations/v1/leads` | `leads:read` | List leads |
| `GET /integrations/v1/leads/find` | `leads:read` | Find one lead by `email`, `phone` or `externalId` |
| `GET /integrations/v1/leads/search` | `leads:read` | Search leads by text (`q`, `limit`, `offset`) |
| `GET /integrations/v1/leads/:id` | `leads:read` | Read a lead |
| `GET /integrations/v1/leads/:id/calls` | `leads:read` | A lead's calls |
| `GET /integrations/v1/leads/:id/activities` | `leads:read` | A lead's activity history |
| `PATCH /integrations/v1/leads/:id/contact` | `leads:write` | Change contact details and `companyName` |
| `PATCH /integrations/v1/leads/:id/status` | `leads:write` | Change the lead status |
| `POST /integrations/v1/leads/:id/assignees` | `leads:write` | Add assignees, people or teams |
| `POST /integrations/v1/leads/semantic-search` | `leads:read` | Find leads by what they said |
| `POST /integrations/v1/leads/listing-match` | `leads:read` | Find leads that fit a listing |
| `POST /integrations/v1/deals` | `deals:write` | Create a deal |
| `GET /integrations/v1/deals/:id` | `deals:read` | Read a deal |
| `PATCH /integrations/v1/deals/:id/stage` | `deals:write` | Move a deal to another stage |
| `POST /integrations/v1/deals/:id/won` | `deals:write` | Mark a deal won |
| `POST /integrations/v1/deals/:id/lost` | `deals:write` | Mark a deal lost |
| `GET /integrations/v1/leads/:leadId/deals` | `deals:read` | A lead's deals |
| `POST /integrations/v1/leads/:leadId/tasks` | `tasks:write` | Create a task |
| `POST /integrations/v1/leads/:leadId/notes` | `notes:write` | Create a note |
| `GET /integrations/v1/leads/:leadId/tags` | `tags:read` | A lead's tags |
| `POST /integrations/v1/leads/:leadId/tags` | `tags:write` | Add tags to a lead |
| `GET /integrations/v1/tags/:tagId` | `tags:read` | Read a tag |
| `GET /integrations/v1/options/{tags,assignees,deal-stages,lead-statuses,lead-types,lead-sources}` | `leads:read` | Values for pickers |
| `GET /integrations/v1/users`, `GET /integrations/v1/users/:id` | `users:read` | Team members |
| `GET /integrations/v1/viewings`, `GET /integrations/v1/viewings/:id` | `viewings:read` | Viewings |
| `GET /integrations/v1/triggers/leads/new` | `triggers:subscribe` | Poll for new leads |
| `POST /integrations/v1/subscriptions` | `triggers:subscribe` | Subscribe a webhook |
| `GET /integrations/v1/subscriptions/:id` | `triggers:subscribe` | Read a subscription |
| `DELETE /integrations/v1/subscriptions/:id` | `triggers:subscribe` | Remove a subscription |

Every lead payload carries `companyName` (string or null). `POST /integrations/v1/leads` accepts it; `PATCH /integrations/v1/leads/:id/contact` sets it, or clears it with `null` or an empty string (at most 255 characters).

The lead, deal, task and assignee writes accept an optional `teamIds` list (up to 20 team ids) next to their user ids. Each team becomes its current members when the request runs. One request assigns at most 50 people; past that it returns `400` with `code: "ASSIGNEE_LIMIT_EXCEEDED"` and writes nothing. See [Teams](https://www.fondaro.com/docs/api/teams.md).

## List a lead's calls

`GET /integrations/v1/leads/:id/calls`

Returns the lead's calls, newest first, without recordings or transcripts.

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `id` | Path | integer | Yes | Lead id |
| `limit` | Query | integer | No | 1 to 100, default 20 |
| `offset` | Query | integer | No | 0 or more, default 0 |

```bash
curl "https://api.fondaro.com/integrations/v1/leads/10132/calls?limit=20" \
  -H "Authorization: Bearer $FONDARO_INTEGRATION_KEY"
```

```json
{
  "calls": [
    {
      "id": "3f1c2b9e-7a4d-4e0b-8c61-0d2e5f7a9b13",
      "ownerId": "user_2abc",
      "direction": "outbound",
      "outcome": "success",
      "status": "completed",
      "durationSeconds": 184,
      "startedAt": "2026-09-30T09:12:00.000Z",
      "endedAt": "2026-09-30T09:15:04.000Z",
      "hasRecording": true
    }
  ],
  "total": 1,
  "totalIsExact": true,
  "hasMore": false,
  "nextOffset": 1
}
```

`outcome` is the call result (`success`, `no_answer`, `busy`, `failed`, `invalid_number` and similar), `null` when none was set. `ownerId` is the team member recorded as making or taking the call, and `null` when none was recorded. It is not the lead's current assignee: a lead reassigned after the call keeps the call's `ownerId`. Resolve it with `GET /integrations/v1/users/:id`.

## List a lead's activity

`GET /integrations/v1/leads/:id/activities`

Returns the lead's history as typed entries, newest first. Each entry has `type`, `occurredAt` and one object named for its kind (`call`, `note`, `task`, `email`, `statusChange`, `dealStageChange`, `deal`, `assigneeChange`, `document`, `viewing`). Entries carry ids and short previews, never email bodies, note HTML or transcripts.

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `id` | Path | integer | Yes | Lead id |
| `types` | Query | string | No | Comma-separated: `call`, `email`, `note`, `task-created`, `task-completed`, `status-change`, `deal-stage-change`, `deal-won`, `deal-lost`, `assignee-change`, `viewing`, `document-attached`, `lead-created` |
| `limit` | Query | integer | No | 1 to 100, default 20 |
| `offset` | Query | integer | No | 0 or more, default 0 |

```bash
curl "https://api.fondaro.com/integrations/v1/leads/10132/activities?types=note,email&limit=20" \
  -H "Authorization: Bearer $FONDARO_INTEGRATION_KEY"
```

```json
{
  "entries": [
    {
      "type": "note",
      "occurredAt": "2026-09-29T16:40:00.000Z",
      "note": { "id": "a7d0...", "preview": "Wants a sea view, budget up to 1.2M", "authorId": "user_2abc" }
    }
  ],
  "total": 14,
  "totalIsExact": false,
  "hasMore": true,
  "nextOffset": 20
}
```

The type filter applies before paging. Page with `nextOffset` until `hasMore` is `false`. When `totalIsExact` is `false`, `total` is a lower bound: count calls with the calls endpoint, not this one. A viewing appears on each of its client leads and on the collaborator lead that brought them.

## List viewings

`GET /integrations/v1/viewings`

Returns viewings registered on your organization's listings, newest first (`viewingAt`, then `id`). Needs `viewings:read`. A viewing can have several clients: `leadId` is the viewing's lead and `otherLeadIds` lists the others, empty when there are none.

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `propertyListingId` | Query | UUID | No | One listing |
| `leadId` | Query | integer | No | A client lead: the viewing's lead or one of its other clients |
| `collaboratorLeadId` | Query | integer | No | The collaborator lead |
| `agentUserId` | Query | string | No | The hosting agent |
| `dealId` | Query | UUID | No | One deal |
| `status` | Query | string | No | `scheduled`, `completed`, `cancelled`, `no_show` |
| `kind` | Query | string | No | `in_person`, `virtual` |
| `from` | Query | ISO 8601 | No | `viewingAt` on or after |
| `to` | Query | ISO 8601 | No | `viewingAt` before |
| `limit` | Query | integer | No | 1 to 100, default 20 |
| `offset` | Query | integer | No | 0 or more, default 0 |

```bash
curl "https://api.fondaro.com/integrations/v1/viewings?leadId=10132&status=scheduled" \
  -H "Authorization: Bearer $FONDARO_INTEGRATION_KEY"
```

```json
{
  "viewings": [
    {
      "id": "9b2e4c1a-0d3f-4a7b-8e6c-5d4f3a2b1c0e",
      "propertyListingId": "d41f0b7c-2e9a-4c5d-8f13-6a7b8c9d0e1f",
      "leadId": 10132,
      "otherLeadIds": [10133],
      "collaboratorLeadId": null,
      "agentUserId": "user_2abc",
      "dealId": null,
      "viewingAt": "2026-10-04T10:00:00.000Z",
      "kind": "in_person",
      "status": "scheduled",
      "source": "dashboard",
      "createdBy": "user_2abc",
      "createdAt": "2026-09-30T08:00:00.000Z",
      "updatedAt": "2026-09-30T08:00:00.000Z",
      "notes": "Bring the keys for the guest house",
      "notesTruncated": false
    }
  ],
  "total": 1,
  "hasMore": false,
  "nextOffset": 1
}
```

In the list, `notes` is the first 500 characters and `notesTruncated` says whether there is more. `GET /integrations/v1/viewings/:id` returns one viewing with its full notes, or `404`.

## Find leads by what they said

`POST /integrations/v1/leads/semantic-search`

Ranks leads by meaning across their notes, call transcripts and summaries, emails and messages.

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `query` | Body | string | Yes | 1 to 500 characters |
| `leadId` | Body | integer | No | Search one lead's records only |
| `occurredFrom` | Body | ISO 8601 | No | Evidence written on or after |
| `occurredTo` | Body | ISO 8601 | No | Evidence written on or before; after `occurredFrom` |
| `maxResults` | Body | integer | No | 1 to 30, default 10 |

```bash
curl -X POST https://api.fondaro.com/integrations/v1/leads/semantic-search \
  -H "Authorization: Bearer $FONDARO_INTEGRATION_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "buyers who mentioned a garden near the beach", "maxResults": 10}'
```

```json
{
  "matches": [
    {
      "leadId": 10132,
      "firstName": "Anna",
      "lastName": "Berg",
      "rank": 1,
      "matchCount": 3,
      "sourceCount": 2,
      "bestMatchAt": "2026-09-12T14:03:00.000Z",
      "evidence": [
        {
          "sourceType": "call_transcript",
          "sourceId": "3f1c2b9e-7a4d-4e0b-8c61-0d2e5f7a9b13",
          "occurredAt": "2026-09-12T14:03:00.000Z",
          "snippet": "...a small garden, walking distance to the beach..."
        }
      ]
    }
  ],
  "tookMs": 412,
  "reranked": true
}
```

`sourceType` is `note`, `call_transcript`, `call_summary`, `email`, `message` or `lead_memory`. `reranked: false` means the final ranking step was unavailable and the first-pass order was returned. The index covers about the last 24 months and fills shortly after records are written, so no match does not prove a lead does not exist.

## Find leads for a listing

`POST /integrations/v1/leads/listing-match`

Ranks leads whose wishes fit a listing. Send exactly one of `propertyListingId` or `listingRef`.

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `propertyListingId` | Body | UUID | One of | A Fondaro listing id |
| `listingRef.source` | Body | string | One of | A property source id, such as `resales_online` |
| `listingRef.id` | Body | string | One of | The listing's id in that source |
| `listingRef.country` | Body | string | No | The country the source returned with the listing, when it gave one |
| `maxResults` | Body | integer | No | 1 to 30, default 10 |

```bash
curl -X POST https://api.fondaro.com/integrations/v1/leads/listing-match \
  -H "Authorization: Bearer $FONDARO_INTEGRATION_KEY" \
  -H "Content-Type: application/json" \
  -d '{"listingRef": {"source": "resales_online", "id": "R123456"}, "maxResults": 10}'
```

The response has the same shape as meaning-based search.

## Webhooks

### Subscribe

`POST /integrations/v1/subscriptions`

Sends the events you choose to an HTTPS URL. Subscribing the same events to the same URL again returns the existing subscription and reactivates it.

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `eventTypes` | Body | string[] | Yes | 1 to 20 event names from the table below |
| `targetUrl` | Body | string | Yes | An `https` URL on port 443 that resolves to a public address |

```bash
curl -X POST https://api.fondaro.com/integrations/v1/subscriptions \
  -H "Authorization: Bearer $FONDARO_INTEGRATION_KEY" \
  -H "Content-Type: application/json" \
  -d '{"targetUrl": "https://example.com/fondaro", "eventTypes": ["lead.created", "message.received"]}'
```

```json
{
  "id": "6e5d4c3b-2a19-4807-b6f5-e4d3c2b1a090",
  "secret": "4f2a...c91e",
  "eventTypes": ["lead.created", "message.received"],
  "targetUrl": "https://example.com/fondaro"
}
```

Store `secret`: it signs every delivery. `GET /integrations/v1/subscriptions/:id` returns `id`, `eventTypes`, `targetUrl` and `isActive`; `DELETE /integrations/v1/subscriptions/:id` removes it and returns `{ "success": true }`. An organization can hold 25 subscriptions.

### Events

| Event | Fires when |
|-------|------------|
| `lead.created` | A lead is created |
| `lead.statusChanged` | A lead's status changes |
| `lead.assigned` | Assignees are added to a lead |
| `lead.tagged`, `lead.untagged` | A tag is added to or removed from a lead, one delivery per tag |
| `deal.created`, `deal.stageChanged`, `deal.won`, `deal.lost` | A deal is created, moves stage, or closes |
| `task.created`, `task.completed` | A task is created or completed |
| `note.created` | A note is created |
| `call.logged` | A call is recorded on a lead |
| `call.analyzed` | A call's analysis is saved. This one carries a written summary of the call |
| `message.received` | An email or chat message (WhatsApp, Instagram, LinkedIn, Telegram) from a lead is stored on the lead |
| `message.sent` | An email or chat message to a lead is sent from Fondaro, or from the agent's own connected mailbox or phone, and stored on the lead |
| `message.updated` | A chat message on a lead is edited after it was sent, in Fondaro or on the phone |
| `message.deleted` | A chat message on a lead is deleted for everyone, in Fondaro or on the phone |
| `meeting.created` | A meeting, a viewing included, is created on the Fondaro calendar, or an event gains an invitee. A lead event that invites nobody outside the team fires `task.created` instead |
| `meeting.responded` | An invitee accepts, declines or tentatively accepts, once per changed answer |

Payloads carry ids and times, not message bodies, subjects or addresses (`call.analyzed` excepted). Read details back with the endpoints above. A message from someone who is not a lead yet fires nothing, and neither does mail brought in when a mailbox is first connected.

### Deliveries

Each delivery is a `POST` with a JSON body and three headers:

| Header | Value |
|--------|-------|
| `X-Fondaro-Event` | The event name |
| `X-Fondaro-Timestamp` | Unix seconds when it was sent |
| `X-Fondaro-Signature` | `sha256=` followed by the hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the subscription `secret` |

```json
{
  "event": "message.received",
  "deliveryId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
  "occurredAt": "2026-09-25T10:00:02.000Z",
  "data": {
    "organizationId": "5b1f0c1e-8a54-4f1d-9d7e-3f2a1c9b7e21",
    "leadId": 10132,
    "messageId": "c2a4e1f0-6b7d-4c3e-9a1f-0e2d3c4b5a69",
    "messageType": "message",
    "channel": "whatsapp",
    "direction": "inbound",
    "connectedAccountId": "8e7d6c5b-4a39-4281-9f0e-1d2c3b4a5968",
    "userId": null,
    "occurredAt": "2026-09-25T10:00:00.000Z"
  }
}
```

Verify the signature over the raw body and reject old timestamps (for example older than 5 minutes). Answer with any `2xx` within 10 seconds; anything else is retried with backoff, up to 8 attempts. A subscription that fails 20 times in a row is deactivated; subscribe again to turn it back on. Delivery is at least once, so deduplicate on `deliveryId`.

For `message.received`, `message.sent`, `message.updated` and `message.deleted`, `messageType` is `email` or `message`, `channel` is `email`, `whatsapp`, `linkedin`, `instagram` or `telegram`, and `userId` is the member who pressed Send in Fondaro (`null` for inbound messages and for messages sent from the agent's own phone or mail app). On `message.updated` and `message.deleted`, `occurredAt` is when the message was changed, and you read its current text back from the lead's timeline (a deleted message has none).

`meeting.created` carries `organizationId`, `eventId`, `leadId`, `leadIds` (every lead and collaborator on the meeting, its own lead first), `viewingId`, `ownerId`, `startsAt`, `endsAt` and `occurredAt`. `meeting.responded` carries `organizationId`, `eventId`, `leadId`, `ownerId`, `response` (`accepted`, `declined` or `tentative`) and `occurredAt`.

## Errors and limits

| Status | When |
|--------|------|
| `400` | Invalid parameters, for example an unknown event type or a `limit` above 100 |
| `401` | Missing, unknown, revoked or expired key |
| `403` | The key lacks the route's scope, or the organization is disabled |
| `404` | The lead, deal, viewing or subscription is not in your organization |
| `409` | The organization already has 25 webhook subscriptions |
| `429` | Rate limit reached; wait `Retry-After` seconds |

Each key may make 120 requests per minute. The two meaning-based search routes also share 30 requests per minute per key and per organization.

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