Record a visitor choice
Use app_topolo_consent.consent.set to record one approved visitor choice for a configured purpose. This public integration route does not authenticate the visitor or grant administrative access.
POST /api/set-consent
Use the intended integration and the visitor’s legitimate choice. Resolve real identifiers from that integration; the examples below contain fictional values and were not executed. Preserve the actual Origin policy—do not spoof or remove a rejected Origin.
Request
| Field | How to use it |
|---|---|
| projectKey or sdkKey | Supply the real integration key. A nonblank projectKey takes precedence. At least one must resolve, even though neither is schema-required. |
| purposeId | Required. Must exist in the resolved project configuration. |
| decision | Required: granted, denied, withdrawn or unknown. |
| identityId, identity, deviceId | Opaque visitor identifiers from the legitimate integration. identityId takes precedence over identity; state uses deviceId only when no identity id exists. These values are not identity-ownership proof. |
| appId, source, region | Optional context. Defaults: web; recognized request GPC signal then gpc, otherwise sdk; detected/mapped request region. |
| metadata | Optional object. The service overlays gpc and userAgent from the actual request. |
Discover the current action and validate the reviewed input before an intended real write. Follow its current confirmation policy. A public Consent SDK key is an integration identifier, not an OAuth token, API key or workspace grant. A custom idempotencyKey is not supported by this action’s input schema.
Response
The route returns HTTP202 for both persistence outcomes:
| Field | Meaning |
|---|---|
| accepted | The request reached this acceptance path. It is not independently verified current state. |
| projectKey, purposeId, decision | The resolved integration and submitted choice. A GPC signal is recorded separately; this handler does not force the submitted decision to denied. |
| syncStatus | synced: persistence promise resolved within250ms. queued: it timed out or rejected. queued is not a durable queue receipt or evidence that nothing changed. |
| idempotencyKey | Derived from project, subject/device/anonymous, purpose, decision and current ISO minute. It is not the fresh event UUID or a guaranteed cross-minute retry identity. |
The optional queue mirror, event insert and state upsert are separate steps, without a demonstrated cross-store transaction. Work can finish after the response or partially persist before failure. The response does not say whether an event insert was ignored or a state update won. Usage telemetry may be attempted separately; acceptance proves no metering, charge, notification or downstream enforcement outcome.
Worked examples
These complete responses are illustrative and unrun. Assume the fictional integration resolves, its Origin policy passes, analytics is configured, rate checking allows the request and source time is2026-10-20T08:00Z. No visitor preference, private record, queue or state was changed.
Fast persistence-promise completion
The persistence promise resolves within250ms. Verify current state separately.
Input:
{
"projectKey": "fixture-project",
"identityId": "identity_fixture_docs",
"purposeId": "analytics",
"decision": "denied",
"source": "banner"
}
Response:
{
"accepted": true,
"projectKey": "fixture-project",
"purposeId": "analytics",
"decision": "denied",
"syncStatus": "synced",
"idempotencyKey": "fixture-project:identity_fixture_docs:analytics:denied:2026-10-20T08:00"
}
Uncertain persistence
The promise rejects or exceeds250ms; earlier queue/event effects may already exist. Reconcile before any further write.
Input:
{
"projectKey": "fixture-project",
"identityId": "identity_fixture_docs",
"purposeId": "analytics",
"decision": "denied",
"source": "banner"
}
Response:
{
"accepted": true,
"projectKey": "fixture-project",
"purposeId": "analytics",
"decision": "denied",
"syncStatus": "queued",
"idempotencyKey": "fixture-project:identity_fixture_docs:analytics:denied:2026-10-20T08:00"
}
Purpose not configured
The key, surface and rate checks pass, but purpose_missing_fixture is absent from configuration. This input is schema-valid; the handler refuses it.
Input:
{
"projectKey": "fixture-project",
"identityId": "identity_fixture_docs",
"purposeId": "purpose_missing_fixture",
"decision": "denied",
"source": "banner"
}
Response:
{
"error": "invalid_consent_event"
}
Verify and handle uncertainty
- Compare the actual returned integration, purpose and decision with the intended visitor operation. Preserve its time and request evidence.
- Reconcile current subject state through supported, validated integration reads and its owner. Administrative logs require separate workspace authority, are bounded and do not by themselves prove the later state upsert succeeded. Preserve actual schema, permission and dependency refusals.
- For queued or a lost response, reconcile possible queue/event/state effects before a separately approved repeat. A later invocation can produce a different minute key, event UUID and telemetry event. Do not automatically retry or run duplicates in parallel.
- Verify effective data-use enforcement in the legitimate integration separately. A stored choice, GPC metadata, configured purpose or accepted response does not prove every tag, provider or model honored that policy.
A later approved preference change creates another event/state update. It does not delete the earlier record or undo downstream effects. Do not restore a guessed choice, erase audit history or invoke organization erasure as rollback.
Limits and refusals
The nominal write limit is120 per project/request-fingerprint/minute. Its optional KV counter is best-effort, non-atomic and fails open on missing/unreadable storage. It is not a global distributed rate guarantee. Missing or anonymous subject identifiers can be accepted but may not resolve through a later subject-state read.
| HTTP / code | Next step |
|---|---|
| 400 / project_key_required | Resolve the real intended integration key/SDK key; schema-optional key fields do not make a keyless write meaningful. |
| 404 / unknown_project | Reconcile intended integration configuration/mapping; do not guess or broaden scope. |
| 403 / invalid_origin | Preserve original request evidence and correct actual integration Origin configuration; do not spoof/drop Origin. |
| 403 / origin_not_allowed | Use only the authorized configured surface; changes to allowed domains/apps are separate admin operations. |
| 429 / rate_limited | Source response includes retryAfterSeconds:60. Respect the actual response and reconcile any prior uncertain work before a later authorized attempt; avoid parallel duplicates. |
| 400 / invalid_consent_event | Purpose must be configured and decision supported; preserve the visitor’s actual intended choice instead of guessing substitutes. |
A schema-invalid extra input field is a local action-validation failure, not a captured HTTP response. SDK/config cache or key mapping uncertainty needs the actual integration owner; do not guess another project or visitor.
Change log
- 2026-10-21: Added the public integration boundary, request/response fields, complete worked examples, persistence and retry limits, and separate verification of actual state versus effective policy. Examples are source-derived and unrun at immutable source4189f07126b1ecfc36aed6588f978346a7bafa2e; deployed serving/runtime association is not established here. Detailed normalization, partition, source and recovery semantics are retained in the paired agent layer.