PUT /v1/projects/{projectId}/quotas. You can also send the same { version: "v2", groups[] } body as quotas on POST /v1/projects. /v1/projects/{projectId}/quota is an alias of /quotas.
A project with no quota returns 200 and { version: "v2", groups: [] }. 404 is only for a project that does not exist.
A quota holds at most 20 groups, and a group at most 50 segments. Rule groups may nest at most 10 levels deep, counting the segment’s root group as depth 1. A configuration over any of these caps is rejected with a 400.
A request the quota rules reject returns a 400 whose error is a list with one message per problem, the same body as a request with a missing or malformed field. Each message starts with where the problem is: a path into the quota, such as groups[0].segments[0].rule.children[0], or groupId/segmentId for a target or external fill entry that names a bucket the quota does not have, or names one twice. This applies to PUT /v1/projects/{projectId}/quotas, PATCH /v1/projects/{projectId}/quotas/targets, PUT /v1/projects/{projectId}/quotas/external-fills and quotas on POST /v1/projects.
Conditions
Each segment has a rule tree. Nodes arekind: "group" (AND / OR children) or kind: "condition". An attribute condition can read any participant attribute a project can target, except employment status, the children attributes and disability type / assistive technologies. Age is a numeric attribute, not age-band enums.
A participant who skipped a question matches no condition on it,
none and count included; a CHECKBOX answered with nothing selected counts as 0. A participant with no value for an attribute matches no condition on it, notIn included; when a project’s quota reads an attribute the participant has not filled in, the platform asks for it before the screener.
industry, jobTitle, companySize, seniority and jobFunctions are B2B only. On a project with targetMarketType: "b2c", a condition on any of them is rejected with a 400 that lists each one, for example groups[0].segments[0].rule.children[0]: companySize is only available on B2B projects; set the project's audience to B2B or remove this condition. This applies to PUT /v1/projects/{projectId}/quotas and to quotas on POST /v1/projects. militaryServiceStatus and workSetting are available on B2B and B2C projects.
Targets are { op: "min" }, { op: "max" }, { op: "exact" } (each with value) or { op: "track" } for counted-but-uncapped buckets. Address a group’s implicit leftover bucket with segmentId: "no-match".
Sample workflow
- Create a project via POST /v1/projects, optionally with
quotas. - Create or replace the quota with PUT /v1/projects/{projectId}/quotas (allowed on draft and published projects).
- Publish. The quota is validated again against the project: if it no longer passes, publish returns a
400startingQuotas need updating before this project can be published:followed by each error, and the project stays a draft. ChangingtargetMarketTypeon a draft leaves its quota as it is, so a draft switched tob2cafter a quota on a B2B-only attribute was saved is refused here; remove the condition or settargetMarketTypeback tob2b. Matching and invites consume the quota. A public invite is always admitted: the invite succeeds, consumption is recorded, and the bucket may go over target. There is no 409 on this path. - Read fill with GET /v1/projects/{projectId}/quotas. Each screener response that occupies a quota carries
quotaAssignment(segments[],tier,revision,assignedAt,tierChangedAt). - Subscribe to
QUOTAS.SEGMENT_FILLEDandQUOTAS.FILLEDon your webhook.
Create or replace
PUT /v1/projects/{projectId}/quotas A group or segment whoseid matches an existing one keeps its progress. An omitted or new id is a new bucket at zero. The response includes changes (created, preserved, removed, rebucketed, externalFillsDropped, overTarget) and warnings.
Update targets
PATCH /v1/projects/{projectId}/quotas/targets Sparse array. One entry per bucket; a duplicate(groupId, segmentId) is 400. Omitted buckets are untouched. Tracked-only is { "op": "track" }. Rules and progress are not rewritten.
Read
GET /v1/projects/{projectId}/quotas Config and progress together, plus derivedfilled, closed, and per-group minProtected. version discriminates the body: "v2" is the document above; "v1" is a legacy { criteria, progress, totalTarget, status } shape still returned for projects that have not been moved.
External fills
PUT /v1/projects/{projectId}/quotas/external-fills Bare array of{ groupId, segmentId, count }. Absolute counts, applied atomically.
Delete
DELETE /v1/projects/{projectId}/quotas Returns204. The next GET is the empty V2 configuration. Assignments on existing responses are not released by delete.
References
Example of how quotas are created via the Respondent Researcher Platform
