# Instasent - product-api (full documentation) > Autocontained dump of every page under /product-api. Paste this into an AI assistant or feed it to an agent as context for product-api integration work. Source: https://docs.instasent.com/ OpenAPI spec: https://docs.instasent.com/openapi/product.openapi.yaml --- URL: https://docs.instasent.com/product-api/guide # Guide The Product API is Instasent's primary management API: organizations, projects, data sources, unified audiences, events, segments, campaigns, automations and direct messaging, all under one token. The Product API is the control plane for everything a brand runs on Instasent. It manages the **organization** and its **projects**, feeds contacts and events into **data sources**, queries the **unified audience** that results from merging those sources, reads **segments**, **campaigns** and **automations**, and sends **direct SMS** to specific contacts when your application needs to trigger a message outside a campaign. If you are looking for raw message throughput — OTPs, receipts, transactional SMS — see the [Transactional API](/transactional-api/overview). If you only need to push customer data into a project, the [Ingest API](/ingest-api/guide) is a focused subset of this one with its own tokens. > **Note**: You need a **Product API token** and your **project UID** to call these endpoints. Both live under **Project settings** in the [dashboard](https://dashboard.instasent.com). ## What you can do The Product API gives you, under a single token: - **Organizations & projects** — read organization info, open projects and manage project-level settings. - **Data sources** — create and manage API data sources as entry points for customer data. - **Unified audience** — query, search and retrieve contacts from the audience that results from merging every data source in the project. - **Events** — search and analyse audience events to understand customer behaviour. - **Segments** — list static and dynamic segments and scroll their contacts. - **Campaigns & automations** — read campaign and automation details, configurations and status. - **Direct messaging** — send direct SMS to specific audience contacts and manage SMS senders. - **Ingest (built-in)** — push contacts and events into data sources using the same endpoints the [Ingest API](/ingest-api/guide) exposes. ## Core entities Five nouns keep reappearing in every endpoint. Getting them right up front saves pain later. - **Organization** — the company on Instasent. Owns billing, user accounts, API tokens and a set of projects. - **Project** — an isolated environment inside an organization. Holds its own data sources, audience, segments, campaigns and automations. Used to split brands, markets or customer segments. - **Data source** — a stream of contacts and events that feeds into a project. A project can have many. Each has its own contacts and events; merging happens at the audience level. - **Contact** — a person. Exists in two forms: - **Datasource contact** — the original record as it arrived in a specific data source. - **Audience contact** (also called the *unified audience*) — the merged view across every data source in the project. - **Event** — an immutable record of a customer interaction (purchase, view, opt-in, any custom activity). Events enrich contact profiles and can trigger automations. ## How contacts merge into the unified audience The unified audience is the product of merging every data source in the project by the **merging attributes** configured on that project. - **Primary merging attribute**: `_user_id`. Contacts across data sources with the same `_user_id` become a single audience contact. - **Additional merging attributes**: a project can configure extras (typically `_email`, `_phone_mobile`). Any matching value triggers a merge. - **Attribute consolidation**: when multiple data sources disagree about the same attribute, the project's priority rules pick the winner. The [Ingest API](/ingest-api/guide) datasource takes precedence by default. - **Event aggregation**: every event from the merged contacts hangs off the unified audience contact, so you see one timeline per customer regardless of the source. - **Source tracking**: the audience contact keeps references to every datasource contact it was built from, via the `_datasources` and `_ds_contact_ids` attributes. That lets you trace any value back to its origin. For example: a contact with `_user_id: "12345"` that exists in both your CRM and your e-commerce data sources becomes a single audience contact whose attributes combine both and whose event timeline merges both histories. ## Discovering what's in a project The attributes a contact can carry and the event types you can filter on are **project-specific** — they depend on how the project is configured and which datasources feed it. Rather than maintain a static catalogue here, the API exposes three discovery endpoints that always return the live shape of your project: - `GET /v1/project/{project}/specs/attributes` — every attribute enabled on contacts in this project: its `uid`, `dataType`, whether it is `unique` (used for merging), `custom`, `readonly`, `eventBased`, etc. - `GET /v1/project/{project}/specs/events` — every event type available in this project, including category, attribution and automation flags. - `GET /v1/project/{project}/specs/events/{eventType}` — the parameter schema for a specific event type: `parameter` key, `dataType`, `required`, `maxLength`, `multiValue`. Use these to validate payloads before writing, to build dynamic UIs on top of the audience, or just to discover which event types (`ecommerce_order_create`, `appointment`, custom ones…) and parameters are in play. See the [API Reference](/product-api/reference) for response shapes. ## Tokens and scopes Access is controlled by **token scopes**. A token is minted for an organization or project and carries only the scopes you grant it — a read-only reporting token looks nothing like the write token behind your CRM sync. ### Datasource management - `PROJECT_DATASOURCE_READ` — read-only access to data sources. - `PROJECT_DATASOURCE_WRITE` — create and modify data sources. ### Organization - `ACCOUNT_READ` — read organization account details, including funds. ### Audience - `PROJECT_AUDIENCE_READ` — read individual audience contacts. - `PROJECT_AUDIENCE_WRITE` — write to audience contacts. - `PROJECT_AUDIENCE_LIST` — list audience contacts (scroll/search). *Requires a specific subscription plan.* - `PROJECT_AUDIENCE_DATA_BASIC` — access to basic contact data. *Requires a specific subscription plan.* - `PROJECT_AUDIENCE_DATA_FULL` — access to full contact data (PII). *Must be manually granted by Instasent.* - `PROJECT_AUDIENCE_DATA_EVENTS` — access to audience events. *Requires a specific subscription plan.* - `PROJECT_AGGREGATIONS` — audience and event aggregations. *Must be manually granted by Instasent.* ### Campaigns & automations - `PROJECT_CAMPAIGN_READ` — read access to campaigns. - `PROJECT_AUTOMATION_READ` — read access to automations. ### Direct messaging - `PROJECT_DIRECT_READ` — read direct SMS. - `PROJECT_DIRECT_WRITE` — create direct SMS. Required to send. ### Data privacy and plan gating The contact fields a call returns depend on **two** things: the scopes on your token **and** the subscription plan of the organization. Having the scope in the token spec is not enough — the plan has to allow it. | Level | Required scope | Returned fields | | ----------- | ----------------------------- | ----------------------------------------------- | | **Default** | `PROJECT_AUDIENCE_READ` | Full name and user id only. | | **Basic** | `PROJECT_AUDIENCE_DATA_BASIC` | Phone, email, country, name and boolean fields. | | **Full** | `PROJECT_AUDIENCE_DATA_FULL` | Full contact data including PII. | Scopes fall into three availability tiers: - **Always available**: `PROJECT_AUDIENCE_READ`, `PROJECT_AUDIENCE_WRITE`, `PROJECT_DATASOURCE_READ/WRITE`, `PROJECT_CAMPAIGN_READ`, `PROJECT_AUTOMATION_READ`, `PROJECT_DIRECT_READ/WRITE`, `ACCOUNT_READ`. - **Gated by subscription plan**: `PROJECT_AUDIENCE_LIST`, `PROJECT_AUDIENCE_DATA_BASIC`, `PROJECT_AUDIENCE_DATA_EVENTS`. These are grantable only on plans that include them — upgrade the plan from the [dashboard](https://dashboard.instasent.com) if you need them. - **Manually granted by Instasent**: `PROJECT_AUDIENCE_DATA_FULL`, `PROJECT_AGGREGATIONS`. Not generally available, reserved for trusted partners, require approval. > **Warning**: Design your integration against **Basic** access. If your plan does not include `PROJECT_AUDIENCE_DATA_BASIC`, most contact fields come back redacted to Default level regardless of what your code expects. Upgrade the plan before assuming the data is there. ### Datasource-specific tokens Every data source can also mint its own token. Those tokens are write-only, scoped to the single data source, and are the recommended path for CRM or e-commerce syncs — see [Ingest API authentication](/ingest-api/authentication). ## Multi-project architecture Projects are fully isolated. Each one keeps its own: - data sources and contacts, - unified audience and events, - segments, campaigns and automations, - SMS senders and direct messages. Use that isolation to split brands, markets or product lines without cross-contaminating audiences. A single organization can run many projects side by side. ## What to read next - [Quickstart](/product-api/quickstart) - Find a contact, read their events, send a direct SMS. - [Authentication](/product-api/authentication) - Token types, scopes and rotation. - [Audience query filter](/product-api/audience-query-filter) - Search and segment the unified audience. - [API Reference](/product-api/reference) - Every endpoint, every parameter. --- URL: https://docs.instasent.com/product-api/quickstart # Quickstart Find a contact in the unified audience, read their recent events and send a direct SMS — end to end in under five minutes. This walkthrough exercises the three things every Product API integration eventually does: locate a contact, inspect their timeline, and trigger a direct message. Budget five minutes. ## Before you start #### 1. Have a project with data Sign in to the [dashboard](https://dashboard.instasent.com) and pick a project that already has at least one audience contact (either pushed via Ingest or imported via a data source). If your project is empty, walk through the [Ingest Quickstart](/ingest-api/quickstart) first. #### 2. Create a Product API token Open **Project settings** → **API tokens** and create a token with at least these scopes: - `PROJECT_AUDIENCE_READ` - `PROJECT_AUDIENCE_DATA_BASIC` (to see phone and email on responses) - `PROJECT_AUDIENCE_DATA_EVENTS` (to read events) - `PROJECT_DIRECT_WRITE` (to send direct SMS) #### 3. Export credentials ```bash export INSTASENT_PROJECT="proj_xxx" export INSTASENT_TOKEN="eyJhbGciOi..." export BASE="https://api.instasent.com/v1/project/$INSTASENT_PROJECT" ``` ## 1. Verify the token `GET /v1/project/{project}` returns the project's metadata and is the cheapest probe for credentials. ```bash curl "$BASE" \ -H "Authorization: Bearer $INSTASENT_TOKEN" ``` A `200` confirms the token is live and the project UID resolves. A `401` means the token is wrong; a `404` means the project UID is wrong or not visible to the token. ## 2. Find an audience contact The audience exposes three search helpers for the common identifier types — user id, phone and email. They return the unified audience contact so you can then read events or send messages. ```bash curl "$BASE/audience/search/phone/%2B34600000000" \ -H "Authorization: Bearer $INSTASENT_TOKEN" ``` Response (trimmed): ```json { "entity": { "id": "uQTuHNBKLdwTxzGldW5pocUNqzyz-066", "_user_id": "USER-123", "_first_name": "Ada", "_email": "ada@example.com", "_phone_mobile": "+34600000000" } } ``` Copy the returned `id` — that is the **audience contact id** you need for the next two calls. ```bash export AUDIENCE_ID="uQTuHNBKLdwTxzGldW5pocUNqzyz-066" ``` > **Tip**: Phone numbers in the path must be URL-encoded (`+` → `%2B`). If you store user ids instead, use `/audience/user/{userId}`; for email use `/audience/search/email/{userEmail}`. ## 3. Read the contact's recent events ```bash curl "$BASE/audience/$AUDIENCE_ID/events" \ -H "Authorization: Bearer $INSTASENT_TOKEN" ``` You get the last events for that contact — purchases, message deliveries, clicks, custom events. Use this to confirm your Ingest pipeline is landing and to drive application logic that depends on the customer timeline. ## 4. Send a direct SMS The direct SMS endpoint takes the sender and audience id in the path and the text in the body. Use `"default"` as the sender id to fall back to the project's default sender. ```bash curl -X POST "$BASE/channel/sms/sms/direct/default/$AUDIENCE_ID" \ -H "Authorization: Bearer $INSTASENT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "text": "Hi Ada — your order is on its way. Track it at {{short:https://track.example.com/O-987}}" }' ``` A `201 Created` returns the SMS entity with its `id`, `status` (`enqueued` initially), `encoding`, `messagesCount`, `pricePerSms` and the `audienceId` the message was dispatched to. Status transitions are pushed to your DLR webhook the same way transactional traffic is — see [Transactional DLRs](/transactional-api/http/dlrs). > **Warning**: The direct SMS endpoint supports `{{short:URL}}` for automatic link-shortening and `{{unsubscribe}}` for an opt-out link. It is designed for individual triggered messages, not bulk campaigns — those belong in a campaign or automation. ## 5. (Optional) send by phone instead of audience id If the contact does not yet exist and your project has **outbound auto-creation** enabled, you can send straight to a phone number and let the API create the audience contact for you: ```bash curl -X POST "$BASE/channel/sms/sms/direct/default/%2B34600000000" \ -H "Authorization: Bearer $INSTASENT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "text": "Welcome to Example Co." }' ``` The response's `metadata.autoCreated` is `true` when a new audience contact was created for the message. If auto-creation is disabled and the phone number is not found, the call returns `404`. ## What to read next - [Guide](/product-api/guide) - Mental model, entities and how the unified audience is built. - [Audience query filter](/product-api/audience-query-filter) - Search and segment the unified audience. - [Authentication](/product-api/authentication) - Token types, scopes and rotation. - [API Reference](/product-api/reference) - Every endpoint, every parameter. --- URL: https://docs.instasent.com/product-api/authentication # Authentication The Product API authenticates each request with a bearer token carrying specific scopes. Tokens are issued per organization or per project in the dashboard. Every request to the Product API carries a bearer token in the `Authorization` header. Tokens are issued in the dashboard, scoped to an organization or a project, and grant only the permissions you explicitly check off when you create them. ## Two kinds of token - **Product API token** — the full-feature token. Can read and write any surface of the Product API the scopes allow: audience, events, segments, campaigns, automations, direct SMS and — because the Ingest API is a subset of Product — contacts and events in any data source of the project. - **Datasource token** — a narrower token minted for a single data source. Write-only against that data source. The right choice for CRM or e-commerce syncs that should not see anything else. See [Ingest authentication](/ingest-api/authentication). Use the widest token only where the workload actually needs it. A reporting dashboard does not need `PROJECT_AUDIENCE_WRITE`; a CRM sync does not need any read scopes. ## Getting a token #### 1. Open Project settings Sign in to the [dashboard](https://dashboard.instasent.com), pick the project that will own the token, and open **Project settings** → **API tokens**. #### 2. Pick the scopes Check only the scopes the integration needs — see the [scope list in the Guide](/product-api/guide#tokens-and-scopes). Tokens are immutable once created; if you need a different scope later, mint a new token and rotate. #### 3. Copy the token and the project UID Both are needed for every call. The token is shown once; the project UID appears on the same page and does not change. > **Warning**: Treat tokens as production secrets. Keep them in an environment variable or a secrets manager; never commit them to the repo or embed them in client-side code. `PROJECT_AUDIENCE_DATA_FULL` tokens in particular carry PII access and should live in your most restricted vault. ## Sending the token Preferred in every environment: the `Authorization` header. The token never appears in URLs, logs or browser history. ```bash curl "https://api.instasent.com/v1/project/$PROJECT" \ -H "Authorization: Bearer $INSTASENT_TOKEN" ``` A missing, malformed or revoked token returns `401 Unauthorized`. A token that is valid but lacks the scope for the endpoint returns `403 Forbidden`. ## Scopes recap Three scopes cover most day-to-day integrations: - `PROJECT_AUDIENCE_READ` + `PROJECT_AUDIENCE_LIST` + `PROJECT_AUDIENCE_DATA_BASIC` — segmentation and reporting dashboards. - `PROJECT_DATASOURCE_WRITE` + `PROJECT_AUDIENCE_WRITE` — CRM / e-commerce syncs and automation backends. - `PROJECT_DIRECT_WRITE` — triggering direct SMS from your application. See the [Guide](/product-api/guide#tokens-and-scopes) for the full list, privacy levels and the scopes that require manual grant by Instasent. ## Rotating a token Tokens do not expire. Rotate them whenever a teammate leaves, whenever a secret might have been exposed, and at least once a year as a hygiene measure. #### 1. Issue the replacement Create a new token with the same scopes **before** revoking the old one. This keeps traffic flowing while you redeploy. #### 2. Roll it out Update your secrets store and redeploy every worker that calls the Product API. #### 3. Revoke the old token Once the replacement is live everywhere, delete the old token in the dashboard. Any request still using it will fail with `401`. ## What's next - **[Rate limits](/product-api/rate-limits)** — per-plan ceilings and the `X-RateLimit-*` headers. - **[Errors](/product-api/errors)** — status codes and retry guidance. --- URL: https://docs.instasent.com/product-api/rate-limits # Rate limits The Product API is rate-limited per organization and per endpoint class. Ceilings depend on your subscription plan and can be raised by upgrading. Every Product API endpoint enforces a request-per-minute ceiling. Limits are scoped by organization and by endpoint class — hot endpoints (scroll, search, aggregations) have their own budgets so a heavy reporting job cannot starve an interactive dashboard. ## Ceilings follow your plan Base limits are tied to the subscription plan your organization is on. Upgrading the plan raises every ceiling across the board, typically by a generous multiplier per tier: | Plan | Profile | | ------ | ----------------------------------------------------------------------------------- | | **S** | Starter workloads: a few interactive users, light sync. | | **M** | Growing integrations: mixed read/write, several daily sync jobs. | | **L** | Production scale: continuous sync, real-time dashboards, larger audiences. | | **XL** | High-volume platforms: multi-region reporting, heavy aggregations, bulk operations. | If you are hitting `429`s regularly and your workload genuinely needs the headroom, the path forward is to **upgrade the plan** — see the **Billing** section of the [dashboard](https://dashboard.instasent.com) or talk to your account manager. For one-off spikes (backfills, seasonal traffic, audits) open a ticket with the expected peak rate and we will raise the ceiling on your organization for the duration. ## Reading the headers Every response includes three headers with the current window state. Log them in production — they are the cheapest way to spot a client that is about to hit the wall. ``` X-RateLimit-Limit 600 X-RateLimit-Remaining 587 X-RateLimit-Reset 1893452400 ``` | Header | Meaning | | ----------------------- | ------------------------------------------------------------------------------ | | `X-RateLimit-Limit` | Total requests allowed in the current window, for the plan and endpoint class. | | `X-RateLimit-Remaining` | Requests left before the window tightens. | | `X-RateLimit-Reset` | Unix timestamp when the counter resets. | ## Designing for the ceiling A few patterns keep a Product API integration comfortably below the line: - **Scroll, don't paginate-and-forget.** The `/audience/scroll` and `/event/scroll` endpoints are built for large traverses — they hold a cursor, cost one hit per page and are generous on page size. - **Cache project-level specs.** Attribute and event specs barely change; call `/specs/*` once per deploy, not per request. - **Batch writes.** Ingest endpoints accept up to 100 items per call. A batched write counts as one hit. - **Separate credentials per workload.** An interactive dashboard and a nightly backfill should not share a token — they share the same ceiling, which hides problems. ## When you hit the limit Requests that exceed the window return **`429 Too Many Requests`**. The body is empty; the `X-RateLimit-Reset` header tells you when to retry. > **Tip**: Back off exponentially rather than retrying in a tight loop. A client that hammers a 429 response keeps the window full and never recovers — waiting until `X-RateLimit-Reset` resolves the situation cleanly. ## What's next - **[Errors](/product-api/errors)** — every status code you might receive, including `429`. - **[API Reference](/product-api/reference)** — per-endpoint documentation. --- URL: https://docs.instasent.com/product-api/errors # Errors The Product API uses standard HTTP status codes. This page lists the ones you will encounter, what they mean and whether a retry is safe. Every response carries a meaningful HTTP status code. A `2xx` means the request succeeded; anything else signals a problem your client should handle explicitly. Error bodies, when present, are JSON with a `message` field describing the failure. ## Status codes ### `200 OK` The request succeeded and the body carries the resource. ### `201 Created` / `202 Accepted` Write endpoints return `201` for direct creates and `202` for batched writes that are accepted for asynchronous processing. Batched writes return a per-item outcome — inspect it before assuming the whole batch landed. ### `204 No Content` The request succeeded and there is no body to return. Used for deletes and idempotent no-ops. ### `400 Bad Request` The request body or query string is malformed or has the wrong shape. Not retryable — fix the payload first. ```json { "message": "Invalid query filter" } ``` ### `401 Unauthorized` The token is missing, revoked or malformed. Not retryable with the same token — check the [Authentication](/product-api/authentication) guide. ### `403 Forbidden` The token is valid but lacks the scope required for this endpoint, or the privacy level is insufficient to return the requested fields. Not retryable as-is — mint a token with the right scope, or upgrade the data privilege level. See [scopes in the Guide](/product-api/guide#tokens-and-scopes). ### `404 Not Found` The `project`, resource id, user id, phone, email or audience id in the URL does not exist or is not visible to the current token. ### `422 Unprocessable Entity` Validation failed. For batched writes, every item in the batch failed — the body lists per-item errors. Not retryable as-is — fix the items and resubmit. ### `429 Too Many Requests` You hit the per-endpoint rate-limit window. Retry after `X-RateLimit-Reset`. See [Rate limits](/product-api/rate-limits). ### `500 Internal Server Error` Something broke on our side. Retry with exponential backoff; if the failure persists, contact support with the request id. ## Retry policy > **Tip**: Retry `429` and `5xx` with exponential backoff, starting at 1 s and capping at a minute or so. Everything in the `4xx` range other than `429` means the request itself is wrong — retrying will not help. ## What's next - **[Rate limits](/product-api/rate-limits)** — the `X-RateLimit-*` headers and plan ceilings. - **[API Reference](/product-api/reference)** — per-endpoint responses and schemas. --- URL: https://docs.instasent.com/product-api/audience-query-filter # Audience query filter Search and segment the unified audience with a structured JSON filter. Combine attribute, event and segment conditions with AND/OR logic, paginate with cursors and run aggregations. The **Audience Contact Query Filter** is a structured JSON filter posted to `/project/{project}/audience/search` (and a handful of related endpoints) to find contacts by attributes, tags, segment membership or associated events. It is the primary search surface for the unified audience and the building block behind every dynamic segment. > **Note**: The Audience query filter is distinct from the generic [Query Filter](/further-reading/query-filter) used on index endpoints across every Instasent API. Similar names, independent semantics — this page documents the audience-specific grammar. ## Quickstart ### Contacts with email containing "yahoo" ```json { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "attribute_condition", "key": "_email", "operator": "contains", "values": ["yahoo"] } ] }, "limit": 10, "offset": 0, "sortField": "_email", "sortAsc": true, "includeAllData": false } ``` ### Contacts with email containing ".com" AND specific tags ```json { "version": "0.0.1", "root": { "type": "group", "join": "and", "children": [ { "type": "attribute_condition", "key": "_email", "operator": "contains", "values": [".com"] }, { "type": "attribute_condition", "key": "_client_tags", "operator": "matches-string", "values": ["&&", "tag-5", "tag-csv-2"] } ] }, "limit": 100, "offset": 0, "sortField": "_email", "sortAsc": false } ``` ## Basic concepts ### Filter structure A query filter is an object with: - **`root`** — the main filter condition, typically a `group` containing multiple conditions. - **`limit`** — maximum number of results to return. - **`offset`** — number of results to skip (for pagination). - **`sortField`** — field to sort by (optional). - **`sortAsc`** — sort direction: `true` for ascending, `false` for descending (optional). - **`includeAllData`** — whether to include all contact data or just essential fields (optional, default: `true`). ### Condition types - **`attribute_condition`** — filter contacts by a contact attribute. - **`event_condition`** — filter by a field on an associated event. - **`group_event`** — group event conditions that apply to the same event type. - **`segment_condition`** — filter by segment membership. - **`group`** — combine multiple conditions with AND/OR logic (`join: "and" | "or"`). ### Operators at a glance | Family | Operators | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Generic | `match-all`, `match-none`, `exists`, `exists-not` | | Boolean | `matches-bool` | | String | `contains`, `contains-not`, `startswith`, `startswith-not`, `endswith`, `endswith-not`, `matches-string`, `matches-string-not` | | Numeric | `matches-number`, `matches-number-not`, `range-number`, `range-number-not` | | Date | `matches-date`, `range-date`, `range-date-not`, `range-date-relative`, `range-date-anniversary`, `range-date-dayversary`, `range-date-timeversary` | | Geographic | `geopoint-distance` | | Segment | `in-segment`, `in-segment-not` (only on `segment_condition`) | See the [Operators reference](#operators-reference) at the bottom for the full schemas, `values` shapes and behavioural notes. ## Common patterns ### Filter by email ```json { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "attribute_condition", "key": "_email", "operator": "contains", "values": [".com"] } ] }, "limit": 100, "offset": 0, "sortField": "_email", "sortAsc": true } ``` ### Filter by tags Tags use `matches-string` with a join keyword as the first element of `values`: `&&` (AND) or `||` (OR). ```json { "version": "0.0.1", "root": { "type": "group", "join": "and", "children": [ { "type": "attribute_condition", "key": "_client_tags", "operator": "matches-string", "values": ["&&", "tag-5", "tag-csv-2"] } ] }, "limit": 100, "offset": 0 } ``` ### Filter by segment membership ```json { "version": "0.0.1", "root": { "type": "group", "join": "and", "children": [ { "type": "segment_condition", "key": "test-segment", "operator": "in-segment" }, { "type": "segment_condition", "key": "other-segment", "operator": "in-segment-not" } ] }, "limit": 100, "offset": 0 } ``` ### Filter by events Contacts with an email AND an `ecommerce_order_create` event that carries a `campaign` parameter: ```json { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "attribute_condition", "key": "_email", "operator": "exists", "values": [] }, { "type": "group_event", "datasource": ["datasource-id-1", "datasource-id-2"], "event": "ecommerce_order_create", "children": [ { "type": "event_condition", "key": "campaign", "operator": "exists", "values": [] } ] } ] }, "limit": 10, "offset": 0, "sortField": "_email", "sortAsc": true } ``` ### Date range queries Contacts imported in the last 7 days: ```json { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "attribute_condition", "key": "_date_imported", "operator": "range-date-relative", "values": { "lowerOffset": -7, "upperOffset": null, "lowerOffsetPeriod": "day", "upperOffsetPeriod": "day" } } ] }, "limit": 100, "offset": 0 } ``` ### Complex event filtering Contacts with `transactional_send` events (SMS) from the last 30 days **OR** with the `marketing` category: ```json { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "attribute_condition", "key": "_email", "operator": "exists", "values": [] }, { "type": "group_event", "event": "transactional_send", "join": "and", "datasource": ["datasource-id-1", "datasource-id-2"], "children": [ { "type": "event_condition", "key": "type", "operator": "matches-string", "values": ["sms"] }, { "type": "group", "join": "or", "children": [ { "type": "event_condition", "key": "created-at", "operator": "range-date-relative", "values": { "lowerOffset": -30, "lowerOffsetPeriod": "day", "upperOffset": 0, "upperOffsetPeriod": "day" } }, { "type": "event_condition", "key": "category", "operator": "matches-string", "values": ["marketing"] } ] } ] } ] }, "limit": 10 } ``` ### Additional filter options Top-level filters that refine the result set without going through `root`: | Field | Effect | | ----------------------- | ------------------------------------------------------------ | | `filterAudienceIds` | Restrict to specific audience contact IDs. | | `filterNotAudienceIds` | Exclude specific audience contact IDs. | | `filterContactIds` | Restrict to specific datasource contact IDs. | | `filterDatasourceIds` | Restrict to specific datasource IDs. | | `filterUniversalSearch` | Search within the universal search field. | | `filterSamplingPercent` | Retrieve only a percentage of results (0–100, default: 100). | | `filterBucketMin` | Minimum bucket number (0–199, default: 0). | | `filterBucketMax` | Maximum bucket number (0–199, default: 199). | | `filterBucketIn` | Restrict to specific bucket numbers (array). | | `filterBucketNotIn` | Exclude specific bucket numbers (array). | Example combining universal search and sampling: ```json { "version": "0.0.1", "limit": 100, "offset": 0, "sortField": "_email", "sortAsc": true, "filterUniversalSearch": ".com", "filterSamplingPercent": 33, "root": { "type": "group", "join": "and", "children": [ { "type": "attribute_condition", "key": "_email", "operator": "contains", "values": [".com"] } ] } } ``` ## Dynamic attributes These are not project attributes but can be used as `key` in `attribute_condition`: - `_universal_search` — universal search field across common identifying attributes. - `_bucket` — bucket number (0–199), for deterministic sampling. ```json { "version": "0.0.1", "root": { "type": "group", "join": "and", "children": [ { "type": "attribute_condition", "key": "_universal_search", "operator": "contains", "values": ["brittney"] }, { "type": "attribute_condition", "key": "_bucket", "operator": "range-number", "values": { "lowerNumber": 0, "upperNumber": 40 } } ] }, "limit": 100, "offset": 0, "sortField": "_email", "sortAsc": true } ``` ### Sampling with buckets Bucket ranges are deterministic — the same contact always falls into the same bucket — so they are ideal for consistent samples across runs. ```json { "version": "0.0.1", "root": { "type": "group", "join": "and", "children": [ { "type": "attribute_condition", "key": "_universal_search", "operator": "contains", "values": ["brittney"] }, { "type": "attribute_condition", "key": "_bucket", "operator": "range-number", "values": { "lowerNumber": 0, "upperNumber": 10 } } ] }, "limit": 100, "offset": 0, "sortField": "_email", "sortAsc": true } ``` ## Cursor-based pagination For large traverses, use cursor pagination instead of `offset`/`limit`. A cursor keeps a consistent snapshot of the data across requests. ### How it works 1. **First request** — run a query without a cursor. The response includes a `cursor` string. 2. **Subsequent requests** — pass the `cursor` from the previous response to continue. 3. **End of results** — when `cursor` is `null`, there are no more results. ### Cursor format The cursor is a **base64-encoded string** containing a snapshot identifier, position information and a keep-alive setting. You do not need to parse or modify it — just pass it back as-is. ### Using cursors ```json // First request { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "attribute_condition", "key": "_email", "operator": "exists", "values": [] } ] }, "limit": 100 } // Response includes: { "cursor": "eyJwb2ludEluVGltZUlkIjoiLi4uIn0=", ... } // Next request — just pass the cursor { "cursor": "eyJwb2ludEluVGltZUlkIjoiLi4uIn0=" } ``` > **Note**: - The cursor expires after a period of inactivity (default: 1–5 minutes). > - `offset` is **not compatible** with cursor pagination and is cleared when a cursor is set. > - Cursors are **stateless** — any process holding the cursor can continue the iteration. ## Aggregations > **Warning**: Aggregations are **disabled by default** and are only available on endpoints that explicitly support them. Check the endpoint in the [API Reference](/product-api/reference) before using them. Access requires the `PROJECT_AGGREGATIONS` scope, which is manually granted by Instasent and reserved for trusted partners. Aggregations analyse and summarise the filtered contacts. They use the standard format with configuration inside `params`; the `@` prefix references an attribute and is auto-resolved. ### Terms aggregation Group contacts by unique values: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "mi-agregacion": { "type": "terms", "params": { "field": "@_country_code", "size": 3 }, "aggs": { "mi-otra-agregacion": { "type": "terms", "params": { "field": "@_email" } } } } } } ``` ### Terms with "other" bucket ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "topCountries": { "type": "terms", "params": { "field": "@_country_code", "size": 5, "other_bucket": true, "other_bucket_label": "other" } } } } ``` Returns the top 5 countries plus an "other" bucket containing the count of everything else. ### Nested terms with "other" buckets Enable "other" buckets at multiple levels: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "mainCategories": { "type": "terms", "params": { "field": "@_product_category", "size": 3, "other_bucket": true, "other_bucket_label": "other_categories" }, "aggs": { "subCategories": { "type": "terms", "params": { "field": "@_product_subcategory", "size": 2, "other_bucket": true, "other_bucket_label": "other_subcategories" } } } } } } ``` ### Range aggregations ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "contactsByBucketRange": { "type": "range", "params": { "field": "@_bucket", "ranges": [ { "from": 0, "to": 33 }, { "from": 33, "to": 66 }, { "from": 66, "to": 100 } ] } } } } ``` ### Getting tags List all system and client tags: ```json // POST /project/my-project-1/audience/search { "version": "0.0.1", "limit": 0, "aggregations": { "system_tags": { "type": "terms", "params": { "field": "@_tags", "size": 100 } }, "client_tags": { "type": "terms", "params": { "field": "@_client_tags", "size": 100 } } } } ``` ### Nested aggregations Aggregate on nested fields (like events): ```json { "version": "0.0.1", "limit": 0, "offset": 0, "sortField": "_email", "sortAsc": true, "aggregations": { "mi-agregacion": { "type": "nested", "params": { "path": "events" }, "aggs": { "tipo-de-eventos": { "type": "terms", "params": { "field": "events.type" } } } } } } ``` ## Native event attributes When filtering by events, these native event attributes are available: | Attribute | Meaning | | ------------------------ | ------------------------------ | | `received-at` | When the event was received. | | `created-at` | When the event was created. | | `event-source` | Source of the event. | | `event-type` | Type of event. | | `audience-id` | ID of the audience contact. | | `audience-ids` | Multiple audience contact IDs. | | `audience-ds-ids` | Audience datasource IDs. | | `audience-categories` | Audience categories. | | `audience-target-groups` | Audience target groups. | | `ds-contact-id` | Datasource contact ID. | | `ds-id` | Datasource ID. | | `ds-event-id` | Datasource event ID. | ## Operators reference Every condition node has the shape: ```json { "type": "attribute_condition", "key": "", "operator": "", "values": "" } ``` The subsections below describe, for each operator, the exact shape of `values` and any behavioural notes. About `key`: - Native attributes have UIDs prefixed with `_` (`_email`, `_phone_mobile`, `_country_code`, `_client_tags`, `_is_subscribed_sms`, `_date_birthday`, `_date_imported`, `_bucket`, `_geopoint`, …). They are defined by Instasent and always available. - Custom attributes use the UID configured by the project (no leading `_`). - On `event_condition`, `key` is an event attribute — either a [native event attribute](#native-event-attributes) or a custom event parameter defined by the project. - On `segment_condition`, `key` is the segment identifier (slug), not an attribute UID. Conventions used below: - **Negation.** Every operator with a `-not` suffix takes the same `values` as its positive counterpart and inverts the match. A missing attribute is *not matched* by the positive operator and *is matched* by the negated one — i.e. negation is "not (positive match)", which includes contacts where the attribute is absent. - **Multi-value attributes + `&&` prefix.** For array-valued attributes (e.g. `_client_tags`), string and numeric "list" operators default to OR semantics across the provided values. Passing `"&&"` as the first element of `values` switches to AND — *all* values must be present. `"||"` (the default) is also accepted explicitly. - **Condition type.** Most operators work on both `attribute_condition` and `event_condition`. Exceptions: `in-segment` / `in-segment-not` are valid only on `segment_condition`, and `range-date-dayversary` is valid only on `event_condition` against `created-at`. ### Generic These work regardless of the attribute's data type. #### `match-all` Matches every contact. Used internally; children of a group with this operator are collapsed away. `values` is ignored. #### `match-none` Matches no contacts. Used internally for invalid or empty queries. `values` is ignored. #### `exists` The attribute is present on the contact (non-null, and non-empty for array attributes). ```json { "type": "attribute_condition", "key": "_email", "operator": "exists", "values": [] } ``` #### `exists-not` The attribute is missing or null. Same `values` shape as `exists`. ### Boolean #### `matches-bool` Exact boolean match. `values` must be an array with exactly one element: `[true]` or `[false]`. ```json { "type": "attribute_condition", "key": "_is_subscribed_sms", "operator": "matches-bool", "values": [true] } ``` > **Note**: There is no `matches-bool-not`. Use `matches-bool` with the opposite boolean, or wrap an `exists-not` inside a negated group. ### String Apply to attribute data types `STRING`, `KEYWORD` and `TEXT`. All string operators accept an array of strings; the first element may optionally be `"&&"` or `"||"` to set the combination mode for multi-value attributes (default `||` / OR). | Operator | Behaviour | Case | Min length | | -------------------- | --------------------------- | ---------------- | ---------- | | `contains` | Substring match (`*value*`) | Case-insensitive | 2 chars | | `contains-not` | Inverse of `contains` | Case-insensitive | 2 chars | | `startswith` | Prefix match (`value*`) | Case-insensitive | 1 char | | `startswith-not` | Inverse of `startswith` | Case-insensitive | 1 char | | `endswith` | Suffix match (`*value`) | Case-insensitive | 1 char | | `endswith-not` | Inverse of `endswith` | Case-insensitive | 1 char | | `matches-string` | Exact value match | Case-sensitive | 1 char | | `matches-string-not` | Inverse of `matches-string` | Case-sensitive | 1 char | Max value length for `contains` / `contains-not`: 128 chars. OR across multiple values (default): ```json { "type": "attribute_condition", "key": "_email", "operator": "contains", "values": ["yahoo", "hotmail"] } ``` AND across multiple values (for multi-value attributes such as `_client_tags`): ```json { "type": "attribute_condition", "key": "_client_tags", "operator": "matches-string", "values": ["&&", "tag-5", "tag-csv-2"] } ``` The example above matches contacts that carry **both** `tag-5` and `tag-csv-2`. Without `"&&"` it would match contacts that carry either of them. ### Numeric Apply to data types `INT` and `DECIMAL`. #### `matches-number` / `matches-number-not` Exact match against one or more numbers. Same `&&` / `||` semantics as the string operators. ```json { "type": "attribute_condition", "key": "_bucket", "operator": "matches-number", "values": [10, 25, 50] } ``` #### `range-number` / `range-number-not` Range query. `values` is an **object**, not an array: ```json { "type": "attribute_condition", "key": "_bucket", "operator": "range-number", "values": { "lowerNumber": 0, "upperNumber": 40, "lowerExcludeEquals": false, "upperExcludeEquals": false } } ``` | Field | Type | Default | Meaning | | -------------------- | -------------- | ------- | ------------------------------------------------------------------------ | | `lowerNumber` | number \| null | `null` | Lower bound. `null` = unbounded below. | | `upperNumber` | number \| null | `null` | Upper bound. `null` = unbounded above. | | `lowerExcludeEquals` | bool | `false` | If `true`, lower bound is exclusive (`gt`); otherwise inclusive (`gte`). | | `upperExcludeEquals` | bool | `false` | If `true`, upper bound is exclusive (`lt`); otherwise inclusive (`lte`). | Both bounds `null` on `range-number` matches everything (and nothing on `range-number-not`). ### Date Apply to data type `DATE`. All date operators honour the project timezone. #### `matches-date` Exact day match. Internally expanded to `[00:00:00, 23:59:59]` in the project timezone. ```json { "type": "attribute_condition", "key": "_date_birthday", "operator": "matches-date", "values": { "date": "1990-05-14" } } ``` | Field | Type | Notes | | ------ | --------------------- | ---------------------- | | `date` | string (`YYYY-MM-DD`) | Required. Exactly one. | #### `range-date` / `range-date-not` Absolute date range. ```json { "type": "attribute_condition", "key": "_date_imported", "operator": "range-date", "values": { "lowerDate": "2026-01-01", "upperDate": "2026-03-31", "lowerExcludeEquals": false, "upperExcludeEquals": false, "lowerRounding": true, "upperRounding": true } } ``` | Field | Type | Default | Meaning | | -------------------- | ------------------------- | ------- | ----------------------------------------------------------------- | | `lowerDate` | ISO date/datetime \| null | `null` | Lower bound. `null` = unbounded below. | | `upperDate` | ISO date/datetime \| null | `null` | Upper bound. `null` = unbounded above. | | `lowerExcludeEquals` | bool | `false` | Exclude equality on lower bound (`gt` vs `gte`). | | `upperExcludeEquals` | bool | `false` | Exclude equality on upper bound (`lt` vs `lte`). | | `lowerRounding` | bool | `false` | If `true`, rounds the lower bound to the start of its day (`/d`). | | `upperRounding` | bool | `false` | If `true`, rounds the upper bound to the end of its day (`/d`). | #### `range-date-relative` / `range-date-relative-not` Range expressed relative to *now*. Useful for "last N days", "next month", etc. ```json { "type": "attribute_condition", "key": "_date_imported", "operator": "range-date-relative", "values": { "lowerOffset": -7, "upperOffset": 0, "lowerOffsetPeriod": "day", "upperOffsetPeriod": "day", "lowerExcludeEquals": false, "upperExcludeEquals": false, "lowerRounding": false, "upperRounding": false } } ``` | Field | Type | Default | Meaning | | -------------------- | ----------- | ------- | ------------------------------------------------------------------------------- | | `lowerOffset` | int \| null | `null` | Signed offset from now. Negative = past, positive = future. `null` = unbounded. | | `upperOffset` | int \| null | `null` | Signed offset from now. | | `lowerOffsetPeriod` | enum | `"day"` | Unit of `lowerOffset`. One of: `min`, `hour`, `day`, `week`, `month`, `year`. | | `upperOffsetPeriod` | enum | `"day"` | Unit of `upperOffset`. Same options. | | `lowerExcludeEquals` | bool | `false` | Same semantics as `range-date`. | | `upperExcludeEquals` | bool | `false` | Same semantics as `range-date`. | | `lowerRounding` | bool | `false` | Rounds the bound to the start of the period. | | `upperRounding` | bool | `false` | Rounds the bound to the end of the period. | Example — "imported in the last 30 days": ```json { "lowerOffset": -30, "lowerOffsetPeriod": "day", "upperOffset": 0, "upperOffsetPeriod": "day" } ``` #### `range-date-anniversary` Matches date anniversaries independent of year (e.g. "birthday is in the next 7 days", "anniversary falls between Jan 25 and Dec 01"). Only available on DATE attributes whose field supports the anniversary index. ```json // Relative mode — N days before/after today's anniversary { "values": { "mode": "relative", "lowerOffsetDays": 0, "upperOffsetDays": 7 } } // Absolute mode — month-day range (format MMDD) { "values": { "mode": "absolute", "lowerDate": "0125", "upperDate": "1201" } } ``` | Field | Type | Required when | Notes | | ----------------- | ---------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------- | | `mode` | `"relative"` \| `"absolute"` | always | Default `"relative"`. | | `lowerOffsetDays` | int | `mode = relative` | Signed. Negative = before, positive = after. `gte`. | | `upperOffsetDays` | int | `mode = relative` | Signed. Exclusive upper (`lt`). Default `1`. | | `lowerDate` | string (`MMDD`) | `mode = absolute` | e.g. `"0125"` = Jan 25. | | `upperDate` | string (`MMDD`) | `mode = absolute` | e.g. `"1201"` = Dec 01. Inverted ranges (e.g. `1215` → `0115`) wrap automatically across the year boundary. | #### `range-date-dayversary` Matches a weekday + hour-of-day window. Only valid on `event_condition` against `created-at`. Use it for recurring weekly schedules — e.g. "events on Friday evenings". ```json { "values": { "lowerDay": "105", "upperDay": "523" } } ``` | Field | Type | Format | Example | | ---------- | ------ | -------------------------------------------------------------------------------- | ------------------------------ | | `lowerDay` | string | `DHH` — 1 digit weekday (`1` = Monday … `7` = Sunday) + 2 digit hour (`00`–`23`) | `"105"` = Monday 05:00, `gte`. | | `upperDay` | string | same | `"523"` = Friday 23:00, `lte`. | If the lower day/hour is *greater* than the upper (e.g. Friday → Monday), the range wraps across the week boundary automatically. #### `range-date-timeversary` Matches a time-of-day window independent of the date. Two `mode`s, analogous to `range-date-anniversary`. ```json // Relative mode — N minutes around the current time { "values": { "mode": "relative", "lowerOffsetMinutes": -15, "upperOffsetMinutes": 15 } } // Absolute mode — HH:MM range (format HHMM or HH:MM — colons are stripped) { "values": { "mode": "absolute", "lowerTime": "0550", "upperTime": "2200" } } ``` | Field | Type | Required when | Notes | | -------------------- | ---------------------------- | ----------------- | ------------------------------------------------------- | | `mode` | `"relative"` \| `"absolute"` | always | Default `"relative"`. | | `lowerOffsetMinutes` | int | `mode = relative` | Signed, `gte`. | | `upperOffsetMinutes` | int | `mode = relative` | Signed, `lt`. Default `1`. | | `lowerTime` | string (`HHMM`) | `mode = absolute` | 24h. `"0550"` = 05:50. | | `upperTime` | string (`HHMM`) | `mode = absolute` | `"2200"` = 22:00. Inverted ranges wrap across midnight. | ### Geographic #### `geopoint-distance` Matches contacts within a radius of a `(longitude, latitude)` point. Only applies to geopoint attributes. ```json { "type": "attribute_condition", "key": "_geopoint", "operator": "geopoint-distance", "values": { "longitude": -3.7038, "latitude": 40.4168, "distance": 25 } } ``` | Field | Type | Required | Notes | | ----------- | ------- | -------- | ----------------------------------- | | `longitude` | decimal | yes | WGS-84 longitude. | | `latitude` | decimal | yes | WGS-84 latitude. | | `distance` | decimal | yes | Radius. Default unit is kilometres. | All three fields are required; missing any raises a validation error. ### Segment Used only on `segment_condition` nodes. The `key` is the segment identifier (slug), and `values` is ignored. #### `in-segment` Contact belongs to the named segment. Segments are evaluated recursively (a segment may reference other segments) with a nesting depth cap of 4 and circular-reference detection. A missing segment resolves to `match-none` — except the special `_all` segment, which resolves to `match-all`. ```json { "type": "segment_condition", "key": "high-value-customers", "operator": "in-segment" } ``` #### `in-segment-not` Contact does *not* belong to the named segment. Same `key` semantics. ### `values` shape — quick reference | Operator | `values` shape | | | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | - | --- | | `match-all`, `match-none` | ignored | | | | `exists`, `exists-not` | `[]` | | | | `matches-bool` | `[true]` or `[false]` | | | | `contains(-not)`, `startswith(-not)`, `endswith(-not)`, `matches-string(-not)` | `string[]`, optional leading `"&&"` / \`" | | "\` | | `matches-number(-not)` | `number[]`, optional leading `"&&"` / \`" | | "\` | | `range-number(-not)` | `{ lowerNumber, upperNumber, lowerExcludeEquals?, upperExcludeEquals? }` | | | | `matches-date` | `{ date: "YYYY-MM-DD" }` | | | | `range-date(-not)` | `{ lowerDate, upperDate, lowerExcludeEquals?, upperExcludeEquals?, lowerRounding?, upperRounding? }` | | | | `range-date-relative(-not)` | `{ lowerOffset, upperOffset, lowerOffsetPeriod, upperOffsetPeriod, lower/upperExcludeEquals?, lower/upperRounding? }` | | | | `range-date-anniversary` | `{ mode: "relative", lowerOffsetDays, upperOffsetDays }` or `{ mode: "absolute", lowerDate: "MMDD", upperDate: "MMDD" }` | | | | `range-date-dayversary` | `{ lowerDay: "DHH", upperDay: "DHH" }` (events only) | | | | `range-date-timeversary` | `{ mode: "relative", lowerOffsetMinutes, upperOffsetMinutes }` or `{ mode: "absolute", lowerTime: "HHMM", upperTime: "HHMM" }` | | | | `geopoint-distance` | `{ longitude, latitude, distance }` | | | | `in-segment`, `in-segment-not` | ignored (segment id goes in `key`) | | | ## What's next - **[Audience event query filter](/product-api/audience-event-query-filter)** — the separate grammar for searching events directly. - **[Query Filter](/further-reading/query-filter)** — the generic index-endpoint filter, used elsewhere across the platform. - **[API Reference](/product-api/reference)** — endpoints that accept this filter (`/audience/search`, `/audience/scroll`, `/audience/segment/{uid}/scroll`, `/audience/aggregations`). --- URL: https://docs.instasent.com/product-api/audience-event-query-filter # Audience event query filter Search and aggregate audience events with a structured JSON filter. Group by event type, filter on parameters, run date histograms and sum metrics across the last N days. The **Event Query Filter** is a structured JSON filter posted to `/project/{project}/event/search` (and related event endpoints) to find events by type, parameters, creation date or any native event attribute. It shares the overall shape of the [Audience query filter](/product-api/audience-query-filter) but is specific to the event collection — the condition grammar is narrower and the aggregation surface is richer (date histograms, sums, metrics). > **Note**: Event parameters live under the event type namespace. For a `create` event you filter on `create.source`; for an order you filter on `ecommerce_order_create.order-euro-amount`. Parameter keys are auto-resolved against the event type configuration of your project. ## Quickstart ### All `create` events from the last 30 days ```json { "version": "0.0.1", "root": { "type": "group", "join": "and", "children": [ { "type": "event_condition", "key": "event-type", "operator": "matches-string", "values": ["create"] }, { "type": "event_condition", "key": "created-at", "operator": "range-date-relative", "values": { "lowerOffset": -30, "upperOffset": null, "lowerExcludeEquals": false, "upperExcludeEquals": false, "lowerRounding": false, "upperRounding": false, "lowerOffsetPeriod": "day", "upperOffsetPeriod": "day" } } ] }, "limit": 10, "offset": 0 } ``` ### Ecommerce orders between €100 and €200 ```json { "version": "0.0.1", "root": { "type": "group", "join": "and", "children": [ { "type": "event_condition", "key": "event-type", "operator": "matches-string", "values": ["ecommerce_order_create"] }, { "type": "event_condition", "key": "ecommerce_order_create.order-euro-amount", "operator": "range-number", "values": { "lowerNumber": 100.0, "upperNumber": 200.0 } } ] }, "limit": 10, "offset": 0 } ``` ## Basic concepts ### Filter structure - **`root`** — the main filter condition, typically a `group` of event conditions. - **`limit`** — maximum number of results to return. - **`offset`** — number of results to skip. - **`sortField`** — field to sort by (optional). - **`sortAsc`** — sort direction: `true` for ascending, `false` for descending (optional). ### Condition types - **`event_condition`** — filter by a native event attribute or an event parameter. - **`group`** — combine multiple conditions with AND/OR logic (`join: "and" | "or"`). ### Native event attributes Available directly as `key` in an `event_condition`: | Attribute | Meaning | | ------------------------ | ------------------------------------------------------------------ | | `bucket` | Bucket number. | | `received-at` | When the event was received. | | `created-at` | When the event was created. | | `event-source` | Source of the event. | | `event-type` | Type of event (e.g. `create`, `update`, `ecommerce_order_create`). | | `audience-id` | ID of the audience contact. | | `audience-ids` | Multiple audience contact IDs. | | `audience-ds-ids` | Audience datasource IDs. | | `audience-categories` | Audience categories. | | `audience-target-groups` | Audience target groups. | | `ds-contact-id` | Datasource contact ID. | | `ds-id` | Datasource ID. | | `ds-event-id` | Datasource event ID. | ### Event parameters Filter by event-specific parameters by prefixing the key with the event type. Examples: - `create.source` — the `source` parameter of `create` events. - `ecommerce_order_create.order-euro-amount` — the `order-euro-amount` parameter of `ecommerce_order_create` events. Parameter keys are auto-resolved based on the event type configuration of the project. ## Common patterns ### By event type and source ```json { "version": "0.0.1", "root": { "type": "group", "join": "and", "children": [ { "type": "event_condition", "key": "event-type", "operator": "matches-string", "values": ["create"] }, { "type": "event_condition", "key": "create.source", "operator": "matches-string", "values": ["datasource|_instasent"] } ] }, "limit": 10, "offset": 0, "sortField": "create.source", "sortAsc": true } ``` ### Date range ```json { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "event_condition", "key": "created-at", "operator": "range-date-relative", "values": { "lowerOffset": -30, "upperOffset": null, "lowerExcludeEquals": false, "upperExcludeEquals": false, "lowerRounding": false, "upperRounding": false, "lowerOffsetPeriod": "day", "upperOffsetPeriod": "day" } } ] }, "limit": 10, "offset": 0 } ``` ### Events for specific contacts ```json { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "event_condition", "key": "audience-id", "operator": "matches-string", "values": ["0aCcUIewHALKk9jpaS9QaazifbmUr8fB"] } ] }, "limit": 100, "offset": 0 } ``` ### Additional filter options Top-level filters that narrow the result set without going through `root`: | Field | Effect | | ----------------------------- | ------------------------------------------------------------ | | `filterAudienceIds` | Restrict to specific audience contact IDs. | | `filterEventIds` | Restrict to specific event IDs. | | `filterEventIdsNot` | Exclude specific event IDs. | | `filterDatasourceIds` | Restrict to specific datasource IDs. | | `filterAudienceDatasourceIds` | Restrict events for contacts from specific datasources. | | `filterSamplingPercent` | Retrieve only a percentage of results (0–100, default: 100). | | `filterBucketMin` | Minimum bucket number (0–100, default: 0). | | `filterBucketMax` | Maximum bucket number (0–100, default: 100). | Example: ```json { "version": "0.0.1", "limit": 100, "offset": 0, "root": { "type": "group", "children": [ { "type": "event_condition", "key": "event-type", "operator": "matches-string", "values": ["create"] } ] }, "filterSamplingPercent": 33, "filterAudienceIds": ["0aCcUIewHALKk9jpaS9QaazifbmUr8fB"], "filterEventIdsNot": ["S9QaazifbmUr8fB0aCcUIewHALKk9jpa"] } ``` ## Cursor-based pagination For large traverses, use cursor pagination instead of `offset`/`limit`. A cursor keeps a consistent snapshot of the data across requests. ### How it works 1. **First request** — run a query without a cursor. The response includes a `cursor` string. 2. **Subsequent requests** — pass the `cursor` from the previous response to continue. 3. **End of results** — when `cursor` is `null`, there are no more results. ### Cursor format The cursor is a **base64-encoded string** containing a snapshot identifier, position information and a keep-alive setting. You do not need to parse or modify it — just pass it back as-is. ### Using cursors ```json // First request { "version": "0.0.1", "root": { "type": "group", "children": [ { "type": "event_condition", "key": "event-type", "operator": "matches-string", "values": ["create"] } ] }, "limit": 100 } // Response includes: { "cursor": "eyJwb2ludEluVGltZUlkIjoiLi4uIn0=", ... } // Next request — just pass the cursor { "cursor": "eyJwb2ludEluVGltZUlkIjoiLi4uIn0=" } ``` > **Note**: - The cursor expires after a period of inactivity (default: 1–5 minutes). > - `offset` is **not compatible** with cursor pagination and is cleared when a cursor is set. > - Cursors are **stateless** — any process holding the cursor can continue the iteration. ## Aggregations > **Warning**: Aggregations are **disabled by default** and are only available on endpoints that explicitly support them. Check the endpoint in the [API Reference](/product-api/reference) before using them. Access requires the `PROJECT_AGGREGATIONS` scope, which is manually granted by Instasent and reserved for trusted partners. Aggregations analyse and summarise the filtered events. All configuration goes inside `params`; use the `@` prefix to reference event parameters — they are auto-resolved. ### Date histogram Group events by time intervals: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "eventsByDay": { "type": "date_histogram", "params": { "field": "@created-at", "calendar_interval": "1d", "format": "yyyy-MM-dd", "time_zone": "UTC" } } } } ``` ### Date histogram with nested aggregations Calculate metrics for each time period: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "salesByDate": { "type": "date_histogram", "params": { "field": "@ecommerce_order_create.created-at", "calendar_interval": "1d", "format": "yyyy-MM-dd (EEE)", "time_zone": "Europe/Madrid" }, "aggs": { "totalSales": { "type": "sum", "params": { "field": "@ecommerce_order_create.order-euro-amount" } }, "orderCount": { "type": "sum", "params": { "field": "@ecommerce_order_create.product-count" } } } } } } ``` ### Fixed interval vs calendar interval - **`calendar_interval`** — calendar-aware intervals (`1d`, `1w`, `1M`, `1y`). - **`fixed_interval`** — fixed time intervals (`1h`, `6h`, `30m`). ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "salesBy6HourInterval": { "type": "date_histogram", "params": { "field": "@ecommerce_order_create.created-at", "fixed_interval": "6h", "format": "HH:00", "time_zone": "UTC" }, "aggs": { "salesAmount": { "type": "sum", "params": { "field": "@ecommerce_order_create.order-euro-amount" } } } } } } ``` ### Terms aggregation Group events by unique values in a field: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "eventTypes": { "type": "terms", "params": { "field": "@event-type", "size": 10 } } } } ``` ### Terms with nested aggregations Detailed metrics per category: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "topProducts": { "type": "terms", "params": { "field": "@ecommerce_order_create.product-name", "size": 5, "missing": "unknown", "order": { "salesCount": "desc" } }, "aggs": { "salesCount": { "type": "sum", "params": { "field": "@ecommerce_order_create.product-count" } }, "salesEuroAmount": { "type": "sum", "params": { "field": "@ecommerce_order_create.product-price-euro" } } } } } } ``` ### Sum aggregation Calculate totals across matching events: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "totalRevenue": { "type": "sum", "params": { "field": "@ecommerce_order_create.order-euro-amount" } } } } ``` ### Complete sales analysis Sales patterns across multiple dimensions: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "root": { "type": "group", "children": [ { "type": "event_condition", "key": "event-type", "operator": "matches-string", "values": ["ecommerce_order_create"] }, { "type": "event_condition", "key": "created-at", "operator": "range-date-relative", "values": { "lowerOffset": -21, "upperOffset": 0, "lowerOffsetPeriod": "day", "upperOffsetPeriod": "day" } } ] }, "aggregations": { "totalSalesAmount": { "type": "sum", "params": { "field": "@ecommerce_order_create.order-euro-amount" } }, "salesByDate": { "type": "date_histogram", "params": { "field": "@ecommerce_order_create.created-at", "calendar_interval": "1d", "format": "yyyy-MM-dd (EEE)", "time_zone": "UTC" }, "aggs": { "salesCount": { "type": "sum", "params": { "field": "@ecommerce_order_create.product-count" } }, "salesEuroAmount": { "type": "sum", "params": { "field": "@ecommerce_order_create.order-euro-amount" } } } }, "topProducts": { "type": "terms", "params": { "field": "@ecommerce_order_create.product-name", "size": 10, "missing": "unknown", "order": { "salesCount": "desc" } }, "aggs": { "salesCount": { "type": "sum", "params": { "field": "@ecommerce_order_create.product-count" } }, "salesEuroAmount": { "type": "sum", "params": { "field": "@ecommerce_order_create.product-price-euro" } } } } } } ``` ### Listing event types in a project ```json { "version": "0.0.1", "limit": 0, "aggregations": { "total": { "type": "terms", "params": { "field": "@event-type", "size": 100 } } } } ``` ### Range aggregations Group events into ranges: ```json { "version": "0.0.1", "limit": 0, "offset": 0, "aggregations": { "contactsByBucketRange": { "type": "range", "params": { "field": "@bucket", "ranges": [ { "from": 0, "to": 33 }, { "from": 33, "to": 66 }, { "from": 66, "to": 100 } ] } } } } ``` ## Operators reference Every condition node has the shape: ```json { "type": "event_condition", "key": "", "operator": "", "values": "" } ``` The subsections below describe, for each operator, the exact shape of `values` and any behavioural notes. About `key`: - A [native event attribute](#native-event-attributes) (e.g. `event-type`, `created-at`, `received-at`, `audience-id`, `ds-id`). - An event parameter, scoped by event type: `.` — e.g. `ecommerce_order_create.order-euro-amount`, `create.source`. Parameters are resolved against the event-type configuration of the project, and their data type drives which operators are valid. | Family | Operators | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Generic | `match-all`, `match-none`, `exists`, `exists-not` | | Boolean | `matches-bool` | | String | `contains`, `contains-not`, `startswith`, `startswith-not`, `endswith`, `endswith-not`, `matches-string`, `matches-string-not` | | Numeric | `matches-number`, `matches-number-not`, `range-number`, `range-number-not` | | Date | `matches-date`, `range-date`, `range-date-not`, `range-date-relative`, `range-date-anniversary`, `range-date-dayversary`, `range-date-timeversary` | | Geographic | `geopoint-distance` | Conventions used below: - **Negation.** Every operator with a `-not` suffix takes the same `values` as its positive counterpart and inverts the match. A missing field is *not matched* by the positive operator and *is matched* by the negated one — i.e. negation is "not (positive match)", which includes events where the field is absent. - **Multi-value fields + `&&` prefix.** For array-valued fields, string and numeric "list" operators default to OR semantics across the provided values. Passing `"&&"` as the first element of `values` switches to AND — *all* values must be present. `"||"` (the default) is also accepted explicitly. ### Generic These work regardless of the field's data type. #### `match-all` Matches every event. Used internally. `values` is ignored. #### `match-none` Matches no events. Used internally. `values` is ignored. #### `exists` The field is present on the event (non-null, and non-empty for array fields). ```json { "type": "event_condition", "key": "ecommerce_order_create.coupon-code", "operator": "exists", "values": [] } ``` #### `exists-not` The field is missing or null. ### Boolean #### `matches-bool` Exact boolean match. `values` must be an array with exactly one element: `[true]` or `[false]`. ```json { "type": "event_condition", "key": "ecommerce_order_create.is-first-order", "operator": "matches-bool", "values": [true] } ``` ### String Apply to fields of type `STRING`, `KEYWORD` and `TEXT`. All string operators accept an array of strings; the first element may optionally be `"&&"` or `"||"` to set the combination mode for multi-value fields (default `||` / OR). | Operator | Behaviour | Case | Min length | | -------------------- | --------------------------- | ---------------- | ---------- | | `contains` | Substring match (`*value*`) | Case-insensitive | 2 chars | | `contains-not` | Inverse of `contains` | Case-insensitive | 2 chars | | `startswith` | Prefix match (`value*`) | Case-insensitive | 1 char | | `startswith-not` | Inverse of `startswith` | Case-insensitive | 1 char | | `endswith` | Suffix match (`*value`) | Case-insensitive | 1 char | | `endswith-not` | Inverse of `endswith` | Case-insensitive | 1 char | | `matches-string` | Exact value match | Case-sensitive | 1 char | | `matches-string-not` | Inverse of `matches-string` | Case-sensitive | 1 char | Max value length for `contains` / `contains-not`: 128 chars. OR across multiple values (default): ```json { "type": "event_condition", "key": "event-type", "operator": "matches-string", "values": ["create", "update"] } ``` AND across multiple values (for multi-value fields such as `audience-target-groups`): ```json { "type": "event_condition", "key": "audience-target-groups", "operator": "matches-string", "values": ["&&", "vip", "newsletter"] } ``` ### Numeric Apply to fields of type `INT` and `DECIMAL`. #### `matches-number` / `matches-number-not` Exact match against one or more numbers. Same `&&` / `||` semantics as the string operators. ```json { "type": "event_condition", "key": "ecommerce_order_create.product-count", "operator": "matches-number", "values": [1, 2, 3] } ``` #### `range-number` / `range-number-not` Range query. `values` is an **object**, not an array: ```json { "type": "event_condition", "key": "ecommerce_order_create.order-euro-amount", "operator": "range-number", "values": { "lowerNumber": 100, "upperNumber": 200, "lowerExcludeEquals": false, "upperExcludeEquals": false } } ``` | Field | Type | Default | Meaning | | -------------------- | -------------- | ------- | ------------------------------------------------------------------------ | | `lowerNumber` | number \| null | `null` | Lower bound. `null` = unbounded below. | | `upperNumber` | number \| null | `null` | Upper bound. `null` = unbounded above. | | `lowerExcludeEquals` | bool | `false` | If `true`, lower bound is exclusive (`gt`); otherwise inclusive (`gte`). | | `upperExcludeEquals` | bool | `false` | If `true`, upper bound is exclusive (`lt`); otherwise inclusive (`lte`). | Both bounds `null` on `range-number` matches everything (and nothing on `range-number-not`). ### Date Apply to fields of type `DATE`. All date operators honour the project timezone. #### `matches-date` Exact day match. Internally expanded to `[00:00:00, 23:59:59]` in the project timezone. ```json { "type": "event_condition", "key": "created-at", "operator": "matches-date", "values": { "date": "2026-04-22" } } ``` | Field | Type | Notes | | ------ | --------------------- | ---------------------- | | `date` | string (`YYYY-MM-DD`) | Required. Exactly one. | #### `range-date` / `range-date-not` Absolute date range. ```json { "type": "event_condition", "key": "created-at", "operator": "range-date", "values": { "lowerDate": "2026-01-01", "upperDate": "2026-03-31", "lowerExcludeEquals": false, "upperExcludeEquals": false, "lowerRounding": true, "upperRounding": true } } ``` | Field | Type | Default | Meaning | | -------------------- | ------------------------- | ------- | ----------------------------------------------------------------- | | `lowerDate` | ISO date/datetime \| null | `null` | Lower bound. `null` = unbounded below. | | `upperDate` | ISO date/datetime \| null | `null` | Upper bound. `null` = unbounded above. | | `lowerExcludeEquals` | bool | `false` | Exclude equality on lower bound (`gt` vs `gte`). | | `upperExcludeEquals` | bool | `false` | Exclude equality on upper bound (`lt` vs `lte`). | | `lowerRounding` | bool | `false` | If `true`, rounds the lower bound to the start of its day (`/d`). | | `upperRounding` | bool | `false` | If `true`, rounds the upper bound to the end of its day (`/d`). | #### `range-date-relative` / `range-date-relative-not` Range expressed relative to *now*. ```json { "type": "event_condition", "key": "created-at", "operator": "range-date-relative", "values": { "lowerOffset": -30, "upperOffset": 0, "lowerOffsetPeriod": "day", "upperOffsetPeriod": "day", "lowerExcludeEquals": false, "upperExcludeEquals": false, "lowerRounding": false, "upperRounding": false } } ``` | Field | Type | Default | Meaning | | -------------------- | ----------- | ------- | ------------------------------------------------------------------------------- | | `lowerOffset` | int \| null | `null` | Signed offset from now. Negative = past, positive = future. `null` = unbounded. | | `upperOffset` | int \| null | `null` | Signed offset from now. | | `lowerOffsetPeriod` | enum | `"day"` | Unit of `lowerOffset`. One of: `min`, `hour`, `day`, `week`, `month`, `year`. | | `upperOffsetPeriod` | enum | `"day"` | Unit of `upperOffset`. Same options. | | `lowerExcludeEquals` | bool | `false` | Same semantics as `range-date`. | | `upperExcludeEquals` | bool | `false` | Same semantics as `range-date`. | | `lowerRounding` | bool | `false` | Rounds the bound to the start of the period. | | `upperRounding` | bool | `false` | Rounds the bound to the end of the period. | #### `range-date-anniversary` Matches date anniversaries independent of year (e.g. "happened in the next 7 days of the year", "falls between Jan 25 and Dec 01"). Only available on DATE fields backed by the anniversary index (event `created-at` and custom date parameters that opt in). ```json // Relative mode — N days before/after today's anniversary { "values": { "mode": "relative", "lowerOffsetDays": 0, "upperOffsetDays": 7 } } // Absolute mode — month-day range (format MMDD) { "values": { "mode": "absolute", "lowerDate": "0125", "upperDate": "1201" } } ``` | Field | Type | Required when | Notes | | ----------------- | ---------------------------- | ----------------- | --------------------------------------------------- | | `mode` | `"relative"` \| `"absolute"` | always | Default `"relative"`. | | `lowerOffsetDays` | int | `mode = relative` | Signed. Negative = before, positive = after. `gte`. | | `upperOffsetDays` | int | `mode = relative` | Signed. Exclusive upper (`lt`). Default `1`. | | `lowerDate` | string (`MMDD`) | `mode = absolute` | e.g. `"0125"` = Jan 25. | | `upperDate` | string (`MMDD`) | `mode = absolute` | Inverted ranges wrap around the year boundary. | #### `range-date-dayversary` Matches a weekday + hour-of-day window. Only valid on `created-at`. Use it for recurring weekly schedules — e.g. "orders on Friday evenings". ```json { "type": "event_condition", "key": "created-at", "operator": "range-date-dayversary", "values": { "lowerDay": "518", "upperDay": "523" } } ``` | Field | Type | Format | Example | | ---------- | ------ | -------------------------------------------------------------------------------- | ------------------------------ | | `lowerDay` | string | `DHH` — 1 digit weekday (`1` = Monday … `7` = Sunday) + 2 digit hour (`00`–`23`) | `"518"` = Friday 18:00, `gte`. | | `upperDay` | string | same | `"523"` = Friday 23:00, `lte`. | If the lower day/hour is *greater* than the upper (e.g. Friday → Monday), the range wraps across the week boundary automatically. #### `range-date-timeversary` Matches a time-of-day window independent of the date. ```json // Relative mode — N minutes around the current time { "values": { "mode": "relative", "lowerOffsetMinutes": -15, "upperOffsetMinutes": 15 } } // Absolute mode — HH:MM range (format HHMM or HH:MM — colons are stripped) { "values": { "mode": "absolute", "lowerTime": "0900", "upperTime": "1800" } } ``` | Field | Type | Required when | Notes | | -------------------- | ---------------------------- | ----------------- | ------------------------------------- | | `mode` | `"relative"` \| `"absolute"` | always | Default `"relative"`. | | `lowerOffsetMinutes` | int | `mode = relative` | Signed, `gte`. | | `upperOffsetMinutes` | int | `mode = relative` | Signed, `lt`. Default `1`. | | `lowerTime` | string (`HHMM`) | `mode = absolute` | 24h. `"0900"` = 09:00. | | `upperTime` | string (`HHMM`) | `mode = absolute` | Inverted ranges wrap across midnight. | ### Geographic #### `geopoint-distance` Matches events within a radius of a `(longitude, latitude)` point. Only applies to geopoint fields (custom event parameters configured as geopoint). ```json { "type": "event_condition", "key": "store_visit.location", "operator": "geopoint-distance", "values": { "longitude": -3.7038, "latitude": 40.4168, "distance": 25 } } ``` | Field | Type | Required | Notes | | ----------- | ------- | -------- | ----------------------------------- | | `longitude` | decimal | yes | WGS-84 longitude. | | `latitude` | decimal | yes | WGS-84 latitude. | | `distance` | decimal | yes | Radius. Default unit is kilometres. | All three fields are required; missing any raises a validation error. ### `values` shape — quick reference | Operator | `values` shape | | | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | - | --- | | `match-all`, `match-none` | ignored | | | | `exists`, `exists-not` | `[]` | | | | `matches-bool` | `[true]` or `[false]` | | | | `contains(-not)`, `startswith(-not)`, `endswith(-not)`, `matches-string(-not)` | `string[]`, optional leading `"&&"` / \`" | | "\` | | `matches-number(-not)` | `number[]`, optional leading `"&&"` / \`" | | "\` | | `range-number(-not)` | `{ lowerNumber, upperNumber, lowerExcludeEquals?, upperExcludeEquals? }` | | | | `matches-date` | `{ date: "YYYY-MM-DD" }` | | | | `range-date(-not)` | `{ lowerDate, upperDate, lowerExcludeEquals?, upperExcludeEquals?, lowerRounding?, upperRounding? }` | | | | `range-date-relative(-not)` | `{ lowerOffset, upperOffset, lowerOffsetPeriod, upperOffsetPeriod, lower/upperExcludeEquals?, lower/upperRounding? }` | | | | `range-date-anniversary` | `{ mode: "relative", lowerOffsetDays, upperOffsetDays }` or `{ mode: "absolute", lowerDate: "MMDD", upperDate: "MMDD" }` | | | | `range-date-dayversary` | `{ lowerDay: "DHH", upperDay: "DHH" }` (only on `created-at`) | | | | `range-date-timeversary` | `{ mode: "relative", lowerOffsetMinutes, upperOffsetMinutes }` or `{ mode: "absolute", lowerTime: "HHMM", upperTime: "HHMM" }` | | | | `geopoint-distance` | `{ longitude, latitude, distance }` | | | ## Date histogram parameters | Parameter | Type | Description | Example values | | ------------------- | ------- | ---------------------------------- | --------------------------------------------------- | | `field` | string | Date field to aggregate on. | `@created-at`, `@ecommerce_order_create.created-at` | | `calendar_interval` | string | Calendar-aware intervals. | `1d`, `1w`, `1M`, `1y` | | `fixed_interval` | string | Fixed time intervals. | `1h`, `6h`, `30m`, `1d` | | `format` | string | Date format for bucket keys. | `yyyy-MM-dd`, `HH:00`, `E` | | `time_zone` | string | Timezone for date calculations. | `UTC`, `Europe/Madrid`, `America/New_York` | | `offset` | string | Time offset for bucket boundaries. | `+1h`, `-30m` | | `min_doc_count` | integer | Minimum document count per bucket. | `0`, `1` | | `extended_bounds` | object | Extend bounds beyond data range. | `{"min":"2024-01-01","max":"2024-12-31"}` | ## Terms aggregation parameters | Parameter | Type | Description | Example values | | --------------- | ------------ | ---------------------------------------- | ------------------------------------------------------ | | `field` | string | Field to group by. | `@product-name`, `@event-type`, `@audience-categories` | | `size` | integer | Number of top terms to return. | `5`, `10`, `100` | | `missing` | string | Label for documents with missing values. | `"unknown"`, `"N/A"` | | `order` | object | Sort order for terms. | `{"salesCount":"desc"}`, `{"_count":"asc"}` | | `min_doc_count` | integer | Minimum document count per term. | `1`, `5` | | `include` | array/string | Include only specific terms. | `["product1","product2"]` | | `exclude` | array/string | Exclude specific terms. | `["excluded1","excluded2"]` | ## What's next - **[Audience query filter](/product-api/audience-query-filter)** — the contact-side filter, with segment and group\_event conditions. - **[API Reference](/product-api/reference)** — endpoints that accept this filter (`/event/search`, `/event/scroll`, `/event/aggregations`). --- URL: https://docs.instasent.com/product-api/webhooks # Webhooks Webhooks for the Product API. > **Note**: This page is coming soon. --- URL: https://docs.instasent.com/product-api/mcp # MCP Model Context Protocol server for the Product API. > **Note**: This page is coming soon.