Skip to content

Compute Cohort

POST
/analytics/sales-report/cohort

The whole Sales Report for one filter, in one round trip.

Cohort plus funnel, pipeline, revenue and drill-down - thirteen sections resolved from a single deal cohort, which is the reason this is one call and not thirteen: the sections agree because they are computed over the same resolved set, not re-derived.

Reads; writes nothing. It is a POST because the filter is a tree, not because it mutates - and it is tiered read for that reason.

The filter is extra="forbid": a misspelled key is a 422 naming it, rather than a silently wider result with a 200 on it.

Authorizations

Parameters

Query Parameters

tz

IANA timezone the local-day window bounds are computed in, e.g. Asia/Tokyo. Changes which deals fall inside the date range.

string
default: UTC

IANA timezone the local-day window bounds are computed in, e.g. Asia/Tokyo. Changes which deals fall inside the date range.

Header Parameters

X-Target-Org-Id
Any of:
string

Request Body required

JSON shape consumed by POST /api/sales-report/cohort.

Empty groups are omitted by callers; absent groups treated as empty.

object
audiences
Any of:

rec_type / audiences — Lead.rec_type is SmallInteger and audience_id is Integer; both appliers coerce and skip what will not parse.

object
in
Array<string>
<= 1000 items
not
boolean
campaign
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
channel
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
country
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
date
required
object
from
required
string format: date
to
required
string format: date
grading_bucket
Any of:

grading_bucket — a CLOSED vocabulary (GRADING_BUCKETS). An unknown bucket produces no band clause and widens to the full book.

object
in
Array<string>
<= 1000 items
Allowed values: high med low
not
boolean
granularity
Any of:
string
Allowed values: day week month
headcount
Any of:

headcount — a CLOSED vocabulary (HEADCOUNT_BANDS), and the exact list /filter-options hands the client.

object
in
Array<string>
<= 1000 items
Allowed values: 1-10 11-50 51-200 201-1000 1001+
not
boolean
industry
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
lifecycle
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
objections
Any of:

objections — codes live in the PER-ORG cb_objection_codes catalog, so only the SHAPE can be checked here. Blank is the one thing the applier provably cannot use.

object
in
Array<string>
<= 1000 items
not
boolean
pipeline
Any of:

pipeline / stage — CBDeal.pipeline_id and .stage_id are real PostgresUUID columns, so a non-UUID can only ever fail open.

object
in
Array<string>
<= 1000 items
not
boolean
pipeline_target
Any of:
string
Allowed values: prospects_meeting prospects_revenue customers_revenue
rec_type
Any of:

rec_type / audiences — Lead.rec_type is SmallInteger and audience_id is Integer; both appliers coerce and skip what will not parse.

object
in
Array<string>
<= 1000 items
not
boolean
rep
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_calendar
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_chat
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_crm
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_email
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_enrichment
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_forms
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_intent
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_linkedin
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_sequence
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_sms
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_voice
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
signals_website
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean
stage
Any of:

pipeline / stage — CBDeal.pipeline_id and .stage_id are real PostgresUUID columns, so a non-UUID can only ever fail open.

object
in
Array<string>
<= 1000 items
not
boolean
tier
Any of:

One filter group — list of values + optional NOT toggle.

in_ is capped at MAX_FILTER_VALUES_PER_GROUP items of MAX_FILTER_VALUE_LENGTH characters each. Over-cap input raises during body parsing, so FastAPI answers 422 with the offending group named in detail[].loc (["body", "<group>", "in"]) and the cap in detail[].msg — no handler code, and therefore no SQL, runs.

THIS SHAPE DOES NOT NEST. in_ holds scalars and there is no group-of-groups member, so a caller cannot vary the tree depth: it is fixed at 2 (query -> group -> values) by construction, and extra="forbid" on both models keeps it that way. A depth check here would be validation that can never fire. Add one WITH the member that introduces nesting, not before it.

Do not confuse this with SegmentFilterGroup (contacts/domain/value_objects/ag_grid_models.py:111), which IS recursive and is where AC-3’s “tree depth” belongs. Different model, different route, still unbounded — see the scope note above the constants.

object
in
Array<string>
<= 1000 items
not
boolean

Responses

200

Successful Response

object
data
required

An OPEN envelope around one report’s answer.

The app wraps every one of these reports in {success, data, meta}, where meta carries the cache state, the window and - on the agency reports - failed_org_ids, the orgs that contributed NOTHING to the totals above. That field is the difference between “this client sent no email” and “we could not query this client”, so it is forwarded rather than flattened away.

Open on purpose: data is dict | list in the app and its contents grow with the product. A closed model would turn every new metric into a silently dropped one.

object
key
additional properties
any
pagination
Any of:
object
has_next
required

Whether there are more pages

boolean
limit
required

Items per page

integer
next_cursor
Any of:
string
notes
Any of:
Array<string>
page
required

Current page number (1-indexed)

integer
total
required

Total number of items

integer
total_available
Any of:
integer
total_is_lower_bound
Any of:
boolean

422

Validation Error

object
detail
Array<object>
object
loc
required
Array
msg
required
string
type
required
string