Interest Manager API Search Payload Guide

Prev Next

Interest Manager API Search Payload Guide

Last Updated: October 5, 2026

What This API Does

The Preference Search API finds contacts who have expressed interest in specific topics. You construct a payload describing which topics you want to search for, how those topics relate to each other (OR vs AND), and whether to expand the search across related topics in the hierarchy (parent/child topics).

The API returns contacts who have matching preference records.


The Three APIs

The Preference Search system provides three REST endpoints:

API Endpoint Method Purpose
Sync Search /preferencesSearch/v1/search POST Execute a search and return results immediately in the same response
Async Search /preferencesSearch/v1/async POST Submit a search that runs in the background; returns a Job ID to poll
Async Result /preferencesSearch/v1/result/{jobId} GET Retrieve results for a completed async search job

Paths are relative to the org's Apex REST base. In a managed-package org the full URL includes the package namespace: T1C_Base

https://{MyDomain}.my.salesforce.com/services/apexrest/{namespace}/preferencesSearch/v1/search

All calls require an OAuth access token (Authorization: Bearer {token}) and Content-Type: application/json. See Interest Manager Search API Setup Guide.

When to Use Sync vs Async

Scenario Recommended API
Simple search, few expected matches (<5,000 contacts) Sync
Complex AND groups with hierarchy expansion Async (avoids CPU timeout)
Large contact populations (10,000+) Async
Need results immediately for UI display Sync
Background processing / scheduled distribution Async

Complete Payload Reference

The Sync and Async APIs accept the same request payload. The only difference is how results are returned.

Full Payload Structure

{
  "searchCriteriaGroup": {
    "searchType": "INTEREST",
    "topicCriteria": {
      "topicOrList": [
        { "subjectTypeId": "a0XXXXXXXXXXX", "subjectId": "a1GXXXXXXXXXX" }
      ],
      "topicAndGroups": [
        [
          { "subjectTypeId": "a0XXXXXXXXXXX", "subjectId": "a1GXXXXXXXXXX" },
          { "subjectTypeId": "a0XXXXXXXXXXX", "subjectId": "a1GXXXXXXXXXX" }
        ]
      ],
      "expandHierarchy": true,
      "hierarchyDirection": "CHILD",
      "maxHierarchyDepth": 2
    },
    "languages": ["en", "fr"],
    "deliveryFormats": ["email", "hardcopy"],
    "reportTypes": ["RT_DAILY_PDF"],
    "fields": ["Email", "Phone"],
    "filterDNC": true
  },
  "pagination": {
    "pageSize": 500,
    "nextKey": null
  },
  "changedAfter": "2026-01-15T00:00:00.000Z"
}

Payload Parameters Explained

searchCriteriaGroup (Required)

The top-level container for all search filters. Every request must include this.

searchType

| Value | Behavior |

|
|-------|----------|-------------|
| "INTEREST" (default) | Returns contacts with any active interest matching the topic criteria | Standard distribution searches — finding who wants content on a topic |
| "RESEARCH" | Only returns contacts whose preference rules are marked research-active | Research-specific distribution — only contacts who opted into research content |

When searchType is "RESEARCH", the deliveryFormats field becomes required because research distribution always needs a delivery channel.

topicCriteria (Required)

The core of the search — defines which topics to look for.

topicOrList — OR Topics

An array of topic references. The API finds contacts interested in any one of these topics.

