> ## Documentation Index
> Fetch the complete documentation index at: https://developers.qomon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Query your contacts with flexible AND/OR conditions across contact info, address, financial activity, email engagement, and custom fields.

The search API lets you query your contacts using a flexible condition-based language. You can filter by any combination of personal info, address, tags, financial activity (transactions, donations, memberships), email campaign engagement, sanctions, and custom fields.

Two endpoints share the same query format:

* `POST /search` — returns a paginated list of matching contacts
* `POST /kpi` — returns only the count of matching contacts, with no contact data

## How queries are structured

Every search query is a two-level tree. The outer group sets the top-level logic; each inner group contains the individual conditions.

```text theme={"system"}
query
└── $all (AND) or $at_least_one (OR)          ← outer group
    ├── $all or $at_least_one                  ← inner group
    │   ├── $condition { attr, ope, value }
    │   └── $condition { attr, ope, value }
    └── $all or $at_least_one                  ← inner group
        └── $condition { attr, ope, value }
```

Each group is either:

* **`$all`** — ALL conditions inside must match (AND logic)
* **`$at_least_one`** — AT LEAST ONE condition must match (OR logic)

The minimum shape — even for a single condition — always requires two levels of nesting:

```json theme={"system"}
{
  "data": {
    "advanced_search": {
      "per_page": 100,
      "query": {
        "$all": [
          {
            "$all": [
              { "$condition": { "attr": "address.city", "ope": "eql", "value": "Paris" } }
            ]
          }
        ]
      }
    }
  }
}
```

**Example:** *City is Paris AND has a mobile number AND (tagged volunteer OR tagged donor)*

```json theme={"system"}
{
  "data": {
    "advanced_search": {
      "per_page": 100,
      "query": {
        "$all": [
          {
            "$all": [
              { "$condition": { "attr": "address.city", "ope": "eql", "value": "Paris" } },
              { "$condition": { "attr": "mobile", "ope": "ext" } }
            ]
          },
          {
            "$at_least_one": [
              { "$condition": { "attr": "tags.name", "ope": "eql", "value": "volunteer" } },
              { "$condition": { "attr": "tags.name", "ope": "eql", "value": "donor" } }
            ]
          }
        ]
      }
    }
  }
}
```

## Operators

| What you want to say                 | `ope` value                        |
| ------------------------------------ | ---------------------------------- |
| equals                               | `eql`                              |
| does not equal                       | `not_eql`                          |
| has a value / is filled in           | `ext`                              |
| has no value / is empty              | `not_ext`                          |
| is before / at most                  | `lte`                              |
| is after / at least                  | `gte`                              |
| is between two values                | `range` — use with `from` and `to` |
| contains the text                    | `contains`                         |
| does not contain the text            | `not_contains`                     |
| starts with                          | `start_with`                       |
| does not start with                  | `not_start_with`                   |
| equals (case-sensitive, exact match) | `eql:strictdata`                   |
| matches any of a list                | `any_of`                           |
| matches none of a list               | `none_of`                          |
| matches all of a list                | `all_of`                           |

Date fields accept relative values: `"now"`, `"now-30d"`, `"now-1y"`, `"now+1y"`. All dates are interpreted as UTC.

## Pagination and sorting

| Parameter              | Description                                                                                          |
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
| `per_page`             | Contacts per page — **required**, 1–1000                                                             |
| `page`                 | Page number (default: 0)                                                                             |
| `sort_attr`            | Sort by: `surname`, `firstname`, `birthdate`, `gender`, `lastchange`, `mail`, `married_name`, `city` |
| `sort_asc`             | `true` = ascending order (default: false)                                                            |
| `include_interactions` | `true` = include each contact's interaction history in the response                                  |

## Getting a count without results

Use `POST /kpi` with the same query body to get the number of matching contacts instantly without fetching the full contact list.

```json theme={"system"}
{ "data": { "advanced_search": { "query": { "$all": [ ... ] } } } }
```

Response: `{ "data": { "total": 1042 }, "status": "success" }`

## Examples

