---
updatedAt: 2026-10-14T12:00:57.000Z
agentTools:
  projectIndex: https://developers.vainu.com/llms.txt
---

# Filtering Company Data

Core concepts of Vainu's Query Language (VQL)

Filter and retrieve company data from Vainu's Nordic business databases using the Organizations and Domains API endpoints.

## Overview using a simple example

This example demonstrates the simplest way to query the API. In the payload we define "what companies we want to find" and "what data-points are returned".

```text Vainu CLI
vainu organizations --payload '{
  "query": {"?GTE": {"financial_data.revenue": 1000000}},
  "database": "FI",
  "fields": [
    "business_id",
    "name",
    "website",
    "business_units.visiting_address.street",
    "business_units.visiting_address.city",
    "business_units.visiting_address.postal_code"
  ],
  "limit": 1
}'
```
```python
import requests
payload = {
    # What companies are you searching for?
    "query": {"?GTE": {"financial_data.revenue": 1000000}},
    # In what country:
    "database": "FI",
    # What datapoints should be returned:
    "fields": [
        "business_id",
        "name",
        "website",
        "company_name",
        "business_units.visiting_address"
    ],
    # How many results:
    "limit": 1
}
response = requests.post(
    "https://api.vainu.io/api/v3/organizations/",
    headers={"Authorization": "Bearer YOUR_ACCESS_TOKEN"},
    json=payload
)
print(response.json())
```

<br />

<Recipe slug="simple-filtering-example-using-v3organizations" title="Simple Filtering Example Using Organizations API" />

### `query`

What companies to get. In this example we get the companies in Finland with latest reported revenue greater than or equal to 1000000 EUR (Finnish local currency).

```python Every query in Vainu's Query Language follows a consistent structure
"query": {
  "?OPERATOR": {
    "field_name": value
  }
}
```

### `fields`

What data is returned with the companies that match query. Check

<https://vainu.app/data-catalogue> for all the options.

### `database`

Specifies which country to search (`FI` for Finland, `SE` for Sweden, `NO` for Norway, `DK` for Denmark, or `NL` for the Netherlands).

### `limit`

Number of results returned — set to `1` here since we're looking up a single company..

## API Endpoints

