openapi: 3.2.0 info: title: Infinite Audience Discovery API version: 1.0.0 termsOfService: https://nofait.ai/terms license: name: Proprietary — All Rights Reserved url: https://nofait.ai/terms description: The primary REST API for Infinite Audience. servers: - url: https://api.infiniteaudience.ai description: Production API Server security: - bearerAuth: [] tags: - name: Discovery description: Audience count and lookup (exploration) paths: /v1/schema/fields: get: summary: Discover available filter attributes operationId: getSchemaFields description: 'Returns the Attribute Catalog — every attribute you can use in the `filters` array when calling the count, lookup, crosstab, and overlap endpoints. For each attribute you''ll get its data type, a plain-English description, a suggested UI widget, and (for categorical attributes) the allowed values. Use this to build dynamic filter UIs or to validate attribute names before submitting a query. Only filterable attributes are returned — there''s no parameter to widen this set. The response is cached and refreshed nightly. Requires ''discovery'' scope.' tags: - Discovery security: - bearerAuth: [] responses: '200': description: Attribute Catalog. content: application/json: schema: type: object properties: fields: type: array items: type: object properties: field_name: type: string example: gender table_name: type: string example: demographics.consumers_v2 bq_type: type: string example: STRING description: type: string description: Plain-English description of the attribute. example: Age bracket of the consumer (e.g. 25-34) filterable: type: boolean operators: type: array items: type: string example: - '=' - '!=' - IN - NOT IN ui_hint: type: - string - 'null' example: multi_select ui_label: type: string example: Gender enum_values: type: - array - 'null' items: type: string example: - 18-24 - 25-34 - 35-44 - 45-54 - 55+ value_labels: oneOf: - type: object additionalProperties: type: string - type: 'null' description: Maps raw stored codes to human-readable labels (e.g. {"M":"Male","F":"Female"}). is_pii: type: boolean groupable: type: boolean anchor: type: boolean data_level: type: - string - 'null' category_display_name: type: - string - 'null' example: Demographics & Household category: type: string required: - field_name - table_name - bq_type - description - filterable - operators - ui_hint - ui_label - enum_values - value_labels - is_pii - groupable - anchor - data_level - category_display_name cached_at: type: string format: date-time required: - fields - cached_at '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ScopeRequired' /v1/discovery/count: post: summary: Count records matching criteria operationId: discoveryCount description: 'Returns the number of records matching the given criteria. Counts the bytes scanned toward your organization''s compute limit. Requires ''discovery'' scope. Provide exactly one of: `filters` (flat array), `filter_groups` (compound AND/OR groups), `segment_id` (a saved segment), `audience_id` (a saved audience), or `segment_ids` + `set_logic` (combine multiple segments). Sending both `filters` and `filter_groups` returns 400 `AMBIGUOUS_INPUT`; sending none returns 400 `NO_INPUT_SPECIFIED`. Count, lookup, crosstab, and overlap all preserve full group/combinator structure: each included segment''s own AND/OR groups are evaluated intact, and an audience''s excluded segments are always subtracted (AND NOT), regardless of `set_logic`.' tags: - Discovery security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: filters: type: array description: Flat filter array (recommended). items: $ref: '#/components/schemas/Filter' filter_groups: type: array description: 'Compound boolean filter groups (alternative to `filters`). Each group has a `combinator` (AND/OR) and a `filters` array. ' items: $ref: '#/components/schemas/FilterGroup' segment_id: type: string description: ID of a saved segment to count. audience_id: type: string description: ID of a saved audience to count. segment_ids: type: array items: type: string description: IDs of multiple segments to combine (requires `set_logic`). set_logic: type: string enum: - union - intersection description: 'How to combine segment_ids: union = match ANY segment (OR); intersection = match EVERY segment (AND). ' excluded_segment_ids: type: array items: type: string description: 'Ad-hoc "A minus B" preview — segments to subtract from the `segment_ids` composition, without persisting an audience. Only meaningful alongside `segment_ids`; do not send `set_logic: ''exclusion''` (invalid) — exclusion is always modeled via this separate param. ' group_by: type: string description: Optional attribute to group counts by (e.g. 'state', 'gender'). responses: '200': description: Record count result. content: application/json: schema: type: object properties: count: type: integer matched_count: type: integer description: 'Only present when the resolved composition includes at least one included matched-subtype segment — the subset of `count` that came from an identity-resolution match. ' groups: type: array description: 'Present when `group_by` is specified. ' items: type: object properties: group_value: type: string count: type: integer required: - group_value - count example: - group_value: California count: 1200000 - group_value: Texas count: 980000 breakdowns: type: object description: 'Present by default (unless `group_by` is set) when the resolved composition supports them — a single filter-based segment/audience or raw filters. Not yet available for non-filter or mixed-subtype compositions. ' properties: age: type: array items: type: object properties: bucket: type: string count: type: integer gender: type: array items: type: object properties: value: type: string bucket: type: string count: type: integer geography: type: array items: type: object properties: value: type: string bucket: type: string count: type: integer behavioral: type: array items: type: object properties: value: type: string bucket: type: string count: type: integer financial: type: array items: type: object properties: value: type: string bucket: type: string count: type: integer warnings: type: array items: type: string description: 'Present only when the resolved segment_id/segment_ids/ audience_id composition includes a non-filter or mixed subtype AND `group_by`/breakdowns were requested — those are not yet available for non-filter or mixed-subtype compositions, so the count is still returned but without the requested grouping. ' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ScopeRequired' '422': $ref: '#/components/responses/AggregationFloorNotMet' '429': $ref: '#/components/responses/RateLimited' '502': $ref: '#/components/responses/QueryExecutionError' /v1/discovery/lookup: post: summary: Return masked sample rows matching criteria operationId: discoveryLookup description: 'Returns up to 25 masked sample rows matching the criteria. PII attributes are masked or omitted. Requires ''discovery'' scope. Provide exactly one of: `filters`, `filter_groups`, `segment_id`, `audience_id`, or `segment_ids` + `set_logic` (same input shapes as /v1/discovery/count). Sending more than one returns 400 `AMBIGUOUS_INPUT`; sending none returns 400 `NO_INPUT_SPECIFIED`. If you pass a segment, audience, or filter groups, full group/combinator structure is preserved — each segment''s own AND/OR groups are evaluated intact, multiple composed segments are combined by `set_logic`, and an audience''s excluded segments are always subtracted (AND NOT), regardless of `set_logic`.' tags: - Discovery security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: filters: type: array description: Flat filter array (recommended). items: $ref: '#/components/schemas/Filter' filter_groups: type: array description: Compound boolean filter groups. items: $ref: '#/components/schemas/FilterGroup' segment_id: type: string description: ID of a saved segment to sample from. audience_id: type: string description: ID of a saved audience to sample from. segment_ids: type: array items: type: string description: IDs of multiple segments to combine (requires `set_logic`). set_logic: type: string enum: - union - intersection description: 'How to combine segment_ids: union = match ANY segment (OR); intersection = match EVERY segment (AND). ' excluded_segment_ids: type: array items: type: string description: 'Ad-hoc "A minus B" preview — segments to subtract from the `segment_ids` composition, without persisting an audience. Only meaningful alongside `segment_ids`; do not send `set_logic: ''exclusion''` (invalid). ' limit: type: integer minimum: 1 maximum: 25 default: 10 fields: type: array minItems: 1 maxItems: 30 uniqueItems: true items: type: string description: Optional non-PII audience fields to preview. Defaults to a small demographic selection. Discover field names with GET /v1/catalog/fields. responses: '200': description: Sample rows. content: application/json: schema: type: object properties: rows: type: array description: Up to limit sample rows containing only the selected non-PII audience fields. items: type: object additionalProperties: true total_matched: type: integer description: Total records matching the criteria (may exceed rows.length, which is capped at `limit`). available_fields: type: array items: type: string description: Field names present on the returned rows (after PII masking/omission). required: - rows - total_matched - available_fields '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ScopeRequired' '422': $ref: '#/components/responses/AggregationFloorNotMet' '429': $ref: '#/components/responses/RateLimited' '502': $ref: '#/components/responses/QueryExecutionError' /v1/discovery/crosstab: post: summary: Cross-tabulate two dimensions across a population operationId: discoveryCrosstab description: 'Returns a pivot table of counts for two categorical dimensions (e.g. state × gender). Requires ''discovery'' scope. Provide exactly one of: `filters`, `filter_groups`, `segment_id`, `audience_id`, or `segment_ids` + `set_logic` to define the population (same input shapes as /v1/discovery/count). Sending more than one returns 400 `AMBIGUOUS_INPUT`; sending none returns 400 `NO_INPUT_SPECIFIED`. A 400 `SAME_FIELD` is returned if `row_field` equals `col_field`. If you pass a segment, audience, or filter groups, full group/combinator structure is preserved — each segment''s own AND/OR groups are evaluated intact, multiple composed segments are combined by `set_logic`, and an audience''s excluded segments are always subtracted (AND NOT), regardless of `set_logic`. A total population below the 50-record privacy floor returns 422. Individual pivot cells may contain fewer than 50 records when the total population qualifies.' tags: - Discovery security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: - row_field - col_field properties: filters: type: array description: Flat filter array defining the population. items: $ref: '#/components/schemas/Filter' filter_groups: type: array description: Compound boolean filter groups (alternative to `filters`). items: $ref: '#/components/schemas/FilterGroup' segment_id: type: string description: ID of a saved segment defining the population. audience_id: type: string description: ID of a saved audience defining the population. segment_ids: type: array items: type: string description: IDs of multiple segments to combine (requires `set_logic`). set_logic: type: string enum: - union - intersection description: 'How to combine segment_ids: union = match ANY segment (OR); intersection = match EVERY segment (AND). ' excluded_segment_ids: type: array items: type: string description: 'Ad-hoc "A minus B" preview — segments to subtract from the `segment_ids` composition, without persisting an audience. Only meaningful alongside `segment_ids`. ' row_field: type: string minLength: 1 maxLength: 64 description: Attribute for the row dimension (e.g. "state"). col_field: type: string minLength: 1 maxLength: 64 description: Attribute for the column dimension (e.g. "gender"). Must differ from row_field. row_label: type: string maxLength: 80 description: Human-readable row label (defaults to row_field). col_label: type: string maxLength: 80 description: Human-readable column label (defaults to col_field). max_rows: type: integer minimum: 1 maximum: 50 default: 20 description: Max distinct row values, ordered by total count desc. max_cols: type: integer minimum: 1 maximum: 20 default: 10 description: Max distinct column values, ordered by total count desc. example: segment_id: seg_1a2b3c row_field: state col_field: gender row_label: State col_label: Gender max_rows: 10 responses: '200': description: Pivot table result. content: application/json: schema: type: object required: - row_field - col_field - row_label - col_label - rows - columns - cells - total_count properties: row_field: type: string col_field: type: string row_label: type: string description: Echoes row_label (defaults to row_field). col_label: type: string description: Echoes col_label (defaults to col_field). rows: type: array description: Distinct row values, ordered by total count desc. items: type: string columns: type: array description: Distinct column values, ordered by total count desc. items: type: string cells: type: object description: cells[rowValue][colValue] = record count. additionalProperties: type: object additionalProperties: type: integer displayed_count: type: integer description: Sum of the returned visible cells. truncated: type: boolean description: True when row or column limits omit categories. total_count: type: integer description: Total records matching the population input. example: displayed_count: 2000347 truncated: false row_field: state col_field: gender row_label: State col_label: Gender rows: - CA - TX columns: - F - M cells: CA: F: 610230 M: 588112 TX: F: 402881 M: 399124 total_count: 2000347 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ScopeRequired' '422': $ref: '#/components/responses/AggregationFloorNotMet' '429': $ref: '#/components/responses/RateLimited' '502': $ref: '#/components/responses/QueryExecutionError' /v1/discovery/overlap: post: summary: Calculate record overlap between two criteria sets operationId: discoveryOverlap description: 'Computes the number and percentage of records that appear in both of two criteria sets, plus side-by-side demographic deltas. Requires ''discovery'' scope. Each side (`a` and `b`) independently accepts exactly one discovery input shape (`filters`, `filter_groups`, `segment_id`, `audience_id`, or `segment_ids` + `set_logic`) — the legacy flat `audience_id_a` / `audience_id_b` fields are also still accepted. A side that provides more than one shape, or combines the structured object with its legacy field, returns 400 `AMBIGUOUS_INPUT`. A side that provides nothing returns 400 `NO_INPUT_SPECIFIED`. When one side is a raw `filters`/`filter_groups` input and the other resolves to a segment the fast path cannot express (a non-filter segment, or a filter segment with a `record_limit`), the raw side is validated against the live Field Catalog and composed server-side; invalid criteria return 400 `INVALID_FILTER`. Each side preserves full group/combinator structure independently: if a side is a segment, audience, or filter groups, each segment''s own AND/OR groups are evaluated intact, multiple composed segments are combined by `set_logic`, and an audience''s excluded segments are always subtracted (AND NOT), regardless of `set_logic` — before the two sides are joined for the overlap count.' tags: - Discovery security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: a: $ref: '#/components/schemas/DiscoveryInput' description: Side A. Mutually exclusive with `audience_id_a`. b: $ref: '#/components/schemas/DiscoveryInput' description: Side B. Mutually exclusive with `audience_id_b`. audience_id_a: type: string description: (Legacy) First audience ID. Mutually exclusive with `a`. audience_id_b: type: string description: (Legacy) Second audience ID. Mutually exclusive with `b`. example: a: segment_id: seg_1a2b3c b: audience_id: aud_9z8y7x responses: '200': description: Overlap result with demographic deltas. content: application/json: schema: type: object required: - a_name - b_name - a_count - b_count - overlap_count - overlap_pct - demographic_deltas properties: a_name: type: string b_name: type: string a_count: type: integer b_count: type: integer overlap_count: type: integer overlap_pct: type: number description: Overlap as a percentage of the smaller side. demographic_deltas: type: array description: 'Side-by-side demographic comparison. Each entry: {metric, label, a, b, delta}. ' items: type: object required: - metric - label - a - b - delta properties: metric: type: string label: type: string a: type: - number - 'null' b: type: - number - 'null' delta: type: - number - 'null' distribution_a: type: array description: Optional paired distribution buckets for side A. items: $ref: '#/components/schemas/DistributionBucket' distribution_b: type: array description: Optional paired distribution buckets for side B. items: $ref: '#/components/schemas/DistributionBucket' distribution_field: type: string description: Attribute the paired distributions are bucketed on. distribution_label: type: string description: Human-readable label for the distribution attribute. age_distribution_a: type: array items: $ref: '#/components/schemas/DistributionBucket' age_distribution_b: type: array items: $ref: '#/components/schemas/DistributionBucket' gender_distribution_a: type: array items: $ref: '#/components/schemas/DistributionBucket' gender_distribution_b: type: array items: $ref: '#/components/schemas/DistributionBucket' warnings: type: array items: type: string description: 'Present only when at least one side resolves to a non-filter or mixed-subtype composition. Two cases: "One or both sides had no composable segments" when a side''s composition is empty (counts are all 0); or "Demographic breakdowns are not yet available for non-filter or mixed-subtype compositions" when counts/ overlap were computed directly but demographic_deltas could not be (returned as an empty array in that case). ' example: a_name: CA Homeowners b_name: TX Families a_count: 1204551 b_count: 988102 overlap_count: 51233 overlap_pct: 5.2 demographic_deltas: - metric: median_age label: Median age a: 44 b: 39 delta: -5 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ScopeRequired' '422': $ref: '#/components/responses/AggregationFloorNotMet' '502': $ref: '#/components/responses/QueryExecutionError' components: schemas: ErrorResponse: type: object required: - error additionalProperties: true properties: error: type: string description: 'Stable machine-readable error code (e.g. `INVALID_STATUS_TRANSITION`, `BILLING_INSUFFICIENT_BALANCE`). Always present. ' message: type: string description: Human-readable explanation of the error. code: type: string description: 'Alternate machine-readable code — present on some endpoints as an alias for `error` for backward compatibility. ' request_id: type: string description: Opaque support/debug identifier when available. next_step: type: string description: Public action for correcting the request or retrying later. issues: type: array items: $ref: '#/components/schemas/FilterValidationIssue' Filter: type: object description: A single filter condition for audience discovery or job execution required: - field - op properties: field: type: string description: Canonical catalog field_name (e.g. age or state). ZIP5 also accepts zip, zip_code, or postal_code as input aliases; saved responses use zip5. op: type: string enum: - '=' - '!=' - < - <= - '>' - '>=' - IN - NOT IN - BETWEEN - LIKE - ARRAY_CONTAINS - ARRAY_CONTAINS_ANY - IS NULL - IS NOT NULL description: Comparison operator text_comparison: type: string enum: - exact - case_insensitive description: 'Optional STRING comparison mode for =, !=, IN, NOT IN, and LIKE, or ARRAY membership mode for ARRAY_CONTAINS and ARRAY_CONTAINS_ANY. Defaults to exact. Case-insensitive mode applies Unicode case folding to both the stored value and the supplied value. ' trim_whitespace: type: boolean description: 'Optionally trim leading and trailing whitespace on both sides of a STRING or ARRAY comparison. Defaults to false; independent of text_comparison. ' value: oneOf: - type: string - type: number - type: boolean - type: array items: oneOf: - type: string - type: number description: 'The value to match against. For state, a published two-letter code or full State name is accepted and normalized to the code. Scalar types (`string`, `number`, `boolean`) are used for single-value operators (`=`, `!=`, `<`, `<=`, `>`, `>=`, `LIKE`, `ARRAY_CONTAINS`). Pass an **array** for `IN`, `NOT IN`, and `ARRAY_CONTAINS_ANY` — pass the array directly with no comma-encoding. `BETWEEN` takes a two-element array `[low, high]`. Omit entirely for `IS NULL` / `IS NOT NULL` checks. ' DiscoveryInput: type: object description: 'One side of a discovery operation — provide exactly one of `filters`, `filter_groups`, `segment_id`, `audience_id`, or `segment_ids` + `set_logic`. Providing more than one returns 400 `AMBIGUOUS_INPUT`; providing none returns 400 `NO_INPUT_SPECIFIED`. ' properties: filters: type: array description: Flat filter list. items: $ref: '#/components/schemas/Filter' filter_groups: type: array description: Compound boolean filter groups. items: $ref: '#/components/schemas/FilterGroup' segment_id: type: string description: ID of a saved segment. audience_id: type: string description: ID of a saved audience. segment_ids: type: array items: type: string description: IDs of multiple segments to combine (requires `set_logic`). set_logic: type: string enum: - union - intersection description: 'How to combine segment_ids: union = match ANY segment (OR); intersection = match EVERY segment (AND). ' DistributionBucket: type: object description: One bucket of a categorical distribution. required: - label - count properties: label: type: string count: type: integer FilterGroup: type: object description: "A group of Filter conditions. All `filters` within a single group are **always ANDed together** — there is no per-group intra-operator. The `combinator` attribute is an **inter-group** operator: it controls how this group is joined to the immediately preceding group in the array. The `combinator` on the first group (index 0) is always ignored.\n\nExample — `(city = \"NYC\" AND state = \"NY\") OR (state = \"CA\")`:\n```json [\n { \"id\": \"g1\", \"filters\": [{\"field\":\"city\",\"op\":\"=\",\"value\":\"NYC\"},\n {\"field\":\"state\",\"op\":\"=\",\"value\":\"NY\"}],\n \"combinator\": \"AND\" },\n { \"id\": \"g2\", \"filters\": [{\"field\":\"state\",\"op\":\"=\",\"value\":\"CA\"}],\n \"combinator\": \"OR\" }\n] ```\nGroup g1's `combinator` is irrelevant (it is the first group). Group g2's `combinator: \"OR\"` means the result is `(g1) OR (g2)`.\n" required: - id - filters - combinator properties: id: type: string description: 'Stable identifier (UUID or NanoID). Used to track groups across edits. ' filters: type: array items: $ref: '#/components/schemas/Filter' description: 'One or more filter conditions. All conditions in this array are combined with AND. Provide at least one filter per group. ' combinator: type: string enum: - AND - OR description: '**Inter-group operator.** Joins this group to the previous group in the array. `AND` narrows the result; `OR` broadens it. Ignored on the first group (index 0) — that group has no predecessor to join. ' FilterValidationIssue: type: object required: - path - code - message properties: path: type: string description: JSON path to the invalid filter field or value. code: type: string enum: - INVALID_FILTER_SHAPE - UNKNOWN_FILTER_FIELD - FIELD_NOT_FILTERABLE - UNSUPPORTED_FILTER_OPERATOR - UNSUPPORTED_TEXT_COMPARISON - INVALID_FILTER_VALUE - INVALID_ENUM_VALUE - AMBIGUOUS_ENUM_VALUE message: type: string suggestions: type: array items: type: string responses: ScopeRequired: description: 'Token is valid but lacks the required scope for this endpoint. Check the endpoint description for the required scope (`discovery`, `purchase`, or `account`). ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: missing_scope: value: error: SCOPE_REQUIRED message: This endpoint requires the creator scope. RateLimited: description: 'Request throttled. Two distinct conditions return 429: **Rate limit** — too many requests per minute for your tier (`error: rate_limit_exceeded`); retry after `retry_after_seconds`. **Compute ceiling** — org-level query-scan budget exhausted (`error: compute_limit_exceeded`); resets hourly or on plan upgrade. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: rate_limit: value: error: rate_limit_exceeded message: Rate limit of 100 requests/minute exceeded for tier 'pilot'. retry_after_seconds: 45 compute_limit: value: error: compute_limit_exceeded message: Org compute ceiling exceeded. Upgrade your plan or wait for the hourly reset. Unauthorized: description: 'Missing or invalid Bearer token. Obtain one via POST /v1/auth/token. When a token was supplied but rejected, `code` distinguishes `TOKEN_EXPIRED` (the token''s lifetime has passed — request a new one via POST /v1/auth/token and retry) from `TOKEN_INVALID` (malformed or revoked — re-authenticate). ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: missing_token: value: error: 'Unauthorized: Missing or invalid Authorization header' token_expired: value: error: Unauthorized code: TOKEN_EXPIRED message: Your session has expired. Please sign in again. QueryExecutionError: description: 'The underlying analytics query failed, or (`AGGREGATION_FLOOR_NOT_MET`) a non-discovery route encountered a result below the privacy floor. `message` is always a friendly, client-safe description — the raw backend error is never returned. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: aggregation_floor_not_met: value: error: Bad Gateway code: AGGREGATION_FLOOR_NOT_MET message: For privacy reasons, a minimum of 50 records is required to view this information. table_missing: value: error: Bad Gateway code: BQ_TABLE_MISSING message: This data is missing — it may have expired or the original upload never finished processing. Try re-uploading, or contact support if this persists. AggregationFloorNotMet: description: The result is below the minimum population that can be returned while preserving privacy. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: Privacy threshold not met code: AGGREGATION_FLOOR_NOT_MET message: For privacy reasons, a minimum of 50 records is required to view this information. BadRequest: description: 'Invalid request — malformed body, missing required attribute, or failed validation. See `error` and `message` for details. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: validation_error: value: error: Bad Request code: MISSING_SEGMENTS message: segment_ids is required for filter audiences. securitySchemes: oidcBearer: type: http scheme: bearer bearerFormat: OAuth access token description: Person-bound MCP OAuth bearer with the openid scope for UserInfo. bearerAuth: type: http scheme: bearer description: 'Bearer access token obtained from POST /v1/auth/token. Include in all protected requests as: Authorization: Bearer '