Ana içeriğe geç

Bulk Limit Assignment API

Overview

Bulk Assignment attaches one assignment — a published plan or a custom value — to a large, filtered population of subjects in a single run, instead of one call per subject. This page documents the Management API surface for that engine, the portable counterpart of the manager's own Bulk Assignment REST endpoints. For the screen and concepts this backs — selection modes, conflict policy, run lifecycle, node-failure recovery — see Limit Plans → Bulk Assignment.

Every address on this page is under /apiops/projects/{projectName} and follows the shared Common Contract: the stable error envelope, correlation IDs, bounded pagination, and Idempotency-Key on every run-creating call. A caller outside the project's scope always gets 404 resourceNotFound, never 403. The admin (platform) project has no subjects of its own, so 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. Subjects, organizations, plans, proxies, groups, and environments are all addressed by the name or code the object is already known by; an unknown reference is 404 resourceNotFound naming the field.

Endpoints

MethodPathDescription
POST/limit-assignment-previewsClassify the selected population without writing anything
POST/limit-assignment-runsQueue a run over the selected population (202 + Location)
GET/limit-assignment-runsList the project's runs, newest first by default
GET/limit-assignment-runs/{runId}One run's progress and outcome
GET/limit-assignment-runs/{runId}/issuesThe FAILED/SKIPPED subjects the run recorded
POST/limit-assignment-runs/{runId}/actions/cancelAsk a queued or running run to stop at its next chunk boundary (200)
POST/limit-assignment-runs/{runId}/actions/rollbackQueue a new run that undoes what the given run did (202)
POST/limit-assignment-runs/{runId}/actions/retry-failedQueue a new run over only the subjects the given run failed on (202)

List runs

GET /limit-assignment-runs?page&size&sort sorts by startedAt,desc by default; sort may also name status or kind. size is capped at 200. A sort naming any other field is 400 unsupportedSortField (params.supported lists the three).

Cancel, rollback, retry-failed

Cancel stops a queued or running run at its next 200-subject chunk boundary; whatever it already wrote stays written. It returns 200 with the run itself — it does not start a new run.

Rollback is only valid on a finished Assign or Retry run: it queues a brand-new run that ends every assignment the source run created and re-opens what it Replaced as new rows — those counters restart, the same as any Replace. Refused with 409 lifecycleConflict (params.reason = limit.bulk.runNotRollbackable) on a run with nothing to roll back.

Retry failed queues a brand-new run over only the subjects the source run's issues recorded as FAILED, always with Keep existing as its conflict policy. Refused with 409 lifecycleConflict (params.reason = limit.bulk.runNotRetryable) when the source run recorded no retryable subject.

Both actions require their own Idempotency-Key and return 202 with a Location pointing at the new run — never the source run.

Idempotency

POST /limit-assignment-runs, .../actions/rollback, and .../actions/retry-failed each require 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:

SituationResult
New keyThe run is queued
Same key, same body, first call already finished409 idempotencyReplayExpiredparams.resource names the run id the first call created; read it with GET .../{runId} instead
Same key, same body, first call still in flight409 idempotencyInProgress
Same key, a different body409 idempotencyKeyReused

A claim whose underlying call failed is released, so the same key may be retried once the cause is fixed.

Request Body

POST /limit-assignment-previews and POST /limit-assignment-runs share one body:

FieldTypeMeaning
selectorobjectThe population — see Selector below
contentobjectThe assignment every matched subject receives — see Content below
conflictPolicyenumKEEP_EXISTING (default) or REPLACE
previewFingerprintstringRuns only. From the preview response; a selector that no longer hashes to it is 409 lifecycleConflict (params.reason = limit.bulk.fingerprintMismatch)
expectedCountintegerRuns only. From the preview response; recorded on the run as expectedCount

Selector

The WHO: which subjects are in scope.

FieldTypeMeaning
subjectTypeenumCONSUMER | API_CLIENT | APPLICATION | ORGANIZATION — required
organizationCodesstring[]Organization codes to scope the population to
includeSubOrganizationsbooleanWith organizationCodes, also include each organization's descendant subtree
searchstringCase-insensitive name/username contains-search
enabledOnlybooleanOnly enabled subjects
existingAssignmentenumANY (default) | NONE (only subjects with no active assignment on this scope) | EXISTS
subjectKeysstring[]An explicit population by public key (Consumer username, API Client clientId, Application name, Organization code) — at most 5,000
excludeSubjectKeysstring[]Subjects to leave out of the filter-wide population — at most 5,000

A subjectKeys/excludeSubjectKeys list beyond 5,000 entries is 400 malformedRequest (params.reason = limit.bulk.includeListTooLarge). A key that resolves to no subject is 404 resourceNotFound (params.subjectType, params.subject — the first missing key — and params.missingCount).

Content

The WHAT: the assignment every matched subject receives — the same content a single POST /api/limit-bindings assignment carries on the manager's own REST surface, addressed by portable references instead of ids.

