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
IANA timezone the local-day window bounds are computed in, e.g. Asia/Tokyo. Changes which deals fall inside the date range.
IANA timezone the local-day window bounds are computed in, e.g. Asia/Tokyo. Changes which deals fall inside the date range.
Header Parameters
Request Body required
JSON shape consumed by POST /api/sales-report/cohort.
Empty groups are omitted by callers; absent groups treated as empty.
object
rec_type / audiences — Lead.rec_type is SmallInteger and
audience_id is Integer; both appliers coerce and skip what will not
parse.
object
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
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
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
object
grading_bucket — a CLOSED vocabulary (GRADING_BUCKETS). An unknown
bucket produces no band clause and widens to the full book.
object
headcount — a CLOSED vocabulary (HEADCOUNT_BANDS), and the exact list
/filter-options hands the client.
object
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
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
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
pipeline / stage — CBDeal.pipeline_id and .stage_id are real
PostgresUUID columns, so a non-UUID can only ever fail open.
object
rec_type / audiences — Lead.rec_type is SmallInteger and
audience_id is Integer; both appliers coerce and skip what will not
parse.
object
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
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
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
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
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
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
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
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
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
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
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
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
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
pipeline / stage — CBDeal.pipeline_id and .stage_id are real
PostgresUUID columns, so a non-UUID can only ever fail open.
object
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
Responses
200
Successful Response
object
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
object
Whether there are more pages
Items per page
Current page number (1-indexed)
Total number of items
422
Validation Error