<AccordionGroup>
  <Accordion title="Active memberships today">
    *Contacts whose membership started on or before today and ends on or after today.*

    ```json theme={"system"}
    {
      "data": {
        "advanced_search": {
          "per_page": 100,
          "query": {
            "$all": [{
              "$all": [
                { "$condition": { "attr": "memberships.start_date", "ope": "lte", "value": "now" } },
                { "$condition": { "attr": "memberships.end_date",   "ope": "gte", "value": "now" } }
              ]
            }]
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Donors with cumulative amount ≥ €100">
    *Amount values are always passed as strings in euros.*

    ```json theme={"system"}
    {
      "data": {
        "advanced_search": {
          "per_page": 100,
          "query": {
            "$all": [{
              "$all": [
                { "$condition": { "attr": "donations.sum_amount", "ope": "gte", "value": "100" } }
              ]
            }]
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Email delivered but not opened (campaign 42)">
    ```json theme={"system"}
    {
      "data": {
        "advanced_search": {
          "per_page": 100,
          "query": {
            "$all": [{
              "$all": [
                { "$condition": { "attr": "emails.campaign_id",  "ope": "eql",     "value": "42" } },
                { "$condition": { "attr": "emails.delivered_at", "ope": "ext" } },
                { "$condition": { "attr": "emails.opened_at",    "ope": "not_ext" } }
              ]
            }]
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Two independent AND groups">
    *Members in 2024 who also made at least one donation.*

    ```json theme={"system"}
    {
      "data": {
        "advanced_search": {
          "per_page": 100,
          "query": {
            "$all": [
              {
                "$all": [
                  { "$condition": { "attr": "memberships.period", "ope": "eql", "value": "2024" } }
                ]
              },
              {
                "$all": [
                  { "$condition": { "attr": "donations.count_amount", "ope": "gte", "value": "1" } }
                ]
              }
            ]
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="OR on cities combined with an AND condition">
    *In Paris or Lyon, and has a mobile number.*

    ```json theme={"system"}
    {
      "data": {
        "advanced_search": {
          "per_page": 100,
          "query": {
            "$all": [
              {
                "$at_least_one": [
                  { "$condition": { "attr": "address.city", "ope": "eql", "value": "Paris" } },
                  { "$condition": { "attr": "address.city", "ope": "eql", "value": "Lyon" } }
                ]
              },
              {
                "$all": [
                  { "$condition": { "attr": "mobile", "ope": "ext" } }
                ]
              }
            ]
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="Filter by form answer (consent, custom field…)">
    *Use `GET /v1/forms/type/{type}` to find your `form_id`, then `GET /forms/{id}` for `form_ref_ids`.*

    ```json theme={"system"}
    {
      "data": {
        "advanced_search": {
          "per_page": 100,
          "query": {
            "$all": [{
              "$all": [{
                "$condition": {
                  "attr": "form",
                  "form_id": 4530,
                  "form_ref_ids": [7486],
                  "ope": "eql",
                  "value": "consent_email"
                }
              }]
            }]
          }
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Attribute reference

### Contact fields

| Attribute         | Meaning                                       | Operators                                                                                |
| ----------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `firstname`       | First name                                    | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with`                       |
| `surname`         | Last name                                     | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with`                       |
| `married_name`    | Married name                                  | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with`                       |
| `gender`          | Gender (`M`, `F`, `O`)                        | `eql`, `not_eql`, `ext`, `not_ext`                                                       |
| `birthdate`       | Date of birth                                 | `eql`, `ext`, `not_ext`, `range`, `lte`, `gte`                                           |
| `age_cat`         | Age category (0–6)                            | `eql`, `not_eql`, `ext`, `not_ext`                                                       |
| `birthcity`       | City of birth                                 | `eql`, `not_eql`, `ext`, `not_ext`                                                       |
| `birthcountry`    | Country of birth                              | `eql`, `not_eql`, `ext`, `not_ext`                                                       |
| `nationality`     | Nationality                                   | `eql`, `not_eql`, `ext`, `not_ext`                                                       |
| `mail`            | Email address                                 | `eql:strictdata`, `not_eql:strictdata`, `ext`, `not_ext`, `start_with`, `not_start_with` |
| `phone`           | Landline                                      | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with`                       |
| `mobile`          | Mobile number                                 | `eql`, `not_eql`, `ext`, `not_ext`, `contains`, `start_with`, `not_start_with`           |
| `tags.name`       | Tag name                                      | `eql`, `not_eql`, `any_of`, `none_of`, `all_of`, `ext`, `not_ext`                        |
| `black_list`      | Blocked from all communication                | `eql`, `not_eql`                                                                         |
| `lastchange`      | Last modification date                        | `eql`, `ext`, `not_ext`, `range`, `lte`, `gte`                                           |
| `user_id`         | ID of last user who edited the contact        | `eql`, `not_eql`, `ext`, `not_ext`                                                       |
| `user_contact_id` | ID of the linked Qomon user                   | `ext`, `not_ext`                                                                         |
| `action_ids`      | IDs of linked actions                         | `eql`, `not_eql`                                                                         |
| `nationbuilderid` | Legacy NationBuilder identifier               | `eql`, `not_eql`, `ext`, `not_ext`                                                       |
| `external_id`     | Your own system's identifier for this contact | `eql`, `not_eql`, `ext`, `not_ext`                                                       |

### Address fields

| Attribute                | Meaning                    | Operators                                                          |
| ------------------------ | -------------------------- | ------------------------------------------------------------------ |
| `address.city`           | City                       | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |
| `address.postalcode`     | Postal code                | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |
| `address.country`        | Country                    | `eql`, `not_eql`, `ext`, `not_ext`                                 |
| `address.street`         | Street name                | `eql`, `not_eql`, `ext`, `not_ext`, `start_with`, `not_start_with` |
| `address.housenumber`    | House number               | `eql`, `not_eql`, `ext`, `not_ext`                                 |
| `address.building`       | Building                   | `eql`, `not_eql`, `ext`, `not_ext`                                 |
| `address.floor`          | Floor                      | `eql`, `not_eql`, `ext`, `not_ext`                                 |
| `address.door`           | Door                       | `eql`, `not_eql`, `ext`, `not_ext`                                 |
| `address.addition`       | Address complement         | `eql`, `not_eql`, `ext`, `not_ext`                                 |
| `address.pollingstation` | Polling station            | `eql`, `not_eql`, `any_of`, `none_of`, `ext`, `not_ext`            |
| `address.invalid`        | Address flagged as invalid | `eql`, `not_eql`, `ext`, `not_ext`                                 |

### Financial fields

<Info>
  These fields match contacts who have **at least one** matching record. For example, `transactions.amount gte "50"` returns contacts with at least one transaction ≥ €50, not contacts whose total is ≥ €50 — use `sum_amount` for totals. **Amount fields are in euros and must always be passed as strings:** `"value": "50"` not `"value": 50`.
</Info>

**Transactions** (`transactions.*`)

| Attribute                          | Meaning                                  | Operators                                                                                      |
| ---------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `transactions.created_at`          | Record creation date                     | `lte`, `gte`, `range`                                                                          |
| `transactions.date`                | Transaction execution date               | `eql`, `not_eql`, `lte`, `gte`, `range`                                                        |
| `transactions.amount`              | Amount of one transaction (€)            | `eql`, `not_eql`, `lte`, `gte`, `range`                                                        |
| `transactions.sum_amount`          | Total amount across all transactions (€) | `eql`, `not_eql`, `lte`, `gte`, `range`                                                        |
| `transactions.count_amount`        | Number of transactions                   | `eql`, `not_eql`, `lte`, `gte`, `range`                                                        |
| `transactions.code_campaign`       | Campaign code                            | `ext`, `not_ext`, `eql`, `not_eql`, `contains`, `not_contains`, `start_with`, `not_start_with` |
| `transactions.payment_method_kind` | Payment method                           | `eql`, `not_eql`                                                                               |
| `transactions.status_id`           | Transaction status ID                    | `eql`, `not_eql`                                                                               |

**Donations** (`donations.*`)

| Attribute                     | Meaning                    | Operators                               |
| ----------------------------- | -------------------------- | --------------------------------------- |
| `donations.created_at`        | Record creation date       | `eql`, `not_eql`, `lte`, `gte`, `range` |
| `donations.transaction_date`  | Donation operation date    | `eql`, `not_eql`, `lte`, `gte`, `range` |
| `donations.amount`            | Amount of one donation (€) | `eql`, `not_eql`, `lte`, `gte`, `range` |
| `donations.sum_amount`        | Total donated (€)          | `eql`, `not_eql`, `lte`, `gte`, `range` |
| `donations.count_amount`      | Number of donations        | `eql`, `not_eql`, `lte`, `gte`, `range` |
| `donations.donation_price_id` | Donation type/tier ID      | `eql`, `not_eql`                        |

**Memberships** (`memberships.*`)

| Attribute                         | Meaning                                                     | Operators                                 |
| --------------------------------- | ----------------------------------------------------------- | ----------------------------------------- |
| `memberships.created_at`          | Record creation date                                        | `eql`, `not_eql`, `lte`, `gte`, `range`   |
| `memberships.start_date`          | Membership start date                                       | `eql`, `not_eql`, `lte`, `gte`, `range`   |
| `memberships.end_date`            | Membership end date                                         | `eql`, `not_eql`, `lte`, `gte`, `range`   |
| `memberships.transaction_date`    | Membership operation date                                   | `eql`, `not_eql`, `lte`, `gte`, `range`   |
| `memberships.period`              | Active year — e.g. `"2024"` means active that calendar year | `eql`, `not_eql`, `lte`, `gte`, `range`   |
| `memberships.multiple_in_period`  | Had more than one membership in a given year                | `eql` (value = year string e.g. `"2024"`) |
| `memberships.membership_price_id` | Membership type/tier ID                                     | `eql`, `not_eql`                          |

### Email engagement fields

<Info>
  These match contacts who have at least one email record where that field is set. A contact with no email history will not match `ext` or `not_ext`.
</Info>

| Attribute                            | Meaning                                | Operators                                                                    |
| ------------------------------------ | -------------------------------------- | ---------------------------------------------------------------------------- |
| `emails.created_at`                  | Email record date                      | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.type`                        | Email type                             | `ext`, `not_ext`, `eql`, `not_eql`                                           |
| `emails.sender`                      | Sender address                         | `ext`, `not_ext`, `eql`, `not_eql`                                           |
| `emails.subject`                     | Email subject line                     | `ext`, `not_ext`, `contains`, `not_contains`, `start_with`, `not_start_with` |
| `emails.user_id`                     | ID of Qomon user who sent the campaign | `ext`, `not_ext`, `eql`, `not_eql`                                           |
| `emails.campaign_id`                 | Campaign ID                            | `ext`, `not_ext`, `eql`, `not_eql`                                           |
| `emails.campaign_name`               | Campaign name                          | `ext`, `not_ext`, `contains`, `not_contains`, `start_with`, `not_start_with` |
| `emails.campaign_goal`               | Campaign goal                          | `ext`, `not_ext`, `eql`, `not_eql`                                           |
| `emails.campaign_is_successful`      | Campaign marked successful             | `ext`, `not_ext`, `eql`, `not_eql`                                           |
| `emails.campaign_performance_rating` | Campaign performance score (0–100)     | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.template_id`                 | Template ID                            | `ext`, `not_ext`, `eql`, `not_eql`                                           |
| `emails.template_name`               | Template name                          | `ext`, `not_ext`, `contains`, `not_contains`, `start_with`, `not_start_with` |
| `emails.delivered_at`                | Email delivered date                   | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.opened_at`                   | Email opened date                      | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.clicked_at`                  | Email clicked date                     | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.bounced_at`                  | Email bounced date                     | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.unsubscribed_at`             | Unsubscribed date                      | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.blacklisted_at`              | Blacklisted date                       | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.abuse_report_at`             | Abuse report date                      | `ext`, `not_ext`, `lte`, `gte`, `range`                                      |
| `emails.clicked_urls`                | A URL that was clicked                 | `ext`, `not_ext`, `eql`, `not_eql`, `contains`, `start_with`                 |

### Sanctions fields

`sanctions.validity` is a computed field — it does not store a date, it tells you whether a sanction is currently active: `"ongoing"` (started and not yet ended) or `"ended"` (end date is in the past).

| Attribute              | Meaning                  | Operators                                                 |
| ---------------------- | ------------------------ | --------------------------------------------------------- |
| `sanctions.created_at` | Sanction creation date   | `eql`, `not_eql`, `lte`, `gte`, `range`                   |
| `sanctions.start_date` | Sanction start date      | `eql`, `not_eql`, `lte`, `gte`, `range`                   |
| `sanctions.end_date`   | Sanction end date        | `eql`, `not_eql`, `ext`, `not_ext`, `lte`, `gte`, `range` |
| `sanctions.status`     | Sanction status          | `eql`, `not_eql`, `contains`, `not_contains`              |
| `sanctions.validity`   | `"ongoing"` or `"ended"` | `eql`                                                     |

### Custom fields and form answers

These attributes use a different condition shape — you must also provide `form_id` and `form_ref_ids`:

| Attribute       | What it searches                                                  | Operators                                                                             |
| --------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `custom_fields` | Custom fields attached to contacts                                | `ext`, `not_ext`, `eql`, `not_eql`, `contains`, `not_contains`, `lte`, `gte`, `range` |
| `form`          | Survey, consent, level of support, presence, or task form answers | `ext`, `not_ext`, `eql`, `not_eql`, `contains`, `not_contains`, `lte`, `gte`, `range` |

```json theme={"system"}
{
  "$condition": {
    "attr": "custom_fields",
    "ope": "eql",
    "form_id": 1001,
    "form_ref_ids": [2001]
  }
}
```

Retrieve your `form_id` values from `GET /v1/forms/type/{type}` and `form_ref_ids` from `GET /forms/{id}`.

<Info>
  `ext` / `not_ext` only check whether the contact has filled in the given `form_id` — `form_ref_ids` is ignored. `eql` without a `value` checks that all listed `form_ref_ids` are present. `eql` with a `value` additionally checks that `value` matches the stored answer.
</Info>