FieldTypeMeaning
familyenumREQUEST_RATE or AI_TOKEN_BUDGET — required
actorScopeenumANY | AUTHENTICATED | ANONYMOUS — must be one the subject type allows
targetobjectWhat the ceiling narrows to — see Target below
modeenumPLAN (default when planKeys is set) or CUSTOM (with customRuleSpec)
planKeysstring[]Published plan keys — at most 200
customRuleSpecobjectThe plan's own rule body (windows / budget), inline — required when mode is CUSTOM
cacheErrorHandlingTypeenumFAIL (default) or CONTINUE — Request Rate only
cacheConnectionTimeoutInSecondsintegerRequest Rate only
showRateLimitStatisticsInResponseHeaderbooleanRequest Rate only
customMessageTemplate / customMessageContentType / customMessageHttpStatusCodestring / string / integerReplaces the standard message on a limit-exceeded block — Request Rate only
validFrom / validUntiltimestampOptional validity window

A plan key that resolves to no plan is 404 resourceNotFound (params.plan). AI_TOKEN_BUDGET is refused with 403 licenseModuleNotEnabled when the project's license does not include the AI Gateway module — checked after authentication, so an anonymous caller cannot use this to enumerate paid modules.

Target

target.type narrows what the assignment counts: ALL (default, no narrowing), API_PROXY, API_PROXY_ENV, API_PROXY_GROUP, API_PROXY_GROUP_ENV, API_ENDPOINT, AI_MODEL, or AI_PROVIDER.

FieldTypeMeaning
typeenumSee above
apiProxyNamestringRequired for API_PROXY / API_PROXY_ENV
apiProxyGroupNamestringRequired for API_PROXY_GROUP / API_PROXY_GROUP_ENV
environmentNamestringRequired for API_PROXY_ENV / API_PROXY_GROUP_ENV
targetKeystringFor API_ENDPOINT / AI_MODEL / AI_PROVIDER — the target's key as shown wherever a Target is displayed; these three have no project-scoped human name on this surface, so the key is taken verbatim
httpMethodsstring[]API_PROXY / API_PROXY_ENV only — empty or absent means every method

An apiProxyName, apiProxyGroupName, or environmentName that resolves to nothing is 404 resourceNotFound (params.apiProxy / params.apiProxyGroup / params.environment). httpMethods on a target type that does not carry the method axis is 400 malformedRequest (params.reason = limit.binding.methodAxisNotAllowed).

Response

Preview — 200

{
"status": "SUCCESS",
"selectorFingerprint": "a1b2c3d4",
"expectedCount": 4000,
"willCreate": 3820,
"willSkip": 150,
"willReplace": 30,
"licenseBlocked": 0,
"estimated": false,
"sample": [
{ "subjectName": "acme-mobile-app", "outcome": "CREATED" },
{ "subjectName": "acme-web-app", "outcome": "SKIPPED_EXISTING", "existingBindingKey": "lpb_9f2a" }
]
}

estimated: true above a population of 5,000 — the counts are an estimate; the run itself reports the real numbers.

Accepted — 202

The receipt returned by POST /limit-assignment-runs, .../actions/rollback, and .../actions/retry-failed, with Location pointing at the new run:

{
"status": "SUCCESS",
"runId": "bar_7f3e1a9c",
"kind": "ASSIGN",
"runStatus": "QUEUED",
"queuedAt": "2026-09-03T10:15:00Z",
"expectedCount": 4000
}

Run — 200

The shape returned by GET .../{runId} and each row of GET /limit-assignment-runs:

FieldTypeMeaning
runIdstringOpaque public id
runStatusenumQUEUED | RUNNING | SUCCESS | PARTIAL | FAILED | CANCELLED | INTERRUPTED | LICENSE_REVOKED
kindenumASSIGN | ROLLBACK | RETRY
phaseenumPREPARE | APPLY | DEPLOY | FINALIZE
startedAt / runningAt / heartbeatAt / finishedAttimestampLifecycle timestamps
durationMsintegerElapsed time, once finished
triggerenumMANUAL (manager UI) or APIOPS (this surface)
triggeredBystringThe caller who started it
selectorobjectThe request's selector, echoed back in the same portable vocabulary
includedSubjectCount / excludedSubjectCountintegerSize of the explicit include/exclude lists (not the full matched population)
selectorFingerprintstringThe fingerprint this run's selector hashed to
contentobjectThe request's content, echoed back the same way
conflictPolicyenumKEEP_EXISTING or REPLACE
expectedCount / actualCount / processedCountintegerPlanned vs. actual population size, and how much has been processed so far
createdCount / replacedCount / skippedCount / failedCount / licenseSkippedCountintegerPer-outcome counters
issueCountintegerHow many issues were recorded
issuesTruncatedbooleantrue once the 1,000-issue cap was hit
cancelRequestedbooleanWhether Cancel was asked for
rollbackOfRunId / retryOfRunIdstringPresent on a ROLLBACK / RETRY run, naming its source run
idempotencyKeystringThe key that started this run
deploymentStateenumIN_SYNC | DEGRADED | PENDING
messageKey / messageParamsstring / objectAn i18n key + parameters describing the run's outcome, when there is one to show