"topicOrList": [
  { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000001" },
  { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000002" },
  { "subjectTypeId": "a0X001AAA000002", "subjectId": "a1G001AAA000003" }
]
  • subjectTypeId — The Interest Subject Type record ID (the category, e.g. "Asset Class", "Region")
  • subjectId — Required. The Interest Subject record ID (the specific topic, e.g. "Equity", "Americas"). A topic reference without subjectId is rejected.

A contact matches if they have an interest in any one of the listed topics.

topicAndGroups — AND Groups

An array of arrays. Each inner array is an AND group — the contact must have a combined interest in all topics within the group simultaneously.

"topicAndGroups": [
  [
    { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000001" },
    { "subjectTypeId": "a0X001AAA000002", "subjectId": "a1G001AAA000004" }
  ],
  [
    { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000002" },
    { "subjectTypeId": "a0X001AAA000002", "subjectId": "a1G001AAA000004" }
  ]
]

In this example: Group 1 is [Topic A AND Topic D], Group 2 is [Topic B AND Topic D]. A contact matches if they have a combined interest for either group.

At least one of topicOrList or topicAndGroups must be provided (or both). Each AND group must contain at least one topic — an empty inner array is rejected.

##### expandHierarchy

Value Behavior
true Automatically include parent/child topics related to your specified topics
false or omitted Search only for the exact topics you specified — unless hierarchyDirection is also supplied (see below)

Note: hierarchyDirection takes precedence. If you send "expandHierarchy": false together with "hierarchyDirection": "CHILD", the API still expands to children. To guarantee no expansion, omit hierarchyDirection or set it to "NONE".

##### hierarchyDirection

Controls which direction to traverse the topic tree when expandHierarchy is true.

Value Behavior
"CHILD" Include child topics below yours
"PARENT" Include parent topics above yours
"BOTH" Include both parent and child topics
"NONE" No expansion (same as expandHierarchy=false)

If expandHierarchy is true but hierarchyDirection is omitted, the system defaults to "PARENT". Values are case-insensitive. The value is only validated when expandHierarchy is true.

##### maxHierarchyDepth

How many levels to traverse in the topic tree.

Value Meaning
1 Immediate parents/children only
2 Up to grandparent/grandchild level
3 (maximum) Up to great-grandparent/great-grandchild level

Default is 3 if omitted.

languages (Optional)

An array of language codes. When provided, only contacts whose preference rules match one of these languages are returned.

"languages": ["en", "fr", "de"]

If omitted, no language filtering is applied — all matching contacts are returned regardless of language preference.

deliveryFormats (Optional for INTEREST, Required for RESEARCH)

An array of delivery format strings. When provided, only contacts whose preference rules support one of these delivery methods are returned.

"deliveryFormats": ["email", "hardcopy", "phone"]

The response includes which specific delivery formats matched for each contact, along with the corresponding delivery endpoints (email addresses, phone numbers, etc.).

For INTEREST searches, a preference rule with no delivery options is treated as unrestricted and still matches. For RESEARCH searches the rule must list a matching delivery option.

reportTypes (Optional)

An array of report type tokens (the External_Id__c values of Interest Subject Type Attribute records, each representing one report type + format pair). When provided, only contacts whose preference rules allow one of these report types are returned.

"reportTypes": ["RT_DAILY_PDF", "RT_WEEKLY_HTML"]
  • Matching is case-insensitive; the response echoes tokens exactly as you sent them.
  • A preference rule with no report types configured is treated as unrestricted and still matches.
  • If omitted, no report type filtering is applied.
  • The fpmApiSearchConfig tool lists the valid tokens for the selected topic types.

fields (Optional)

An array of Salesforce Contact field API names to include in the response's contactDetails object.

"fields": ["Email", "Phone", "MailingCity"]

When omitted, contactDetails still contains Id, Name, Email, HasOptedOutOfEmail and the Contact fields configured for each delivery channel. Use this to retrieve additional contact fields needed for your distribution workflow without making a separate query. Use API names (namespaced for package fields, e.g. {namespace}__Field__c); an invalid field name fails the request.

filterDNC (Optional)

dncInfo is always returned for any contact that has a Do Not Contact restriction (or HasOptedOutOfEmail = true), regardless of this flag. filterDNC controls whether blocked contacts are removed:

Value Behavior
true Exclude a contact when every requested deliveryFormats value is blocked by DNC. Requires deliveryFormats; has no effect without it.
false or omitted Return all matched contacts; the caller is responsible for honouring dncInfo

Because filtering happens while building each page, totalMatches is the pre-filter count and a page may contain fewer items than pageSize.

pagination (Optional)

Controls result paging for large result sets.

pageSize

Number of contacts to return per page.

API Default Notes
Sync 1000 Set via pagination.pageSize. No enforced maximum, but keep ≤ 1000 to stay within heap/CPU limits
Async Result 1000 Set via the pageSize query parameter (max 1000). pagination in the Async submit body is ignored

nextKey (Sync)

A cursor token returned by a previous response. Pass it in your next request to retrieve the next page of results.

  • First request: omit nextKey or set to null
  • Subsequent pages: resend the identical payload with pagination.nextKey set to the value from the previous response

How sync paging works: the first call runs the search and caches the full result set; later pages are served from that cache without re-running the search. The cache is only reused when:

  • the request comes from the same Salesforce user that made the first call,
  • the search criteria are unchanged (only pagination may differ), and
  • the snapshot is younger than the configured staleness window (default 30 minutes).

If any condition fails the search is re-executed and page 1 is returned again with a new nextKey. Clients should detect this (e.g. repeated contact IDs) and restart their page loop.

Incremental sync searches: removedList is returned on the first page only (up to pageSize entries); cached follow-up pages return an empty removedList. Use the Async API for incremental runs that may remove more than one page of contacts.

For the Async Result API, nextKey is a plain numeric offset — see Async Result API — Query Parameters.

changedAfter (Optional — Incremental Search)

An ISO-8601 datetime string. When provided, the API performs an incremental search — only returning contacts whose preferences changed after this timestamp.

"changedAfter": "2026-07-01T00:00:00.000Z"

Use case: You distributed to a topic audience last week. Now you want only the contacts who have been added or modified since then, plus a list of contacts who should be removed (their interest expired).

Rules:

  • Must be in ISO-8601 format
  • Must not be in the future (validated by the API)
  • The response includes executionTimestamp — save this value and use it as changedAfter in your next incremental call

The Two Ways to Search for Topics

OR Topics — "Any of These"

When you add topics to the OR list, the API finds contacts interested in any one of those topics.

Example: Search for contacts interested in Equity OR Fixed Income OR Commodities.
A contact with an interest in just Fixed Income will be included.

AND Groups — "All of These Together"

When you create an AND group, the API finds contacts who have expressed interest in all topics in the group simultaneously (as a combined preference, not separate interests).

Example: An AND group of [Equity, Americas] finds contacts whose preference specifically covers "Equity in the Americas" as a combined interest — not contacts who separately like Equity and separately like Americas.

You can include multiple AND groups. A contact matching any one AND group is included in results.


Hierarchy Expansion

Topics are organized in a parent-child tree. For example:

Asset Class (root)
  +-- Equity
  |     +-- Large Cap Equity
  |     +-- Small Cap Equity
  +-- Fixed Income
        +-- Investment Grade
        +-- High Yield

When you enable hierarchy expansion, the API automatically includes related topics in the search:

Direction What It Does Example
CHILD Includes child topics below yours Searching "Equity" also finds contacts interested in "Large Cap Equity" or "Small Cap Equity"
PARENT Includes parent topics above yours Searching "Large Cap Equity" also finds contacts interested in "Equity"
BOTH Includes parents and children Searching "Equity" finds contacts at all levels of that branch

Hierarchy Depth controls how many levels to traverse (1 to 3). Depth 1 means only immediate parents/children. Depth 3 traverses up to great-grandparent or great-grandchild level. Default is 3 when omitted.


How Hierarchy Expansion Multiplies AND Group Complexity

This is where the most important limits come into play. With OR topics, hierarchy expansion just adds more individual topics to check — it grows linearly.

With AND groups, hierarchy expansion multiplies the number of combinations the API must search for. This is because the API needs to check every possible combination of expanded topics.

Worked Example

Your AND group: [Equity, Americas]

With hierarchy expansion (CHILD, depth 1):

  • Equity expands to: {Equity, Large Cap Equity, Small Cap Equity} (3 topics)
  • Americas expands to: {Americas, North America, Latin America} (3 topics)

Combinations the API must search: 3 x 3 = 9 combinations

  • Equity + Americas
  • Equity + North America
  • Equity + Latin America
  • Large Cap Equity + Americas
  • Large Cap Equity + North America
  • Large Cap Equity + Latin America
  • Small Cap Equity + Americas
  • Small Cap Equity + North America
  • Small Cap Equity + Latin America

How Quickly It Grows

Topics in AND Group Expansion per Topic Total Combinations
2 3 (depth 1, CHILD) 9
2 8 (depth 3, BOTH) 64
3 5 (depth 2, PARENT) 125
3 8 (depth 3, BOTH) 512
4 5 (depth 2, PARENT) 625
4 8 (depth 3, BOTH) 4,096 (capped to 2,000)
5 4 (depth 1, BOTH) 1,024
5 8 (depth 3, BOTH) 32,768 (capped to 2,000)

The formula is: (expanded topics per position) ^ (number of positions in AND group)


The Three Safety Limits

The API applies three progressive safety limits to keep the search reliable and performant:

Limit 1: Per-Group Cap — 2,000 Combinations

Each AND group can generate at most 2,000 topic combinations. If the Cartesian product would exceed 2,000, the API stops generating further combinations for that group.

What this means for you: If you have 4 topics in an AND group with deep hierarchy expansion (BOTH, depth 3), the API will only search the first 2,000 combinations it generates. Some matching contacts may not be found because their specific combination was not included in the search.

The API response signals this with isTopicExpansionTruncated = true.

Limit 2: Aggregate Cap — 10,000 Combinations Total

Across ALL your AND groups combined, the API allows a maximum of 10,000 total topic combinations. If you have multiple AND groups each generating many combinations, the API will stop processing further AND groups once the running total reaches 10,000.

Example: If you have 7 AND groups and groups 1–5 generate 2,000 each (total = 10,000), groups 6 and 7 will not be processed at all.

Why this exists: Each combination consumes system resources. Without this limit, a payload with many complex AND groups could exhaust memory or processing time, causing the entire search to fail rather than return partial results.

Limit 3: Query Strategy Routing — 500 Combinations

When the total number of combinations exceeds 500, the API automatically switches to a more robust (indexed) query strategy. This is invisible to you as a user — your results are the same — but it ensures the search remains fast even at higher volumes.

Below 500 combinations, a simpler query approach works fine. Above 500, the system needs indexed lookups to avoid slow or failing database queries.


Practical Guidance for Building Payloads

Safe Configurations (No Truncation Risk)

Configuration Estimated Combinations
10 OR topics, no hierarchy 10
10 OR topics, hierarchy CHILD depth 2 ~80
1 AND group: 2 topics, CHILD depth 1 ~9
1 AND group: 3 topics, PARENT depth 1 ~27
3 AND groups: 2 topics each, CHILD depth 1 ~27 total

Approaching Limits (May Trigger Per-Group Truncation)

Configuration Estimated Combinations
1 AND group: 4 topics, BOTH depth 2 ~625–2,000
1 AND group: 3 topics, BOTH depth 3 ~512–2,000
5 AND groups: 3 topics each, CHILD depth 2 ~500–625 total

Exceeding Limits (Will Truncate — Partial Results)

Configuration Estimated Combinations Which Limit Triggers
1 AND group: 5 topics, BOTH depth 3 Capped at 2,000 Per-group cap
3 AND groups: 4 topics each, BOTH depth 3 3 x 2,000 = 6,000 Per-group cap (each group)
6 AND groups: 4 topics each, BOTH depth 2 6 x ~1,700 = ~10,000 Aggregate cap

Using the Estimation Tool (fpmApiSearchConfig)

The API Search Configuration utility (fpmApiSearchConfig Lightning Web Component) provides a pre-flight estimation before you submit a search. When you generate a payload:

  1. The tool calculates how many topic combinations your configuration produces
  2. It displays one of three statuses:
Status Color Meaning
Topic Combinations: N Grey (Info) Your configuration is within all limits. Full coverage expected.
Topic combination limit exceeded Yellow (Warning) One or more AND groups exceed the 2,000 per-group cap. Results will be incomplete for those groups.
Aggregate topic combination limit exceeded Red (Error) Total combinations across all AND groups exceed 10,000. Some AND groups will not be processed at all.

What to Do When You See a Warning or Error

Problem Solutions
Single AND group too complex Reduce topics in the group (3 instead of 5), lower hierarchy depth (1 instead of 3), or narrow direction (PARENT instead of BOTH)
Aggregate too high Reduce the number of AND groups, or simplify individual groups
Need full coverage Split into multiple separate API calls, each with fewer AND groups

API Response Structure

All three APIs return the same response shape (with minor variations noted below).

Sync Search Response

{
  "responseType": "sync",
  "status": "SUCCESS",
  "totalMatches": 142,
  "executionTimestamp": "2026-07-20T10:30:00.000Z",
  "nextKey": "eyJhZGRlZEN1cnNvciI6...",
  "isPartialResult": false,
  "isTopicExpansionTruncated": false,
  "distributionList": [ ... ],
  "removedList": [ ... ],
  "message": null
}

Async Search Response (Submission)

{
  "responseType": "async",
  "status": "PROCESSING",
  "jobId": "7075g00000AbCdEFGHI",
  "executionTimestamp": "2026-07-20T10:30:00.000Z",
  "message": null
}

Poll the Result API with this jobId. While the job is still running it returns "status": "PROCESSING" with "message": "Search is still running" and empty lists. Keep executionTimestamp from this submission response for your next changedAfter.

Async Result Response (Retrieval)

{
  "responseType": "async",
  "status": "COMPLETED",
  "jobId": "7075g00000AbCdEFGHI",
  "totalMatches": 8420,
  "executionTimestamp": "2026-07-20T10:30:00.000Z",
  "nextKey": "1000",
  "isPartialResult": false,
  "isTopicExpansionTruncated": false,
  "distributionList": [ ... ],
  "removedList": [ ... ],
  "distributionIds": null,
  "removedIds": null,
  "message": null
}

Response Fields Explained

Field Type Description
responseType String "sync" or "async" — identifies which API produced the response
status String "SUCCESS" (sync done), "PROCESSING" (async running), "COMPLETED" (async done), "FAILED" (error)
totalMatches Integer Total number of contacts that matched (across all pages)
executionTimestamp DateTime When the search executed — save this for your next changedAfter incremental call
nextKey String Pagination cursor. If present, more results are available. Sync: opaque token for pagination.nextKey. Async Result: numeric offset for the offset query parameter
jobId String Async job identifier — use this to poll the Result API
isPartialResult Boolean true if the search hit platform row limits before finding all matches. Results are incomplete.
isTopicExpansionTruncated Boolean true if any AND group hit the 2,000 per-group cap or the 10,000 aggregate cap. Some topic combinations were not searched.
distributionList Array Full contact details for matched contacts (see below)
removedList Array Contacts removed since changedAfter (incremental search only)
distributionIds Array ID-only mode: just contact IDs without details (async with excludeDetails=true)
removedIds Array ID-only mode: removed contact IDs
message String Error or status message (null on success)

distributionList Item Structure

Each entry in distributionList represents one matched contact:

{
  "contactId": "003XXXXXXXXXXXXXXX",
  "name": "Jane Smith",
  "contactDetails": {
    "Email": "jane.smith@company.com",
    "Phone": "+1-555-0123",
    "MailingCity": "Toronto"
  },
  "deliveryFormats": ["email", "hardcopy"],
  "languages": ["en", "fr"],
  "reportTypes": ["RT_DAILY_PDF"],
  "deliveryFieldsByFormat": {
    "email": ["Email", "Secondary_Email__c"],
    "hardcopy": ["MailingAddress"]
  },
  "deliveryEndpointsByFormat": {
    "email": ["jane.smith@company.com", "j.smith@personal.com"],
    "hardcopy": ["123 Bay St, Toronto, ON"]
  },
  "dncInfo": {
    "hasDNC": true,
    "byDeliveryFormat": {
      "email": { "isBlocked": false, "scope": [] },
      "phone": { "isBlocked": true, "scope": ["Phone", "MobilePhone"] }
    }
  }
}
Field Description
contactId Salesforce Contact record ID
name Contact full name
contactDetails Additional Contact fields you requested via the fields parameter
deliveryFormats Which delivery formats matched for this contact
languages Which languages matched for this contact
reportTypes Which report types matched (only when the request included reportTypes)
deliveryFieldsByFormat For each delivery format, which Contact field names hold the delivery address
deliveryEndpointsByFormat For each delivery format, the actual values (email addresses, phone numbers, mailing addresses)
dncInfo Do Not Contact status. Present whenever the contact has a DNC restriction or has opted out of email; null otherwise. scope lists the blocked Contact fields, or ["ALL"] when the whole channel is blocked

removedList Item Structure (Incremental Search)

Only populated when you pass changedAfter. Each entry represents a contact whose interest has expired since your last search:

{
  "contactId": "003XXXXXXXXXXXXXXX",
  "email": "former.interest@company.com",
  "name": "John Doe"
}

Async Result API — Query Parameters

When calling GET /preferencesSearch/v1/result/{jobId}:

Parameter Type Description
offset Integer 0-based absolute position in the result set (default: 0). Pass the previous response's nextKey here.
pageSize Integer Contacts per page (default and maximum: 1000).
excludeDetails String Set to "true" to receive only distributionIds and removedIds arrays instead of full distributionList / removedList objects. Useful when you only need Contact IDs for downstream processing.
GET /services/apexrest/{namespace}/preferencesSearch/v1/result/7075g00000AbCdEFGHI?pageSize=500&offset=500

distributionList and removedList are paged together using the same offset; keep paging until nextKey is null.

HTTP status codes (Result API only):

Code Meaning
200 COMPLETED page, or PROCESSING (poll again later)
400 Missing or malformed jobId, or offset out of range
404 Job not found (wrong Id, or results already purged by the storage cleaner)
500 Job FAILED, or an error while building the page; see message

The Sync and Async submit endpoints always return HTTP 200 — check status (FAILED) and message for validation errors.


Sample Payloads by Use Case

Minimal Search — Single OR Topic

{
  "searchCriteriaGroup": {
    "topicCriteria": {
      "topicOrList": [
        { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000001" }
      ]
    }
  }
}

Research Distribution with Language and Delivery

{
  "searchCriteriaGroup": {
    "searchType": "RESEARCH",
    "topicCriteria": {
      "topicOrList": [
        { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000001" },
        { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000002" }
      ],
      "expandHierarchy": true,
      "hierarchyDirection": "CHILD",
      "maxHierarchyDepth": 1
    },
    "languages": ["en"],
    "deliveryFormats": ["email"],
    "filterDNC": true
  }
}

AND Group with Hierarchy — Finding Combined Interests

{
  "searchCriteriaGroup": {
    "topicCriteria": {
      "topicAndGroups": [
        [
          { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000001" },
          { "subjectTypeId": "a0X001AAA000002", "subjectId": "a1G001AAA000005" }
        ]
      ],
      "expandHierarchy": true,
      "hierarchyDirection": "CHILD",
      "maxHierarchyDepth": 2
    },
    "deliveryFormats": ["email", "hardcopy"]
  }
}

Incremental Search — Changes Since Last Run

{
  "searchCriteriaGroup": {
    "topicCriteria": {
      "topicOrList": [
        { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000001" }
      ]
    },
    "deliveryFormats": ["email"]
  },
  "changedAfter": "2026-07-15T08:00:00.000Z"
}

The response will include:

  • distributionList — contacts added or modified since July 15
  • removedList — contacts whose interest expired since July 15
  • executionTimestamp — save this for your next incremental call

Mixed OR + AND with Pagination

{
  "searchCriteriaGroup": {
    "topicCriteria": {
      "topicOrList": [
        { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000003" }
      ],
      "topicAndGroups": [
        [
          { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000001" },
          { "subjectTypeId": "a0X001AAA000002", "subjectId": "a1G001AAA000005" }
        ],
        [
          { "subjectTypeId": "a0X001AAA000001", "subjectId": "a1G001AAA000002" },
          { "subjectTypeId": "a0X001AAA000002", "subjectId": "a1G001AAA000005" }
        ]
      ],
      "expandHierarchy": true,
      "hierarchyDirection": "PARENT",
      "maxHierarchyDepth": 1
    },
    "languages": ["en", "fr"],
    "deliveryFormats": ["email"]
  },
  "pagination": {
    "pageSize": 500
  }
}

If the response contains "nextKey": "eyJhZGRlZC...", send the same payload again with "nextKey" added to pagination:

{
  "searchCriteriaGroup": { ... same as before ... },
  "pagination": {
    "pageSize": 500,
    "nextKey": "eyJhZGRlZC..."
  }
}

Use Cases with Hierarchy Expansion

Use Case 1: Broad Distribution Search

Goal: Find all contacts interested in anything related to Equity across all regions.

Approach: Single AND group [Equity, Global] with CHILD expansion, depth 2.

Why it works: CHILD expansion from "Equity" catches Large Cap, Small Cap, etc. CHILD expansion from "Global" catches Americas, EMEA, APAC. The Cartesian product covers all specific sub-combinations. With 5 equity sub-types and 4 regional sub-regions: 5 x 4 = 20 combinations. Well within limits.

Use Case 2: Research-Specific Multi-Region Search

Goal: Find contacts opted into research for Fixed Income products across specific regions.

Approach: Multiple AND groups:

  • [Fixed Income, Americas]
  • [Fixed Income, EMEA]
  • [Fixed Income, APAC]

With CHILD expansion, depth 1.

Why it works: Each group is simple (2 topics, modest expansion). Three groups at ~9 combinations each = 27 total. Very safe.

Use Case 3: Complex Multi-Asset Multi-Region (Approaching Limits)

Goal: Find contacts interested in any combination of 4 asset classes across 3 region groupings, including sub-topics.

Approach: Single AND group [Asset Class A, Asset Class B, Asset Class C, Region Group] with BOTH expansion, depth 2.

Risk: 4 topics each expanding to ~6 related topics = 6^4 = 1,296 combinations. Under the 2,000 per-group cap, but above the 500 indexed-query threshold. The API will use indexed queries automatically — no action needed, but the estimation tool will confirm you are in safe territory.

Use Case 4: Full Topic Matrix (Exceeding Limits — Needs Restructuring)

Goal: Find contacts matching any of 10 different asset-class + region + strategy combinations, each with hierarchy expansion.

Bad approach: 10 AND groups, each with 3-4 topics, BOTH direction, depth 3.
This produces 10 x 2,000 = 20,000 combinations — exceeds the 10,000 aggregate cap.

Better approach:

  • Split into 2 separate API calls (5 AND groups each)
  • Or reduce hierarchy depth to 1 (keeps each group under 125 combinations, total = 1,250)
  • Or narrow direction to CHILD only (reduces expansion per topic by ~50%)

Use Case 5: Incremental Distribution Pipeline

Goal: Run daily distributions, sending only to contacts whose preferences changed since the last run.

Approach:

  1. First run: Normal search (no changedAfter). Save executionTimestamp from response.
  2. Subsequent runs: Pass saved timestamp as changedAfter.
  3. Process distributionList (new/modified contacts) and removedList (contacts to unsubscribe).
  4. Save the new executionTimestamp for next run.

Quick Reference Card

Question Answer
How many topics can exist in a combined interest? Up to 10 (limited by package defined storage slots), but 3–4 is the practical expectation
How many values can I include for each topic? No hard limit on group count, but total combinations across all groups capped at 10,000
How many AND groups can I include? No hard limit on group count, but total combinations across all groups capped at 10,000
How many OR topics can I include? No hard limit; OR topics grow linearly, not exponentially
Does hierarchy depth affect limits? Yes — deeper hierarchy means more expanded topics per position, which multiplies exponentially in AND groups
Which direction is safest? PARENT or CHILD (one direction). BOTH doubles the expansion and squares the impact on AND combinations
Can I mix OR and AND in one search? Yes. OR and AND results are combined (union). They each consume from the same total, but OR is usually negligible
What happens when a limit is hit? The API still returns results — it just may miss some contacts. The response includes a flag indicating partial coverage
How do I check before submitting? Use the fpmApiSearchConfig tool — it estimates combinations and warns before you call the API
What is isPartialResult? Platform row limits were hit — too many matching records. Results are incomplete regardless of topic settings.
What is isTopicExpansionTruncated? Topic combination limits were hit — not all AND combinations were searched. Reduce group complexity.
How do I paginate? Sync: pass nextKey into pagination.nextKey of the same payload. Async Result: pass nextKey as the offset query parameter. Stop when no nextKey is returned
Sync vs Async? Sync for simple/fast searches (<5K contacts). Async for complex/large searches. Same payload for both.

Validation Rules

The API rejects requests that violate these rules (returns an error before executing):

Rule Error Condition
Request body required Empty or null body
Request body size Exceeds 250 KB
Payload parseable Malformed JSON, wrong data types, or a non-ISO-8601 changedAfter
searchCriteriaGroup required searchCriteriaGroup missing
deliveryFormats required for RESEARCH When searchType is "RESEARCH", deliveryFormats must be provided
topicCriteria required topicCriteria missing
Topic criteria required Neither topicOrList nor topicAndGroups provided
Topic references complete Any topic missing subjectId, or an empty AND group
changedAfter not in future Timestamp must be in the past
Hierarchy direction valid Must be PARENT, CHILD, BOTH, or NONE (checked only when expandHierarchy is true)
Hierarchy depth valid Must be 1, 2, or 3
Feature enabled ACE.FPM.Settings › Enabled is false for the calling user → "Insufficient permissions. Contact system administrator."

Rules are checked in the order above; only the first failure is reported.

Error response shape:

{
  "responseType": "sync",
  "status": "FAILED",
  "message": "Every topic reference must include a subjectId and AND groups cannot be empty.",
  "distributionList": []
}