# Matching organizations and people

> How organizations/match, organizations/switchboards/match and people/match find records, which organization fields are free and which are revealed for credits, which contacts organizations/contacts/match returns, every status they answer with, and full JSON for a hit, no match, an ambiguous match and running out of credits.

Matching answers one question per item: *which Funnelfeedr record is this?* The match endpoints take 1–25 items, answer each one with a `status`, and return results in request order with your `ref` echoed back. Matching is free; only a reveal costs credits.

| `status` | Meaning | Charged |
|---|---|---|
| `matched` | Exactly one record fits. `matchedBy` says which identifier hit. | Only for details revealed |
| `ambiguous` | `people/match` only: more than one person fits. Up to 5 `candidates`; nothing revealed. | No |
| `not_found` | Nothing fits. On `people/match`, `organizations/switchboards/match` and `organizations/contacts/match`, `reason` says what was missing. | No |
| `invalid` | The item itself is unusable. `error` says why; the rest of the batch still runs. | No |

A request that is malformed as a whole — no `items`, more than 25, a body that is not valid JSON — fails with a `422` instead. See [Errors](https://funnelfeedr.com/developers/errors.md).

## Organizations

`POST /external/v1/organizations/match` — free for the basics; an organization's details cost credits when you reveal them. Counts against the `search` [rate limit](https://funnelfeedr.com/developers/rate-limits.md) bucket, and the `spend` bucket too when it reveals.

Identify each organization by:

| Field | Example | Notes |
|---|---|---|
| `id` | `"6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21"` | A Funnelfeedr organization id from an earlier response. |
| `orgNumber` | `"556677-8899"` | National registration number, with or without the hyphen. |
| `countryCode` | `"SE"` | Optional, and only together with `orgNumber`: `SE`, `NO`, `DK` or `FI`. Without it the digit count picks the registry (10 digits SE, 9 NO, 8 DK or FI). |
| `linkedInUrl` | `"https://www.linkedin.com/company/nordvik-logistik"` | The organization's LinkedIn page. |
| `domain` | `"nordviklogistik.se"` | The website domain. `www.` and a pasted URL such as `https://www.nordviklogistik.se/about` work too. |

Send one identifier, or several to fall back: they are tried in the order `id`, `orgNumber`, `linkedInUrl`, `domain`, and the first hit wins (`matchedBy` tells you which). A malformed identifier — an org number with no digits, a LinkedIn URL that is not an organization's page, a `countryCode` without an `orgNumber` — makes the item `invalid` rather than silently falling back. Organization names are not accepted: a name is ambiguous, and a silently wrong organization is worse than a miss.

Several identifiers for one organization, for example from a CRM row that has both. `orgNumber` is tried first, and `domain` only if it misses:

```json
{
  "items": [
    { "ref": "row-7", "domain": "nordviklogistik.se", "orgNumber": "556677-8899", "countryCode": "SE" }
  ],
  "dryRun": false
}
```

### Free match

Three organizations from a CRM export, each with its own identifier:

```json
{
  "items": [
    { "ref": "crm-1001", "domain": "nordviklogistik.se" },
    { "ref": "crm-1002", "orgNumber": "5590123456", "countryCode": "SE" },
    { "ref": "crm-1003", "domain": "unknown-startup.se" }
  ],
  "dryRun": false
}
```

One hit by domain, one by organization number, one miss. Nothing is charged: each organization comes back with its basics and the identifier it was matched by, and its details held back.

```json
{
  "results": [
    {
      "ref": "crm-1001",
      "status": "matched",
      "matchedBy": "domain",
      "organization": {
        "id": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "name": "Nordvik Logistik AB",
        "detailsRevealed": false,
        "countryCode": "SE",
        "domain": "nordviklogistik.se",
        "city": "Göteborg",
        "legalForm": "Aktiebolag",
        "foundedYear": 2004,
        "url": "https://app.funnelfeedr.com/organizations/6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21"
      }
    },
    {
      "ref": "crm-1002",
      "status": "matched",
      "matchedBy": "orgNumber",
      "organization": {
        "id": "2b7e1c55-0d3a-4f7e-9c1b-8a6d5e4f3a21",
        "name": "Fjällräven Frakt AB",
        "detailsRevealed": false,
        "organizationNumber": "559012-3456",
        "countryCode": "SE",
        "city": "Stockholm",
        "legalForm": "Aktiebolag",
        "foundedYear": 2017,
        "url": "https://app.funnelfeedr.com/organizations/2b7e1c55-0d3a-4f7e-9c1b-8a6d5e4f3a21"
      }
    },
    {
      "ref": "crm-1003",
      "status": "not_found"
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0,
  "creditsRemaining": 412.4,
  "dryRun": false
}
```

An organization match is `matched`, `not_found` or `invalid`; it is never `ambiguous`. A domain matches only the organization flagged as the site's owner, so a subsidiary sharing its parent's website is not returned for the parent's domain — use its organization number. A Swedish sole trader's number is a personal identity number and comes back with its last four digits masked (`850101-XXXX`), even when the details are revealed.

Funnelfeedr does not research unknown organizations on demand: an organization it does not have is `not_found`, immediately and for free.

### Basics and details

Every organization object has two parts:

- **The basics, free on every response:** `id`, `url`, `name`, `countryCode`, `legalForm`, `city`, `foundedYear`, and the identifier the item was matched by. An `orgNumber` match returns `organizationNumber`, a `domain` match returns `domain` (the domain you sent, normalized), a `linkedInUrl` match returns `linkedInUrl`; an `id` match returns nothing extra.
- **The details, revealed for credits:** the identifiers you did not match by (`organizationNumber`, `domain`, `linkedInUrl`), `website`, `employeesMin`/`employeesMax`, `revenueMin`/`revenueMax`/`revenueCurrency`, `oneSentenceDescription`, `description` (English), `technologies`, `keywords`, `tags`, and the rest of the address: `addressLine1`, `addressLine2`, `postalCode`, `stateOrProvince`.

`detailsRevealed` says which you got. It is `true` when the object carries every detail Funnelfeedr has: your account revealed them within the last year, or the organization has none beyond the basics. An organization your account has revealed returns its details on every plain match, for free, for a year after the reveal.

### Reveal the details

Options for the whole request:

| Option | Meaning |
|---|---|
| `reveal` | `["details"]` to reveal the details of every `matched` organization. Leave it out to match only, for free. |
| `dryRun` | `true` to get the price of the reveal without spending anything. |
| `maxCredits` | The most the request may spend. **Required** when `reveal` is set and `dryRun` is not `true`. |

Revealing costs 0.2 credits per organization by default, nothing for an organization your account revealed within the last year, and nothing for one with no details beyond the basics. A request that reveals also needs an `Idempotency-Key` header, fails as a whole with `402` or `422 credit_cap_exceeded` exactly like a [people reveal](#out-of-credits), and spends nothing when it fails. With `reveal` set, each matched result carries a `creditCost`: what that organization costs (on a dry run) or cost. Start with a [dry run](https://funnelfeedr.com/developers/credits-and-reveal.md#organization-details) when you are not sure of the price.

After a dry run quoted 0.2 credits:

```http
POST /external/v1/organizations/match HTTP/1.1
Host: api.funnelfeedr.com
Authorization: Bearer ff_live_...
Content-Type: application/json
Idempotency-Key: 2c8f4b1a-6e3d-4a9f-b7c2-5d1e0f8a3b64
```

```json
{
  "items": [
    { "ref": "crm-1001", "domain": "nordviklogistik.se" }
  ],
  "reveal": ["details"],
  "maxCredits": 0.2,
  "dryRun": false
}
```

```json
{
  "results": [
    {
      "ref": "crm-1001",
      "status": "matched",
      "matchedBy": "domain",
      "organization": {
        "id": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "name": "Nordvik Logistik AB",
        "detailsRevealed": true,
        "organizationNumber": "556677-8899",
        "countryCode": "SE",
        "domain": "nordviklogistik.se",
        "website": "https://www.nordviklogistik.se",
        "linkedInUrl": "https://www.linkedin.com/company/nordvik-logistik",
        "city": "Göteborg",
        "addressLine1": "Hamngatan 12",
        "postalCode": "411 06",
        "employeesMin": 50,
        "employeesMax": 99,
        "revenueMin": 100000000,
        "revenueMax": 250000000,
        "revenueCurrency": "SEK",
        "oneSentenceDescription": "Nordvik Logistik runs third-party warehousing and road freight for retailers in western Sweden.",
        "description": "Nordvik Logistik AB is a Gothenburg logistics provider offering warehousing, order fulfilment and domestic road freight to retail and e-commerce customers across western Sweden.",
        "technologies": ["Google Analytics", "HubSpot", "WordPress"],
        "keywords": ["3PL", "road freight", "warehousing"],
        "tags": ["Logistics", "B2B"],
        "legalForm": "Aktiebolag",
        "foundedYear": 2004,
        "url": "https://app.funnelfeedr.com/organizations/6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21"
      },
      "creditCost": 0.2
    }
  ],
  "creditsCharged": 0.2,
  "creditsQuoted": 0.2,
  "creditsRemaining": 412.2,
  "dryRun": false
}
```

The response also carries `Funnelfeedr-Credits-Remaining: 412.2`. From now on a plain match of Nordvik Logistik returns these details for free, with `detailsRevealed: true`, until a year after the reveal.

## Organizations' contacts

`POST /external/v1/organizations/contacts/match` — free, `search` bucket.

This lists the people you would want to reach at up to 25 organizations in one call: **only the contacts that match your account's target contact roles**, the roles set up in Funnelfeedr under **Settings → Target contact roles**. They are exactly the contacts the web app shows under the **Target roles** filter of the organization's contacts tab, in the same order. Contacts that come from your connected CRM carry no target-role verdict in Funnelfeedr, so they are left out while the filter applies.

If your account has **no target contact roles set up**, the web app has no target-role filter and lists every contact at the organization — and so does this endpoint. `filteredByTargetRoles` on each result tells you which you got: `true` when the list is filtered to your target roles, `false` when it is every contact. Set up target roles in the app if you only want the people you sell to.

Identify the organizations exactly as for `organizations/match` — the same fields, the same `id` → `orgNumber` → `linkedInUrl` → `domain` fallback. `limit` (1 to 50, default 20) is the page size for every organization in the request.

| `status` | Meaning |
|---|---|
| `matched` | `organization` and one page of its `contacts`, with `totalCount` (across all pages), `filteredByTargetRoles` and `nextCursor` (`null` on the organization's last page). |
| `not_found` | `reason` is `organization_not_found`: no organization matches the identifiers. |
| `invalid` | The item is unusable — no identifier, a malformed one, or a `cursor` that was altered or belongs to another organization (`invalid_cursor`); see `error`. |

Each result's `organization` is the same object `organizations/match` returns, masked the same way: the basics and the identifier it was matched by, plus the details only if your account has already revealed them. This endpoint never reveals anything. Email and phone are masked until your account reveals them; name, job title and LinkedIn never are. Pass the ids you want to `POST /contacts/reveal` — see [Credits & reveal](https://funnelfeedr.com/developers/credits-and-reveal.md).

```http
POST /external/v1/organizations/contacts/match HTTP/1.1
Host: api.funnelfeedr.com
Authorization: Bearer ff_live_...
Content-Type: application/json

{
  "items": [
    { "ref": "crm-1001", "domain": "nordviklogistik.se" },
    { "ref": "crm-1002", "orgNumber": "5590123456", "countryCode": "SE" }
  ],
  "limit": 2
}
```

```json
{
  "results": [
    {
      "ref": "crm-1001",
      "status": "matched",
      "matchedBy": "domain",
      "organization": {
        "id": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "name": "Nordvik Logistik AB",
        "detailsRevealed": false,
        "countryCode": "SE",
        "domain": "nordviklogistik.se",
        "city": "Göteborg",
        "legalForm": "Aktiebolag",
        "foundedYear": 2004,
        "url": "https://app.funnelfeedr.com/organizations/6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21"
      },
      "contacts": [
        {
          "id": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
          "name": "Anna Svensson-Berg",
          "jobTitle": "Head of Procurement",
          "isExecutive": false,
          "isDecisionMaker": true,
          "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
          "organizationName": "Nordvik Logistik AB",
          "email": "XXXXX@nordviklogistik.se",
          "emailIsMasked": true,
          "phone": "+46701XXXXXX",
          "phoneIsMasked": true,
          "linkedInUrl": "https://linkedin.com/in/anna-svensson-berg"
        },
        {
          "id": "d41d8cd9-8f00-4b20-9e98-0ecf8427e1b2",
          "name": "Johan Lindqvist",
          "jobTitle": "CFO",
          "isExecutive": true,
          "isDecisionMaker": true,
          "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
          "organizationName": "Nordvik Logistik AB",
          "email": "XXXXX@nordviklogistik.se",
          "emailIsMasked": true,
          "phoneIsMasked": false
        }
      ],
      "totalCount": 3,
      "filteredByTargetRoles": true,
      "nextCursor": "eyJzIjoib3JnYW5pemF0aW9uLWNvbnRhY3RzOjZmOWQyZjFlNmYzNzRhMmY5YTNlMWI1YzBmNGQ3ZTIxIiwicCI6MiwibiI6Mn0"
    },
    {
      "ref": "crm-1002",
      "status": "matched",
      "matchedBy": "orgNumber",
      "organization": {
        "id": "2b7e1c55-0d3a-4f7e-9c1b-8a6d5e4f3a21",
        "name": "Fjällräven Frakt AB",
        "detailsRevealed": false,
        "organizationNumber": "559012-3456",
        "countryCode": "SE",
        "city": "Stockholm",
        "legalForm": "Aktiebolag",
        "foundedYear": 2017,
        "url": "https://app.funnelfeedr.com/organizations/2b7e1c55-0d3a-4f7e-9c1b-8a6d5e4f3a21"
      },
      "contacts": [],
      "totalCount": 0,
      "filteredByTargetRoles": true,
      "nextCursor": null
    }
  ]
}
```

Nordvik Logistik has a third contact. To read it, send the result's `nextCursor` back as an item's `cursor`. The cursor already names the organization, so the item needs nothing else; you can send next pages of several organizations, and first pages of others, in the same request:

```json
{
  "items": [
    { "ref": "crm-1001", "cursor": "eyJzIjoib3JnYW5pemF0aW9uLWNvbnRhY3RzOjZmOWQyZjFlNmYzNzRhMmY5YTNlMWI1YzBmNGQ3ZTIxIiwicCI6MiwibiI6Mn0" }
  ]
}
```

That result carries no `matchedBy`, and its `organization` echoes no identifier, since the cursor rather than an identifier named the organization. A cursor keeps the page size it started with, so `limit` does not change it. See [Pagination](https://funnelfeedr.com/developers/pagination.md).

## Switchboards

`POST /external/v1/organizations/switchboards/match` — free, or credits when it reveals a number or email. Counts against the `search` rate-limit bucket, and the `spend` bucket too when it reveals.

Use it when you want to call an organization rather than a named person: its switchboard, reception or general information line. Identify up to 25 organizations exactly as for `organizations/match` — the same fields, the same `id` → `orgNumber` → `linkedInUrl` → `domain` fallback. For each one Funnelfeedr picks the switchboard the way its own dialer does when a rep is told to "call the switchboard": the organization's best-ranked contact that is not a person and has a phone number, preferring a number in the organization's own country.

| `status` | Meaning | Charged |
|---|---|---|
| `matched` | `contact` is the switchboard. Its phone, and its email when it has one, are masked unless your account revealed them before, or this call reveals them. | Only if revealed |
| `not_found` | `reason` is `organization_not_found` (no organization matches the identifiers) or `no_switchboard` (the organization is known, and returned in `organization`, but Funnelfeedr has no switchboard with a phone number for it). | No |
| `invalid` | The item is unusable; see `error`. | No |

Options for the whole request:

| Option | Meaning |
|---|---|
| `reveal` | `["phone"]`, `["email"]` or `["phone", "email"]` to reveal them on every matched switchboard. Leave it out to find switchboards for free. |
| `dryRun` | `true` to get the price of the reveal without spending anything. |
| `maxCredits` | The most the request may spend. **Required** when `reveal` is set and `dryRun` is not `true`; checked before any lookup runs. |

A switchboard costs what revealing it in the app does — the rates for a contact that is not a person: 1.0 credit for the phone and 0.2 for the email by default, the sum for both, nothing for what your account has revealed before. A request that reveals also needs an `Idempotency-Key` header, fails as a whole with `402` or `422 credit_cap_exceeded` exactly like a [people reveal](#out-of-credits), and spends nothing when it fails. See [Credits & reveal](https://funnelfeedr.com/developers/credits-and-reveal.md).

Each result's `organization` is the same object `organizations/match` returns, masked the same way: the basics and the identifier it was matched by, plus the details only if your account has already revealed them. This endpoint never reveals organization details; use [`organizations/match`](#reveal-the-details) for that.

### Found

```json
{
  "items": [
    { "ref": "crm-1001", "domain": "nordviklogistik.se" },
    { "ref": "crm-1002", "orgNumber": "5590123456", "countryCode": "SE" }
  ]
}
```

```json
{
  "results": [
    {
      "ref": "crm-1001",
      "status": "matched",
      "matchedBy": "domain",
      "organization": {
        "id": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "name": "Nordvik Logistik AB",
        "detailsRevealed": false,
        "countryCode": "SE",
        "domain": "nordviklogistik.se",
        "city": "Göteborg",
        "legalForm": "Aktiebolag",
        "foundedYear": 2004,
        "url": "https://app.funnelfeedr.com/organizations/6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21"
      },
      "contact": {
        "id": "5a1c9e7f-3b2d-4c6e-8f0a-9d4b7e2c1f38",
        "name": "Nordvik Logistik AB",
        "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "organizationName": "Nordvik Logistik AB",
        "emailIsMasked": false,
        "phone": "+46317XXXXXX",
        "phoneIsMasked": true
      }
    },
    {
      "ref": "crm-1002",
      "status": "not_found",
      "reason": "no_switchboard",
      "matchedBy": "orgNumber",
      "organization": {
        "id": "2b7e1c55-0d3a-4f7e-9c1b-8a6d5e4f3a21",
        "name": "Fjällräven Frakt AB",
        "detailsRevealed": false,
        "organizationNumber": "559012-3456",
        "countryCode": "SE",
        "city": "Stockholm",
        "legalForm": "Aktiebolag",
        "foundedYear": 2017,
        "url": "https://app.funnelfeedr.com/organizations/2b7e1c55-0d3a-4f7e-9c1b-8a6d5e4f3a21"
      }
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0,
  "creditsRemaining": 412.4,
  "dryRun": false
}
```

A switchboard is a contact like any other, so its `id` also works with `POST /contacts/reveal`. Its `name` is whatever Funnelfeedr has for the line — often the organization's name.

### Found and revealed

After a dry run quoted 1.0 credit:

```http
POST /external/v1/organizations/switchboards/match HTTP/1.1
Host: api.funnelfeedr.com
Authorization: Bearer ff_live_...
Content-Type: application/json
Idempotency-Key: 7d2e9a41-0c5b-4f8e-b3a6-91e4c2d7f058
```

```json
{
  "items": [
    { "ref": "crm-1001", "domain": "nordviklogistik.se" }
  ],
  "reveal": ["phone"],
  "maxCredits": 1,
  "dryRun": false
}
```

```json
{
  "results": [
    {
      "ref": "crm-1001",
      "status": "matched",
      "matchedBy": "domain",
      "organization": {
        "id": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "name": "Nordvik Logistik AB",
        "detailsRevealed": false,
        "countryCode": "SE",
        "domain": "nordviklogistik.se",
        "city": "Göteborg",
        "legalForm": "Aktiebolag",
        "foundedYear": 2004,
        "url": "https://app.funnelfeedr.com/organizations/6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21"
      },
      "contact": {
        "id": "5a1c9e7f-3b2d-4c6e-8f0a-9d4b7e2c1f38",
        "name": "Nordvik Logistik AB",
        "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "organizationName": "Nordvik Logistik AB",
        "emailIsMasked": false,
        "phone": "+46317001234",
        "phoneIsMasked": false
      },
      "reveal": {
        "creditCost": 1,
        "newInfoTypes": ["phone"],
        "alreadyRevealedInfoTypes": [],
        "unavailableInfoTypes": []
      }
    }
  ],
  "creditsCharged": 1,
  "creditsQuoted": 1,
  "creditsRemaining": 411.4,
  "dryRun": false
}
```

Revealing the switchboard does not reveal the organization's details: `detailsRevealed` stays `false` until the account reveals them with `organizations/match`.

### Not found and invalid

```json
{
  "results": [
    {
      "ref": "crm-1003",
      "status": "not_found",
      "reason": "organization_not_found"
    },
    {
      "ref": "crm-1004",
      "status": "invalid",
      "error": {
        "code": "missing_identifier",
        "message": "Give one of id, orgNumber, linkedInUrl or domain."
      }
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0,
  "creditsRemaining": 412.4,
  "dryRun": false
}
```

## People

`POST /external/v1/people/match` — free, or credits when it reveals. Counts against the `search` rate-limit bucket, and the `spend` bucket too when it reveals.

Identify each person by one of these, strongest first. If an item has several, the strongest wins and `matchedBy` says which was used:

| Field | Example | Notes |
|---|---|---|
| `linkedInUrl` | `"https://www.linkedin.com/in/johan-lindqvist-cfo"` | A LinkedIn profile (`/in/`). |
| `email` | `"anna.svensson-berg@nordviklogistik.se"` | Matched exactly, ignoring case — but only against addresses your account already sees unmasked. See [Matching by email](#matching-by-email). |
| `name` + `organization` | `"Anna Svensson"` and `{ "domain": "nordviklogistik.se" }` | A name needs an organization; alone it is `invalid` (`missing_organization`). |

`organization` takes the same identifiers as an organization match — `id`, `orgNumber` (+ optional `countryCode`), `linkedInUrl`, `domain` — with the same fallback order.

Options for the whole request:

| Option | Meaning |
|---|---|
| `reveal` | What to reveal for each `matched` person: any of `"email"`, `"phone"`, `"linkedIn"`. Leave it out to match only, for free. LinkedIn URLs are never masked, so `"linkedIn"` is free. |
| `dryRun` | `true` to get the price of the reveal without spending anything. |
| `maxCredits` | The most the request may spend. **Required** when `reveal` is set and `dryRun` is not `true`; checked before any lookup runs. |

A request that reveals also needs an `Idempotency-Key` header. See [Credits & reveal](https://funnelfeedr.com/developers/credits-and-reveal.md).

### How names match

Names match deterministically, not fuzzily. Case, accents and punctuation are normalised, so `Åsa Öberg` and `Asa Oberg` are the same name, and a hyphen counts as a space. A hyphenated last name also matches either half: `Anna Svensson` and `Anna Berg` both match `Anna Svensson-Berg`. Nicknames, typos and dropped middle names do not match: "Kalle" does not find "Karl" in v1. If a name is `not_found`, try another identifier you have rather than variations of the name.

A name is looked for only among the people the app lists at that organization. A LinkedIn URL is looked for everywhere, but a person the app lists under no organization is `person_not_found`.

### Matching by email

An email only matches an address your account can already see in full: one it has revealed before, or one that belongs to your account. Any other address — including one Funnelfeedr has on record but your account has not revealed — is `not_found` with `person_not_found`, and costs nothing. Matching is not a way to check whether a guessed email exists. If you have only a suspected email, match by `linkedInUrl` or by `name` plus `organization` instead, then reveal the email if you need it.

### Hit

A person found exactly once. Without `reveal`, email and phone come back masked unless your account already revealed them, and nothing is charged.

```json
{
  "items": [
    { "ref": "lead-42", "name": "Anna Svensson", "organization": { "domain": "nordviklogistik.se" } }
  ],
  "dryRun": false
}
```

```json
{
  "results": [
    {
      "ref": "lead-42",
      "status": "matched",
      "matchedBy": "name",
      "contact": {
        "id": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
        "name": "Anna Svensson-Berg",
        "jobTitle": "Head of Procurement",
        "isExecutive": false,
        "isDecisionMaker": true,
        "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "organizationName": "Nordvik Logistik AB",
        "email": "XXXXX@nordviklogistik.se",
        "emailIsMasked": true,
        "phone": "+46701XXXXXX",
        "phoneIsMasked": true,
        "linkedInUrl": "https://linkedin.com/in/anna-svensson-berg"
      }
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0,
  "creditsRemaining": 412.4,
  "dryRun": false
}
```

### Hit with reveal

The same person, revealing email and phone after a [dry run](https://funnelfeedr.com/developers/credits-and-reveal.md#quote-with-dryrun) quoted 1.2 credits:

```http
POST /external/v1/people/match HTTP/1.1
Host: api.funnelfeedr.com
Authorization: Bearer ff_live_...
Content-Type: application/json
Idempotency-Key: 4f6a1c2e-9b7d-4e3a-8c51-2d0f7b9e6a14
```

```json
{
  "items": [
    { "ref": "lead-42", "name": "Anna Svensson", "organization": { "domain": "nordviklogistik.se" } }
  ],
  "reveal": ["email", "phone"],
  "maxCredits": 1.2,
  "dryRun": false
}
```

```json
{
  "results": [
    {
      "ref": "lead-42",
      "status": "matched",
      "matchedBy": "name",
      "contact": {
        "id": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
        "name": "Anna Svensson-Berg",
        "jobTitle": "Head of Procurement",
        "isExecutive": false,
        "isDecisionMaker": true,
        "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "organizationName": "Nordvik Logistik AB",
        "email": "anna.svensson-berg@nordviklogistik.se",
        "emailIsMasked": false,
        "phone": "+46701234567",
        "phoneIsMasked": false,
        "linkedInUrl": "https://linkedin.com/in/anna-svensson-berg"
      },
      "reveal": {
        "creditCost": 1.2,
        "newInfoTypes": ["email", "phone"],
        "alreadyRevealedInfoTypes": [],
        "unavailableInfoTypes": []
      }
    }
  ],
  "creditsCharged": 1.2,
  "creditsQuoted": 1.2,
  "creditsRemaining": 411.2,
  "dryRun": false
}
```

The response also carries `Funnelfeedr-Credits-Remaining: 411.2`. If the person has no phone number on record, `phone` is listed in `unavailableInfoTypes` and only the email is charged. A revealed or quoted contact keeps the organization (and the title there) it was matched at, even when the person also works somewhere else.

### No match

Nothing is charged. `reason` tells you which half failed: the organization (`organization_not_found`) or the person at an organization that was found (`person_not_found`).

```json
{
  "items": [
    { "ref": "lead-44", "name": "Karl Berg", "organization": { "domain": "nordviklogistik.se" } },
    { "ref": "lead-45", "name": "Eva Holm", "organization": { "domain": "unknown-startup.se" } }
  ]
}
```

```json
{
  "results": [
    {
      "ref": "lead-44",
      "status": "not_found",
      "reason": "person_not_found",
      "matchedBy": "name"
    },
    {
      "ref": "lead-45",
      "status": "not_found",
      "reason": "organization_not_found",
      "matchedBy": "name"
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0,
  "creditsRemaining": 412.4,
  "dryRun": false
}
```

### Ambiguous

More than one person fits — two people at the organization with that name, or several contacts sharing a LinkedIn URL or email. Nothing is revealed or charged for them, even if the request asked for a reveal. Up to five candidates come back with their id, name, job title and organization (never contact details); choose one and reveal it by id with `POST /contacts/reveal`, or ask the user.

```json
{
  "items": [
    { "ref": "lead-46", "name": "Anna Svensson", "organization": { "domain": "nordviklogistik.se" } }
  ]
}
```

```json
{
  "results": [
    {
      "ref": "lead-46",
      "status": "ambiguous",
      "matchedBy": "name",
      "candidates": [
        {
          "id": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
          "name": "Anna Svensson-Berg",
          "jobTitle": "Head of Procurement",
          "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
          "organizationName": "Nordvik Logistik AB"
        },
        {
          "id": "e3b0c442-98fc-4c14-9afb-f4c8996fb924",
          "name": "Anna Svensson",
          "jobTitle": "Warehouse Manager",
          "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
          "organizationName": "Nordvik Logistik AB"
        }
      ]
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0,
  "creditsRemaining": 412.4,
  "dryRun": false
}
```

### Invalid item

```json
{
  "results": [
    {
      "ref": "lead-47",
      "status": "invalid",
      "error": {
        "code": "missing_organization",
        "message": "A name needs an organization (id, domain, orgNumber or linkedInUrl): a name alone names nobody."
      }
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0,
  "creditsRemaining": 412.4,
  "dryRun": false
}
```

### Out of credits

If the account cannot pay for the whole reveal, the **whole request** fails with `402` before anything is revealed or charged — no partial results, and no matches either. Top up in the app, then send the same request again; only successful responses are stored against an `Idempotency-Key`, so retrying with the same key runs it.

```http
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
```

```json
{
  "type": "https://funnelfeedr.com/developers/errors/insufficient_credits",
  "title": "Not enough credits",
  "status": 402,
  "detail": "This request needs 3.4 credits; the account has 1.2. Nothing was revealed or charged. Buy credits in Funnelfeedr under Settings → Subscription, then retry.",
  "code": "insufficient_credits",
  "retryable": false,
  "creditsRequired": 3.4,
  "creditsRemaining": 1.2,
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
```

### Over your cap

If the reveal would cost more than `maxCredits`, the request fails with `422 credit_cap_exceeded`, again before anything is spent and without matches. `creditsRequired` is the real cost, so you can ask the user whether to go ahead — or run the request with `dryRun: true` first.

```json
{
  "type": "https://funnelfeedr.com/developers/errors/credit_cap_exceeded",
  "title": "The request would cost more than maxCredits",
  "status": 422,
  "detail": "This reveal costs 3.4 credits, more than maxCredits (2). Nothing was revealed or charged. Raise maxCredits or reveal fewer contacts or info types.",
  "code": "credit_cap_exceeded",
  "retryable": false,
  "creditsRequired": 3.4,
  "maxCredits": 2,
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
```