| Endpoint                                                | Description                                                                                                                                                                   |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST https://api.vainu.io/api/v3/organizations/`       | Filter and retrieve company data by business registry (country-specific)                                                                                                      |
| `POST https://api.vainu.io/api/v3/organizations/count/` | Get result count without returning data. Payload is identical to sync Organizations API. fields/offset/limit are ignored.                                                     |
| `POST https://api.vainu.io/api/v3/organizations/async/` | To get a lot of data use async. Payload is identical to sync Organizations API: \[<https://api.vainu.io/api/v3/organizations/>`](https://api.vainu.io/api/v3/organizations/`) |

## Query Structure

Every request body is a JSON object with these parameters:

| Parameter                   | Type    | Required                 | Default | Description                                                 |
| --------------------------- | ------- | ------------------------ | :------ | ----------------------------------------------------------- |
| `query`                     | object  | Yes                      |         | Filter conditions using query operators                     |
| `fields`                    | array   | Yes                      |         | List of field names to include in the response              |
| `database`                  | string  | Yes (organizations only) |         | Country database code: `FI`, `SE`, `NO`, `DK`, `NL`         |
| `limit`                     | integer | No                       |         | Maximum results to return                                   |
| `offset`                    | integer | No                       | 0       | Number of results to skip (for pagination)                  |
| `order`                     | string  | No                       | null    | Field name to sort by. Prefix with `-` for descending order |
| `is_active`                 | boolean | No                       | yes     | Filter for active/inactive companies                        |
| `unwind_subdocument`        | string  | No                       | null    | Subdocument field to unwind (flatten) into separate rows    |
| `unwind_subdocument_query`  | object  | No                       | null    | Filter conditions applied to the unwound subdocument        |
| `unwind_subdocument_limit`  | integer | No                       | null    | Maximum unwound rows to return                              |
| `unwind_subdocument_offset` | integer | No                       | null    | Number of unwound rows to skip                              |
| `aggregation`               | object  | No                       | null    | Aggregation pipeline configuration                          |
| `lookup`                    | object  | No                       | null    | Lookup (join) configuration                                 |

## Query Operators

Operators define how to match values. They start with `?` followed by the operator name:

### Logical Operators

| Operator | Description                            | Example                                 |
| -------- | -------------------------------------- | --------------------------------------- |
| `?ALL`   | AND — all conditions must match        | `{"?ALL": [condition1, condition2]}`    |
| `?ANY`   | OR — at least one condition must match | `{"?ANY": [condition1, condition2]}`    |
| `?NOT`   | Negates a condition                    | `{"?NOT": {"?EQ": {"field": "value"}}}` |

### Comparison Operators

<Table align={["left","left","left"]}>
  <thead>
    <tr>
      <th>
        Operator
      </th>

      <th>
        Description
      </th>

      <th>
        Example
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `?EQ`
      </td>

      <td>
        Equal to
      </td>

      <td>
        `{"?EQ": {"financial_data.employees.absolute_count": 50}}`
      </td>
    </tr>

    <tr>
      <td>
        `?GT`
      </td>

      <td>
        Greater than
      </td>

      <td>
        `{"?GT": {"financial_data.employees.absolute_count": 50}}`
      </td>
    </tr>

    <tr>
      <td>
        `?GTE`
      </td>

      <td>
        Greater than or equal to
      </td>

      <td>
        `{"?GTE": {"financial_data.employees.absolute_count": 50}}`
      </td>
    </tr>

    <tr>
      <td>
        `?LT`
      </td>

      <td>
        Less than
      </td>

      <td>
        `{"?LT": {"financial_data.employees.absolute_count": 50}}`
      </td>
    </tr>

    <tr>
      <td>
        `?LTE`
      </td>

      <td>
        Less than or equal to
      </td>

      <td>
        `{"?LTE": {"financial_data.employees.absolute_count": 50}}`
      </td>
    </tr>

    <tr>
      <td>
        `?IN`
      </td>

      <td>
        Value is in a list
      </td>

      <td>
        `{"?IN": {"status": ["active", "pending"]}}`
      </td>
    </tr>

    <tr>
      <td>
        `?RANGE`
      </td>

      <td>
        Value is within `[min, max]`
      </td>

      <td>
        `{"?RANGE": {"financial_data.revenue": [1000000, 5000000]}}`

        `{"?RANGE": {"financial_data.revenue": [1000000, null]}} `identical to`{"?GTE": {"financial_data.revenue": 1000000}}`
      </td>
    </tr>
  </tbody>
</Table>

### String Operators

| Operator      | Description                           | Example                               |
| ------------- | ------------------------------------- | ------------------------------------- |
| `?CONTAINS`   | Contains substring (case-sensitive)   | `{"?CONTAINS": {"name": "tech"}}`     |
| `?ICONTAINS`  | Contains substring (case-insensitive) | `{"?ICONTAINS": {"name": "TeCH"}}`    |
| `?STARTSWITH` | Starts with value (case-sensitive)    | `{"?STARTSWITH": {"name": "Nordic"}}` |
| `?ENDSWITH`   | Ends with value                       | `{"?ENDSWITH": {"domain": ".no"}}`    |

### `?STARTSWITH` allows searching with either a single string or a list of values.

#### Search companies that use any Salesforce technologies:

`{"?STARTSWITH": {"technology_data.name": "Salesforce"}}`

#### Search companies that use any Salesforce or HubSpot technologies:

`{"?STARTSWITH": {"technology_data.name": ["Salesforce", "HubSpot"]}}`

### Special Operators

| Operator  | Description                                                                                                                               | Example                                                 |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `?MATCH`  | Matches conditions within a subdocument array (e.g., contacts, addresses). All conditions inside must match the *same* subdocument entry. | See [Contact Search](#example-3-complex-contact-search) |
| `?EXISTS` | Check if a field exists                                                                                                                   | `{"?EXISTS": {"website": true}}`                        |

## Matching Subdocuments

Several Vainu fields are **arrays of subdocuments** — for example `business_units`, `contacts`, and `vehicles`. Each element is its own object with its own fields (a business unit has its own `industry`, `visiting_address.city`, `types`, and so on). How your conditions combine across those elements depends on how you write the query.

### Default: conditions match across the whole array

When you put conditions on subdocument fields using dotted paths (`business_units.industry`, `business_units.visiting_address.city`), each condition is matched **independently against the array**. A company matches as long as *some* element satisfies each condition — but they don't have to be the *same* element.

```python
# Matches a company that has ANY unit in the SaaS industry
# AND ANY unit located in Helsinki — possibly two different units.
"query": {
    "?ALL": [
        {"?EQ": {"business_units.industry": "SaaS"}},
        {"?EQ": {"business_units.visiting_address.city": "Helsinki"}}
    ]
}
```

Use this when you want a broad footprint across the company and don't care whether a single unit covers everything.

### `?MATCH`: all conditions on the same subdocument

Wrap the conditions in `?MATCH` to require that **one single element** of the array satisfies all of them at once (this compiles to MongoDB's `$elemMatch`).

```python
# Matches only if the SAME business unit is both in SaaS AND in Helsinki.
"query": {
    "?MATCH": {
        "business_units": {
            "?ALL": [
                {"?EQ": {"industry": "SaaS"}},
                {"?EQ": {"visiting_address.city": "Helsinki"}}
            ]
        }
    }
}
```

Field names inside a `?MATCH` block are relative to the subdocument (`industry`, not `business_units.industry`). `?MATCH` only decides *which companies* are returned — it does **not** remove non-matching elements from the response, so the full array is still returned in `fields`. `?MATCH` blocks can be nested and combined with `?NOT`.

### One row per subdocument: `unwind_subdocument`

By default the API returns one row per company. To get one row per matching subdocument instead — e.g. to export at the business-unit level — use the top-level `unwind_subdocument` payload parameters (these are request parameters, **not** a query operator):

* `unwind_subdocument`: the array field to expand, e.g. `"business_units"`
* `unwind_subdocument_query`: a VQL query applied to each subdocument; only matching subdocuments produce rows
* `unwind_subdocument_limit` / `unwind_subdocument_offset`: paginate the expanded rows

```python
payload = {
    "query": {"?EQ": {"business_units.visiting_address.city": "Helsinki"}},
    "database": "FI",
    "fields": ["business_id", "name", "business_units"],
    # Expand: one row per matching business unit
    "unwind_subdocument": "business_units",
    "unwind_subdocument_query": {"?EQ": {"visiting_address.city": "Helsinki"}},
    "limit": 50
}
```

A company with three Helsinki units produces three rows, each carrying a single unit.

### Pruning subdocuments in the response: `?FILTER_SUBDOCUMENTS`

`?FILTER_SUBDOCUMENTS` keeps one row per company but **strips out subdocuments that don't match**, so the company record shows only the relevant elements. It is a stage of the **Vainu aggregation language** — a separate pipeline mechanism, not a `query` operator — so it is supplied as an aggregation stage (a list of stages) and takes a `path` and a `query`:

```python
{
    "?FILTER_SUBDOCUMENTS": {
        "path": "business_units",
        "query": {"?EQ": {"visiting_address.city": "Helsinki"}}
    }
}
```

A company with two Helsinki units and one Stockholm unit is returned once, with only the two Helsinki units in the payload. The aggregation language has additional stages (for grouping, counting, and reshaping data); `?FILTER_SUBDOCUMENTS` is the one relevant to subdocument matching.

### Quick reference

| Approach                             | Rows returned                | All conditions on the same element?  | Non-matching subdocs removed from payload? |
| ------------------------------------ | ---------------------------- | ------------------------------------ | ------------------------------------------ |
| Default (dotted paths)               | One per company              | No                                   | No                                         |
| `?MATCH`                             | One per company              | Yes                                  | No                                         |
| `unwind_subdocument` parameter       | One per matching subdocument | Yes (via `unwind_subdocument_query`) | Yes (only matching subdocs shown)          |
| `?FILTER_SUBDOCUMENTS` (aggregation) | One per company              | Yes                                  | Yes                                        |

## Examples

<br />

### Example 2: Software Companies OR Consulting Firms with Revenue 1M-10M

This example demonstrates mixing ?ALL and ?AND for more complex query logic:

```python
payload = {
    "query": {
        "?ALL": [
            {
                "?ANY": [
                    {"?EQ": {"official_industries.code": ["62"]}},  # Software
                    {"?EQ": {"official_industries.code": ["63"]}},  # IT Services
                    {"?EQ": {"official_industries.code": ["70"]}}   # Consulting
                ]
            },
            {"?RANGE": {"financial_data.revenue": [1000000, 10000000}]}  # Revenue >= 1M
        ]
    },
    "database": "FI",
    "fields": ["business_id", "name", "financial_data", "official_industries"],
    "limit": 100,
}
response = requests.post(organizations_endpoint, headers=headers, json=payload)
print(response.json())


```
```text Vainu CLI
vainu organizations --payload '{
  "query": {
    "?ALL": [
      {"?EQ": {"official_industries.code": ["62", "63", "70"]}},
      {"?RANGE": {"financial_data.revenue": [1000000, 10000000]}}
    ]
  },
  "database": "FI",
  "fields": ["business_id", "name", "financial_data.revenue", "official_industries.code"],
  "limit": 100
}'
```

### Example 3: Complex Contact Search

Find companies with C-level contacts matching specific criteria. The `?MATCH` operator ensures all conditions apply to the *same* contact.  Without`?MATCH` it's possible to match to company where one contact has first condition and another contact has second condition.

```text Vainu CLI
vainu organizations --payload '{
    "query": {
      "?ALL": [
        {"?MATCH": {
          "contacts": {
            "?ALL": [
              {"?CONTAINS": {"titles.title": "CEO"}},
              {"?ENDSWITH": {"email": ".com"}}
            ]
          }
        }}
      ]
    },
    "fields": [
      "business_id",
      "name",
      "contacts.full_name",
      "contacts.email",
      "contacts.titles"
    ],
    "database": "FI",
    "limit": 20
  }'
```
```python
payload = {
    "query": {
        "?ALL": [
            {"?MATCH": {
                "contacts": {
                    "?ALL": [
                        {"?CONTAINS": {"titles.title": "CEO"}},
                        {"?ENDSWITH": {"email": ".com"}}
                    ]
                }
            }}
        ]
    },
    "fields": [
        "business_id",
        "name",
        "contacts.full_name",
        "contacts.email",
        "contacts.titles"
    ],
    "database": "FI",
    "limit": 20
}

response = requests.post(organizations_endpoint, headers=headers, json=payload)
print(response.json())
```

### Example 4: Location-Based Search with Multiple Conditions

Combine location, industry, and employee count filters. This query finds technology companies with main location from the official registry in Helsinki with more than 50 employees.

```text Vainu CLI
vainu organizations --payload '{
    "query": {
      "?ALL": [
        {"?GTE": {"financial_data.employees.absolute_count": 10}},
        {"?MATCH": {
          "business_units": {
            "?ALL": [
              {"?EQ": {"visiting_address.city": "Helsinki"}},
              {"?EQ": {"types": "registry_main_location"}}
            ]
          }
        }},
        {"?EQ": {"official_industries.code": "56111"}}
      ]
    },
    "fields": [
      "business_id",
      "name",
      "financial_data.employees.absolute_count",
      "visiting_address.city",
      "website"
    ],
    "database": "FI",
    "limit": 20
  }'
```
```python
payload = {
    "query": {
        "?ALL": [
            # More that 10 employees:
            {"?GTE": {"financial_data.employees.absolute_count": 10}},
            # Business unit with any type in Helsinki (56111)
            {"?MATCH": {
                "business_units": {
                    "?ALL": [
                        {"?EQ": {"visiting_address.city": "Helsinki"}},
                        {"?EQ": {"types": "registry_main_location"}}
                    ]
                }
            }},
            # Restaurants and cafeterias (56111)
            {"?EQ": {"official_industries.code": "56111"}},
        ]
    },
    "fields": [
        "business_id",
        "name",
        "staff_number",
        "prospect_addresses.visiting_city",
        "website"
    ],
    "database": "FI",
    "limit": 20
}

response = requests.post(organizations_endpoint, headers=headers, json=payload)
print(response.json())
```

### Example 5: Financial Data Filtering with Sort Order

Query companies by multiple financial metrics and sort results by revenue descending. Use `null` in a `?RANGE` to leave one bound open (e.g., revenue above 1M with no upper limit).

```text Vainu CLI
vainu organizations --payload '{
  "query": {
    "?ALL": [
      {"?RANGE": {"financial_data.revenue": [100000, null]}},
      {"?GT": {"financial_data.profit": 100}},
      {"?RANGE": {"financial_data.employee_count": [10, 100]}}
    ]
  },
  "fields": [
    "business_id",
    "name",
    "financial_data.revenue",
    "financial_data.profit",
    "financial_data.employee_count",
    "financial_statements"
  ],
  "database": "FI",
  "limit": 20
}'
```
```python
payload = {
    "query": {
        "?ALL": [
            {"?RANGE": {"financial_data.revenue": [100000, None]}},
            {"?GT": {"financial_data.profit": 100}},
            {"?RANGE": {"financial_data.employee_count": [10, 100]}}
        ]
    },
    "fields": [
        "business_id",
        "name",
        "financial_data.revenue",
        "financial_data.profit",
        "financial_data.employee_count",
        "financial_statements"
    ],
    "database": "FI",
    "order": "-financial_data.revenue",
    "limit": 20
}

response = requests.post(organizations_endpoint, headers=headers, json=payload)
print(response.json())
```

<br />

### Example 6: Finding companies that mention "AI Agents" on their website

`website_data.keywords` stores n-grams (shingles) of phrases and keywords found on a company's website.

<Cards>
  <Card title="What are n-grams?" icon="fa-rocket">
    In this context n-grams are single keywords or multi word phrases found in the website. In website_data.keywords, each token can contain up to 4 words. For example, you can search for companies that have "Company Data is Fun" on their website, but you cannot search for "Company Data is Fun and Interesting".
  </Card>
</Cards>

### Features of `website_data.keywords` data point:

* Always case-insensitive
* Maximum n-gram size is 40 characters
* Maximum n-gram length is 4 words
* Minimum n-gram length is 4 characters (For example "AI" is not included but "AI Agent" is)
* All non-ASCII characters are removed except for `"ä ö å æ ø"`

### Full example

This example searches for companies in Sweden that mention any of these phrases on their website: `"AI Agent"`, `"Autonomous Agent"`, `"Agentic AI"`, `"Agentic Workflows"`, or `"AI Workforce"`.

In other words any of the n-grams we found int the website starts with these phrases.

```python
payload = {
    "query": {
        "?ALL": [
            {"?STARTSWITH": {"website_data.keywords": ["AI Agent", "Autonomous Agent", "Agentic AI", "Agentic Workflows", "AI Workforce"]}}
        ]
    },
    "fields": [
        "business_id",
        "name",
    ],
    "database": "SE",
    "limit": 20
}

response = requests.post(organizations_endpoint, headers=headers, json=payload)
print(response.json())
```

### Example 7: Search for inactive companies that have changed status in past 30 days

Search for latest inactive companies that have changes official status value.

Payload supports all the same logic but includes:  `"is_active": False`.

<Callout icon="🚤" theme="warning">
  ### Tip&#x20;

  For speed use the same field in the query and sort!
</Callout>

```text Vainu CLI
vainu organizations --payload '{
  "query": { "?GTE": { "modifications.status": "2026-08-11T00:00:00" } },
  "database": "SE",
  "fields": ["business_id", "name", "official_status", "is_active", "founded"],
  "is_active": false,
  "order": "-modifications.status",
  "limit": 20
}'
```
```python
from datetime import datetime, timedelta

thirty_days_ago = (datetime.now() - timedelta(days=30)).isoformat()
response = requests.post(
    "https://api.vainu.io/api/v3/organizations/",
    json={
        "query":  {"?GTE": {"modifications.status": thirty_days_ago}},
        "database": "SE",
        "fields": ["business_id", "name", "official_status", "is_active", "founded"],
        "is_active": False,
        "order": "-modifications.status",
        "limit": 20,
    },
    headers={"Authorization": f"Bearer {access_token}"},
)
print(response.json())
```

## Advanced Features

### Vehicle Data Search

<Callout icon="🔒" theme="warning">
  Vehicle data fields require special API permissions. Contact Vainu support to enable access.
</Callout>

Find companies with specific vehicle types in their fleet. `?MATCH` here ensures that the same vehicle matches all the conditions.

```text Vainu CLI
vainu organizations --payload '{
  "query": {
    "?ALL": [
      { "?MATCH": {
          "vehicles": {
            "?ALL": [
              { "?EQ": { "vehicle_class": "N1" } },
              { "?EQ": { "brand_human_readable": "Volvo" } }
            ]
          }
      }}
    ]
  },
  "fields": ["business_id", "name", "vehicles"],
  "database": "SE",
  "limit": 10
}'
```
```text Python
payload = {
    "query": {
        "?ALL": [
            {"?MATCH": {
                "vehicles": {
                    "?ALL": [
                        {"?EQ": {"vehicle_class": "N1"}},
                        {"?EQ": {"brand_human_readable": "Volvo"}}
                    ]
                }
            }}
        ]
    },
    "fields": [
        "business_id",
        "name",
        "vehicles"
    ],
    "database": "SE",
    "limit": 10
}

response = requests.post(organizations_endpoint, headers=headers, json=payload)
print(response.json())
```

### List Filtering

Lists can be created in the <Anchor target="_blank" href="https://vainu.app">Vainu Platform</Anchor>  To get all the companies in your vainu list using API with selected datapoints.

```text Vainu CLI
vainu organizations --format jsonl --payload '{
  "list": "CHANGE-ME-TO-YOUR-LIST-ID",
  "fields": [
    "business_id",
    "name",
    "website"
  ],
  "limit": 20
}'
```
```python
payload = {
    "list": LIST_ID
    "fields": [
        "business_id",
        "name",
        "website"
    ],
    "limit": 20
}
response = requests.post(organizations_endpoint, headers=headers, json=payload)
print(response.json())
```

### Include target group as a part of your query:

```python
modifications_range = [datetime.datetime.now()-datetime.timedelta(days=7), datetime.datetime.now()]
payload = {
    "query": {
        "?ALL": [
            {"?IN": {"target_group._id": [YOUR_VAINU_LIST_ID]}},
            {
                "?ANY": [
                    {"?RANGE": {"modifications.basic": modifications_range}},
                    {"?RANGE": {"modifications.financial_statements": modifications_range}},
                ],
            },
        ]
    },
    "fields": [
        "business_id",
        "name",
        "website"
    ],
    "database": "FI",
    "limit": 20
}

response = requests.post(organizations_endpoint, headers=headers, json=payload)
print(response.json())
```

### Exporting Data in Different Formats

Append a `format` query parameter to the endpoint URL to receive results as CSV, JSONL, or XLSX instead of the default JSON.

```python
# Export as CSV
csv_response = requests.post(
    f"{organizations_endpoint}?format=csv",
    headers=headers,
    json=payload
)

# Export as XLSX
xlsx_response = requests.post(
    f"{organizations_endpoint}?format=xlsx",
    headers=headers,
    json=payload
)

# Export as JSON Lines (one JSON object per line)
jsonl_response = requests.post(
    f"{organizations_endpoint}?format=jsonl",
    headers=headers,
    json=payload
)
```

<br />

## Important Notes

<Callout icon="❗️" theme="error">
  - Some fields (e.g., vehicle data) require special API permissions.
    - Rate limits apply to all endpoints. Avoid excessive concurrent requests.
    - Always specify the correct `database` code (`FI`, `SE`, `NO`, `DK`, `NL`) matching the country of the companies you're querying.
    - For large datasets (10,000+ results), use the async endpoints to avoid request timeouts.
    - Contact Vainu support for questions about field availability and permission requirements.
</Callout>