Limit Assignments Management API
Overview
This is the single-assignment counterpart of the Bulk Limit Assignment API: instead of attaching one assignment to a large, filtered population in a run, it creates, reads, updates, activates, ends, and validates one assignment at a time. It documents the Management API surface for the manager's own Limit Assignments REST endpoints (/api/limit-bindings) — the same lifecycle (DRAFT → ACTIVE → ENDED, no delete — ending is the removal), addressed portably instead of by internal id.
Every address on this page is under /apiops/projects/{projectName} and follows the shared Common Contract: the stable error envelope, correlation IDs, and bounded pagination. A caller outside the project's scope always gets 404 resourceNotFound, never 403. Assignments are project-owned (their subjects are consumers, clients, applications, organizations, proxies, or rules of a project), so the admin (platform) project has no subjects of its own and a platform-scoped call is refused with 400 malformedRequest (params.reason = platformScopeNotSupported).
References on this surface are portable — no Mongo id ever crosses the boundary. A subject is addressed by the public key its type is already known by (a consumer's username, an API client's clientId, an application's name, an organization's code, an API proxy's name); an AUDIENCE_RULE subject is identified by the assignment itself. Plans, proxies, groups, and environments are addressed by name the same way. An unknown reference is 404 resourceNotFound naming the field.
Not exposed here: the operator override pin/release (a Manager-screen recovery control that marks a row so a portal-subscription sync will not re-point it) and the covering-API-proxy / referencing-plan lookups (screen conveniences) — a pipeline reads the assignments it needs through the list filters instead.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /limit-assignments | List the project's assignments, newest first by default |
GET | /limit-assignments/{bindingKey} | One assignment by its key |
POST | /limit-assignments | Create an assignment (201 + Location) |
PUT | /limit-assignments/{bindingKey} | Replace an assignment's editable content |
POST | /limit-assignments/{bindingKey}/actions/activate | Move a DRAFT assignment to ACTIVE |
POST | /limit-assignments/{bindingKey}/actions/end | Move an ACTIVE assignment to ENDED |
POST | /limit-assignments/actions/validate | Run every save-time rule without writing anything |
List assignments
GET /limit-assignments?family&status&subjectType&subjectKey&page&size&sort sorts by assignedAt,desc by default; sort may also name status or family. size is capped at 200 (see Common Contract → Pagination). subjectKey narrows to one subject and requires subjectType alongside it — a subjectKey without a subjectType is 400 malformedRequest (params.field = subjectType, params.reason = subjectKeyRequiresSubjectType). An absent status returns every status, drafts and ended rows included. When present, subjectKey carries the subject's public key in the same per-type convention as Subject below — a consumer's username, an API client's clientId, an application's name, an organization's code, an API proxy's name — with one exception: for subjectType=AUDIENCE_RULE the subject is the assignment itself, so subjectKey is that assignment's own bindingKey and filters directly on it. For PLATFORM/PROJECT, subjectKey is ignored, since neither subject type carries a key of its own.
Update, activate, end
Update (PUT) replaces the editable content of a DRAFT or ACTIVE assignment — mode, plan keys or custom rule, audienceRule, the enforcement posture, and the validity window. The subject, family, and target are the row's active-scope identity and cannot be changed this way; changing them means ending the assignment and creating a new one. The body's version is optional but, when present, must match the stored one, or the call is refused with 409 versionConflict.
Activate turns a DRAFT assignment live and pushes it to the running environments; it is refused with 409 lifecycleConflict (params.reason = limit.binding.notDraft) on a row that is not DRAFT. End withdraws an ACTIVE assignment and records why; it is refused with 409 lifecycleConflict (params.reason = limit.binding.notActive) on a row that is not ACTIVE, or limit.binding.alreadyEnded on one that already is. Both actions take an optional version in the body, fencing the same conditional write the required status already fences: when the row's status still matches but its version has moved on, the refusal is 409 lifecycleConflict (params.reason = limit.binding.versionConflict) — not the top-level versionConflict key PUT uses.
Both are naturally idempotent through the row's own optimistic version and take no Idempotency-Key.
Idempotency
POST /limit-assignments requires an Idempotency-Key header (see Common Contract → Idempotency); a missing or blank header is 400 idempotencyKeyRequired. The key is claimed per project and per operation, together with a canonical hash of the request body:
| Situation | Result |
|---|---|
| New key | The assignment is created |
| Same key, same body, first call already finished | 409 idempotencyReplayExpired — params.resource names the assignment key the first call created; read it with GET .../{bindingKey} instead |
| Same key, same body, first call still in flight | 409 idempotencyInProgress |
| Same key, a different body | 409 idempotencyKeyReused |
A claim whose underlying call failed is released, so the same key may be retried once the cause is fixed. PUT, the two lifecycle actions, and actions/validate take no Idempotency-Key — the update is fenced by version instead, and the two actions are fenced by both version and the row's current status (see Update, activate, end above).
Request Body
POST and PUT /limit-assignments/{bindingKey} share one body:
| Field | Type | Meaning |
|---|---|---|
subject | object | The assignment's owner — see Subject below. Immutable after create |
content | object | The assignment itself: family, actor scope, target, plan keys or custom rule, posture, validity — see Content below |
audienceRule | object | Required when subject.type is AUDIENCE_RULE, forbidden otherwise — see Audience rule below |
status | enum | Create only. DRAFT (default) or ACTIVE; ENDED is refused with 409 lifecycleConflict (params.reason = limit.binding.statusNotCreatable) |
version | integer | Update only, as the optimistic-concurrency token — see Update, activate, end above |
Subject
The portable owner reference:
| Field | Type | Meaning |
|---|---|---|
type | enum | ORGANIZATION | CONSUMER | APPLICATION | API_CLIENT | API_PROXY | AUDIENCE_RULE — required |
key | string | The subject's public key in the type's own convention: a consumer's username, an API client's clientId, an application's name, an organization's code, an API proxy's name. Absent for AUDIENCE_RULE (its key is minted by the server and echoed back as the assignment's own bindingKey on read) |
A key that resolves to no subject is 404 resourceNotFound (params.subjectType, params.subject). Which actorScope values a subject type may carry, and which of ORGANIZATION/CONSUMER/APPLICATION/API_CLIENT may hold two live assignments that differ only by actorScope, is the same matrix the manager surface documents — see Limit Plans.
PROJECT and PLATFORM are no longer assignable: project and platform limits were removed. POST /limit-assignments and POST /limit-assignments/actions/validate with subject.type PROJECT or PLATFORM are refused with 400 malformedRequest (params.field = subject.type, params.reason = scopeSubjectRemoved, params.project = the project name). The enum values stay in the schema only because ended rows in the history can still carry them. Update, activate and end of a stored PROJECT/PLATFORM row are unchanged (system administrator only); an ended PROJECT/PLATFORM assignment is read back without a subject key. Assign the plan to a consumer, API client, application, organization or API proxy instead.
Content
The same shape the Bulk Limit Assignment API carries as its run content — see Content and Target there for the full field-by-field reference (family, actorScope, target, planKeys, customRuleSpec, cacheErrorHandlingType, cacheConnectionTimeoutInSeconds, showRateLimitStatisticsInResponseHeader, the three customMessage* fields, validFrom/validUntil). It also carries the Counted As choice, assignmentMode and inheritTo, described next.
One difference: on this surface mode may additionally be OFF (this subject contributes no local bucket of its own — ancestor ceilings still apply), on top of PLAN and CUSTOM. INHERIT is retired and refused on create and update; a row that carried it was rewritten to OFF at upgrade. See Limit Plans, Assignments & Defaults API for what each mode requires and forbids. The manager screens apply plans in PLAN mode.
Counted As — assignmentMode and inheritTo. For an ORGANIZATION, CONSUMER or APPLICATION subject with a plan or a custom rule, assignmentMode is SELF (the default when absent: one counter for the subject and everything under it) or DESCENDANTS (every subject of the kind named by inheritTo beneath this one that has no assignment of its own gets the plan with its own counter; the subject itself is not counted — except an organization with inheritTo = ORGANIZATION, whose direct members are also counted together in one counter). inheritTo is API_CLIENT (the default), CONSUMER (includes applications) or ORGANIZATION for an organization, and only API_CLIENT for a consumer or an application. An update that sends assignmentMode changes it in place; one that leaves it out keeps the stored value. A pair that the subject type cannot carry is 400 malformedRequest (params.reason = limit.binding.assignmentModeSubject or limit.binding.inheritToInvalid).
A plan key that resolves to no plan is 404 resourceNotFound (params.plan); a plan that is not deployed, or a legacy retired plan, is 409 lifecycleConflict (params.reason = limit.binding.planNotPublished / limit.binding.planRetired). AI_TOKEN_BUDGET content needs no license module: the API layer applies no module gate, so both families behave identically on every installation.
Two assignments of one subject that are counted the same way must not limit the same API, endpoint or AI proxy. A plan has no general limit, so the general overlap (limit.binding.planOverlapGeneral) arises only when both assignments carry their own inline custom limit on all traffic. Create, an update that changes the plans or the counting mode, and activate are refused with 409 lifecycleConflict (params.reason = limit.binding.planOverlap or limit.binding.planOverlapGeneral) when they would.
Audience rule
Required when subject.type is AUDIENCE_RULE, forbidden for every other subject type (400 malformedRequest, params.reason = limit.binding.audienceRuleShape). Unlike target, this is content — it can be changed by PUT:
| Field | Type | Meaning |
|---|---|---|
identitySource | enum | AUTH_RESOLVED (default) or VARIABLE |
variableId | string | The global Variable's id — required when identitySource is VARIABLE |
operator | enum | A comparison operator (EQ, CONTAINS, STARTS_WITH, IN, …) — required |
value | string | Required |
outOfTargetAction | enum | BLOCK (default; shown as "Rejected (403)"), GENERAL_QUOTA, or NONE — NONE leaves identities outside the audience untouched: neither blocked nor counted. Omitting the field still means BLOCK, so an existing client keeps its behaviour. This setting belongs to the target, not to one assignment (Request Rate only): when another active rule assignment already sits on the same target, the value sent is replaced by the one that target carries. Change it for the whole target with PUT /api/limit-bindings/targets/out-of-target on the manager's own REST surface. |
generalQuotaMode | enum | TOTAL (default) or PER_IDENTITY — read only when outOfTargetAction is GENERAL_QUOTA |
generalQuotaWindows | array | REQUEST_RATE only. At least one window (same shape as a Request Rate plan's windows) — required when outOfTargetAction is GENERAL_QUOTA, ignored for BLOCK and NONE |
generalQuotaBudget | object | AI_TOKEN_BUDGET only. The AI token budget (same shape as a plan's aiBudget) with at least one positive token, USD or request-count ceiling — required when outOfTargetAction is GENERAL_QUOTA |
additionalClauses | array | Further conditions combined with OR with operator/value (each with its own identitySource, variableId, operator, value) |
identityRegexList | string[] | Regular expressions combined with OR with every other condition; the first capture group is the counted identity |
An AUDIENCE_RULE subject works with both families. The general quota must match content.family: generalQuotaWindows for REQUEST_RATE, generalQuotaBudget for AI_TOKEN_BUDGET. The other one is 400 malformedRequest (params.reason = limit.binding.audienceRuleFamily).
Response
Assignment — 200 / 201
The shape every read and write endpoint on this page returns:
| Field | Type | Meaning |
|---|---|---|
bindingKey | string | Opaque public id — also the subject key echoed for an AUDIENCE_RULE subject |
subject | object | The portable Subject reference |
family | enum | REQUEST_RATE or AI_TOKEN_BUDGET |
actorScope | enum | ANY | AUTHENTICATED | ANONYMOUS |
target | object | The Target this assignment narrows to |
mode | enum | PLAN | CUSTOM | OFF |
planKeys | string[] | Plan keys, when mode is PLAN |
assignmentMode / inheritTo | enum / enum | The Counted As choice — see Content |
customRuleSpec | object | The inline rule body, when mode is CUSTOM |
audienceRule | object | The Audience rule, when subject.type is AUDIENCE_RULE |
cacheErrorHandlingType / cacheConnectionTimeoutInSeconds | enum / integer | Enforcement posture — Request Rate only |
showRateLimitStatisticsInResponseHeader | boolean | Adds the remaining-limit headers to the response. Applies to Request Rate and AI Token Budget assignments; an AI plan that leaves its own setting unset uses this value |
customMessageTemplate / customMessageContentType / customMessageHttpStatusCode | string / string / integer | The custom limit-exceeded message, when set — Request Rate only |
source | enum | How this row came to exist: MANUAL | CREATION_DEFAULT | BULK | MIGRATION | IMPORT | PORTAL_SUBSCRIPTION |
status | enum | DRAFT | ACTIVE | ENDED |
validFrom / validUntil | timestamp | Optional validity window |
assignedAt / assignedBy | timestamp / string | When the row was created, and by whom |
endedAt / endedBy / endedReason | timestamp / string / string | Present once status is ENDED |
operatorOverride | boolean | Whether an operator has pinned this row against a portal-subscription resync |
version | integer | The optimistic-concurrency token for PUT and the two lifecycle actions |
bulkRunId | string | Present when source is BULK — the run that wrote this row |
replacedBindingKey | string | Present when this row replaced an earlier one under a bulk run's Replace conflict policy |
List envelope — 200
GET /limit-assignments returns the shared paged envelope — see Common Contract → Pagination:
{
"status": "SUCCESS",
"items": [],
"page": 0,
"size": 20,
"totalElements": 42,
"totalPages": 3,
"sort": ["assignedAt,desc"]
}
Validate — 200
POST /limit-assignments/actions/validate returns 200 with an empty body when the assignment would be accepted, and 400 malformedRequest with the reason otherwise — nothing is written either way.
Errors
Every error follows the Common Contract's error envelope — branch on errorKey, never on message.
| Error key | When |
|---|---|
malformedRequest | The body could not be understood, a reference field was left blank (params.field names it), subjectKey was sent without subjectType on the list, an httpMethods list was sent on a target type that does not support one (params.reason = limit.binding.methodAxisNotAllowed), audienceRule was missing/present on the wrong subject type or its general quota did not match the family, a PROJECT or PLATFORM subject was sent on create or validate (params.field = subject.type, params.reason = scopeSubjectRemoved), or any other save-time shape rule failed — params.reason names the service's own key |
resourceNotFound | An unknown assignment key, or a subject/content reference (plan, apiProxy, apiProxyGroup, environment, or subjectType + subject) that resolves to nothing |
versionConflict | PUT's body version did not match the stored one — params.entity, params.expected, params.current |
lifecycleConflict | params.reason names the service's own cause: limit.binding.scopeAlreadyBound (this subject/family/target/actorScope already has a live assignment), limit.binding.conflict (an unrelated unique-index collision), limit.binding.notDraft (activate on a row that is not DRAFT), limit.binding.notActive (end on a row that is not ACTIVE), limit.binding.alreadyEnded, limit.binding.endedIsFinal (update on an ENDED row), limit.binding.planRetired, limit.binding.planNotPublished, limit.binding.planOverlap / limit.binding.planOverlapGeneral, limit.binding.keyAlreadyExists, limit.binding.keyImmutable, limit.binding.scopeImmutable (update tried to move subject/family/target), limit.binding.counterIdentityImmutable, limit.binding.versionConflict (activate/end sent a version the row has moved past, while its status still matched), or limit.binding.statusNotCreatable (status: ENDED on create) |
idempotencyKeyRequired | POST /limit-assignments was missing its Idempotency-Key header |
idempotencyKeyReused | The same key was sent with a different request body |
idempotencyReplayExpired | The same key and body already completed — params.resource names the assignment it created |
idempotencyInProgress | The same key and body's first call is still running |
permissionDenied | Authenticated, but the caller lacks Identity/AI Budgets Manage (writes) or View (reads) |
unsupportedSortField | sort named a field other than assignedAt, status, or family |
See Common Contract → Error keys for the complete closed set and the status-code table.
Permissions & Licensing
- Reads (
GET) require the Identity permission at View; writes (POST/PUT) require it at Manage. The AI Budgets permission is also accepted in place of Identity. - No license module is checked on this surface:
AI_TOKEN_BUDGETcontent is gated by permission exactly likeREQUEST_RATE. - The admin (platform) project has no subjects and is refused with
400 malformedRequest(params.reason = platformScopeNotSupported) — assignments are always project-scoped on this surface. - A project outside the caller's authorized scope is always
404 resourceNotFound, never403— see Common Contract → Telling 402, 403 and 404 apart.
Example
Create a draft assignment, validate a change before saving it, activate the draft, then end it:
# 1. Create — DRAFT by default; requires Idempotency-Key
curl -X POST "https://apinizer.example.com/apiops/projects/finance/limit-assignments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c2a9e10-4b3d-4f8a-9e21-6a0d3f5b7c88" \
-d '{
"subject": { "type": "API_CLIENT", "key": "acme-mobile-app" },
"content": {
"family": "REQUEST_RATE",
"actorScope": "AUTHENTICATED",
"mode": "PLAN",
"planKeys": ["standard-100rps"],
"target": { "type": "ALL" }
}
}'
# 201 Created, Location: /apiops/projects/finance/limit-assignments/lpb_9f2a1c
# 2. Validate a content change without writing anything
curl -X POST "https://apinizer.example.com/apiops/projects/finance/limit-assignments/actions/validate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": { "type": "API_CLIENT", "key": "acme-mobile-app" },
"content": {
"family": "REQUEST_RATE",
"actorScope": "AUTHENTICATED",
"mode": "PLAN",
"planKeys": ["standard-500rps"],
"target": { "type": "ALL" }
}
}'
# 3. Activate the draft
curl -X POST "https://apinizer.example.com/apiops/projects/finance/limit-assignments/lpb_9f2a1c/actions/activate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "version": 0 }'
# 4. End it later
curl -X POST "https://apinizer.example.com/apiops/projects/finance/limit-assignments/lpb_9f2a1c/actions/end" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "version": 1, "reason": "Client decommissioned" }'
Related Documentation
- Bulk Limit Assignment API - The population-scale counterpart of this same engine, sharing its Content and Target shapes
- Limit Plans, Assignments & Defaults API - The manager's own REST surface behind Limit Plans, including its single-assignment endpoints (
/api/limit-bindings) this page mirrors portably - Limit Plans - The manager UI screen and concepts this surface backs
- Common Contract - The shared error envelope, idempotency, pagination, and status-code rules every endpoint on this page follows
- Error Handling - The envelope used by the manager's older endpoints