Issue — 200, paged

One row of GET .../{runId}/issues:

FieldTypeMeaning
seqintegerSequence within the run
subjectTypeenumThe selector's subject type
subjectNamestringThe subject's public name — never an id
outcomeenumCREATED | REPLACED | SKIPPED_EXISTING | SKIPPED_LICENSE | FAILED | ENDED | RESTORED
bindingKeystringThe assignment this issue is about, when there is one
messageKey / messageParamsstring / objectAn i18n key + parameters explaining the outcome
occurredAttimestampWhen this row was recorded

Only FAILED and SKIPPED_* rows are kept, at most 1,000 per run.

List envelope — 200

GET /limit-assignment-runs and GET .../{runId}/issues both return the shared paged envelope:

{
"status": "SUCCESS",
"items": [],
"page": 0,
"size": 20,
"totalElements": 137,
"totalPages": 7,
"sort": ["startedAt,desc"]
}

Errors

Every error follows the Common Contract's error envelope — branch on errorKey, never on message.

Error keyWhen
malformedRequestThe body could not be understood, a reference field was left blank, an httpMethods list was sent on a target type that does not support one, or an include/exclude list exceeded 5,000 entries (params.reason = limit.bulk.includeListTooLarge)
resourceNotFoundAn unknown run id, or a selector/content reference (plan, organization, apiProxy, apiProxyGroup, environment, or subject + subjectType + missingCount) that resolves to nothing
lifecycleConflictparams.reason names the engine cause: limit.bulk.runAlreadyActive (a second run started while one is queued or running), limit.bulk.fingerprintMismatch (the selector no longer matches the preview), limit.bulk.runNotCancellable, limit.bulk.runNotRollbackable, or limit.bulk.runNotRetryable
idempotencyKeyRequiredA run-creating call was missing its Idempotency-Key header
idempotencyKeyReusedThe same key was sent with a different request body
idempotencyReplayExpiredThe same key and body already completed — params.resource names the run it created
idempotencyInProgressThe same key and body's first call is still running
permissionDeniedAuthenticated, but the caller lacks Identity/AI Budgets Manage (writes) or View (reads)
licenseModuleNotEnabledAn AI_TOKEN_BUDGET selector/content was used without the AI Gateway license module
unsupportedSortFieldsort named a field other than startedAt, status, or kind

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) require it at Manage. The AI Budgets permission is also accepted in place of Identity.
  • AI_TOKEN_BUDGET content additionally requires the AI Gateway license module; REQUEST_RATE needs no license module.
  • The admin (platform) project has no subjects and is refused with 400 malformedRequest (params.reason = platformScopeNotSupported) — Bulk Assignment is always project-scoped.
  • A project outside the caller's authorized scope is always 404 resourceNotFound, never 403 — see Common Contract → Telling 402, 403 and 404 apart.

Example

Preview the population, start a run with an idempotency key, poll it, then read its issues:

# 1. Preview — zero-write
curl -X POST "https://apinizer.example.com/apiops/projects/finance/limit-assignment-previews" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"selector": {
"subjectType": "API_CLIENT",
"organizationCodes": ["acme-partners"],
"includeSubOrganizations": true,
"existingAssignment": "NONE"
},
"content": {
"family": "REQUEST_RATE",
"mode": "PLAN",
"planKeys": ["standard-100rps"],
"target": { "type": "ALL" }
},
"conflictPolicy": "KEEP_EXISTING"
}'

# 2. Start — requires Idempotency-Key; body repeats the preview plus its fingerprint
curl -X POST "https://apinizer.example.com/apiops/projects/finance/limit-assignment-runs" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9d0c8f4a-2f1e-4c7b-9c2f-8b3a1d5e7c40" \
-d '{
"selector": {
"subjectType": "API_CLIENT",
"organizationCodes": ["acme-partners"],
"includeSubOrganizations": true,
"existingAssignment": "NONE"
},
"content": {
"family": "REQUEST_RATE",
"mode": "PLAN",
"planKeys": ["standard-100rps"],
"target": { "type": "ALL" }
},
"conflictPolicy": "KEEP_EXISTING",
"previewFingerprint": "a1b2c3d4",
"expectedCount": 4000
}'
# 202 Accepted, Location: /apiops/projects/finance/limit-assignment-runs/bar_7f3e1a9c

# 3. Poll the run
curl "https://apinizer.example.com/apiops/projects/finance/limit-assignment-runs/bar_7f3e1a9c" \
-H "Authorization: Bearer $TOKEN"

# 4. Once finished, read what failed or was skipped
curl "https://apinizer.example.com/apiops/projects/finance/limit-assignment-runs/bar_7f3e1a9c/issues?page=0&size=50" \
-H "Authorization: Bearer $TOKEN"