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 withoutsubjectIdis 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:
hierarchyDirectiontakes precedence. If you send"expandHierarchy": falsetogether with"hierarchyDirection": "CHILD", the API still expands to children. To guarantee no expansion, omithierarchyDirectionor 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
fpmApiSearchConfigtool 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
nextKeyor set tonull - Subsequent pages: resend the identical payload with
pagination.nextKeyset 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
paginationmay 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:
removedListis returned on the first page only (up topageSizeentries); cached follow-up pages return an emptyremovedList. 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 aschangedAfterin 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:
- The tool calculates how many topic combinations your configuration produces
- 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) andmessagefor 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 15removedList— contacts whose interest expired since July 15executionTimestamp— 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:
- First run: Normal search (no
changedAfter). SaveexecutionTimestampfrom response. - Subsequent runs: Pass saved timestamp as
changedAfter. - Process
distributionList(new/modified contacts) andremovedList(contacts to unsubscribe). - Save the new
executionTimestampfor 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": []
}