Topolo Docs Topolo Platform
Open workspace

Rotate a Consent project public SDK key

Rotate an approved public integration key and reconcile current configuration, best-effort mappings and embed rollout without replaying uncertain rotations.

Outcome

Rotate one Consent project’s public integration SDK key and reconcile the database, mapping/cache and embed rollout separately. This does not rotate user/service credentials or itself update integrations.

Action: app_topolo_consent.projects.sdk_key.rotate.

POST /api/projects/{projectKey}/sdk-key/rotate

Before you start

Verify original production principal/organization, exact Consent workspace admin grant and app_topolo_consent.workspace:write. Discover actual reachable non-archived project and current public SDK configuration through authorized current records. Bind the canonical workspace separately; key/project values do not confer workspace access.

Approve rotation of this integration’s public Consent SDK key and the actual configuration/cache/mapping effects. Plan the authorized embed/app configuration update with the integration owner. This public integration identifier is not a user OAuth credential or service API key and does not grant admin actions; treat it according to its intended distribution/audience.

Before any real mutation, reconcile previous uncertain rotation and record actual project/current key and affected integrations. This input provides no idempotency key or caller-chosen new key. All shown SDK strings/configuration are fictional source-derived fixtures; no real key rotation, secret read, embed, network or notification change was executed.

Request

{
  "projectKey": "project_fixture_docs"
}

Discover the current exact action and approved real project, validate input and inspect the resource-bound plan. Confirm actual integration effects. The fictional key/id examples authorize no operation.

Effects and response

Input is exactly projectKey. The route requires workspace admin access, app workspace:write and reachable non-archived project in the original verified organization/workspace. No fields set a desired key, previous key, environment, force flag or cache policy.

The source reads current config, generates randomPublicSdkKey using project.environment (staging prefix for staging, otherwise live prefix), parses existing configJson as an object and overwrites sdkKey while preserving parsed properties. It then updates that project config_json and updated_at in control storage by resolved id. There is no revision/CAS guard or caller operation key shown.

After DB write it reads the project/config again, falling back to prior config plus generated key if project disappeared. It concurrently publishes config:<projectKey> with now timestamp and attempts deletion of current.sdkKey’s mapping when it differs. Cache publication also attempts a new SDK mapping. These storage/mapping writes are not atomic with DB or each other; concurrent rotation/read-back can produce a mixed intermediate result.

The cache promise is attached to ctx.waitUntil when available and raced against250ms. synced means the combined promise resolved before that deadline; queued means timeout/rejection, not a durable job receipt or proof of no effects. Unbound KV causes helpers to return without persistence, while mapping put/delete helpers catch their own errors. Thus synced alone does not prove both mappings were changed or every reader adopted the new configuration.

The response is {sdkKey,previousSdkKey,config,cacheStatus,source:d1}. sdkKey is this call’s generated key and previousSdkKey is the prior read. Returned config overlays that generated key and now even if read-back observed a concurrent configuration. Compare current actual state before concluding the returned pair remains current; this response is not a concurrency or global-revocation receipt.

Public project-key resolution takes projectKey before sdkKey, can use a cached SDK mapping and can scan up to200 recent active projects to discover a matching stored key. Config reads are cache-first. A stale old mapping/config or an integration naming projectKey is not guaranteed to fail immediately after rotation. Do not treat the declared broad every-old-embed-fails-closed wording as independent global enforcement proof.

The handler does not rewrite existing site/mobile embeds, rotate OAuth/service credentials, change workspace grants, erase recorded consent or send a rotation notification. Integrations still need their approved configuration rollout and actual current-serving verification. Repeating the action creates another key and side effects, not a safe way to poll for readiness.

Complete worked responses

Rotation returns its generated fixture key

Complete synthetic/unrun HTTP200. Assumes original admin/write grant, active reachable fixture project, the complete cached configuration shown, stored previous fixture SDK key, successful DB update/re-read without concurrent changes, and cache work resolving before250ms. Key strings are illustrative, not issued; no rotation occurred.

Input:

{
  "projectKey": "project_fixture_docs"
}

Response:

{
  "sdkKey": "pk_live_tc_0abc1230def4560ghi789",
  "previousSdkKey": "pk_live_tc_fixture0000000000000000",
  "config": {
    "tenantId": "org_record_fixture",
    "authOrgId": "org_fixture",
    "projectId": "project_record_fixture",
    "workspaceId": "ws_consent_fixture",
    "projectKey": "project_fixture_docs",
    "sdkKey": "pk_live_tc_0abc1230def4560ghi789",
    "mode": "global",
    "updatedAt": "2026-10-20T08:01:00.000Z",
    "cacheTtlSeconds": 300,
    "staleWhileRevalidateSeconds": 86400,
    "purposes": [],
    "vendors": [],
    "domains": [],
    "apps": [],
    "copy": {
      "title": "Fixture preferences",
      "summary": "Fictional configuration",
      "acceptAll": "Accept all",
      "rejectAll": "Reject all",
      "manage": "Manage",
      "gpcApplied": "GPC applied"
    }
  },
  "cacheStatus": "synced",
  "source": "d1"
}

Retain actual pair if executed, then reconcile current persistence/serving before rollout. Synced alone is not durable mapping/global old-key revocation proof.

DB result with uncertain cache work

Complete synthetic/unrun HTTP200. Same fictional source premises except cache work rejects or exceeds the250ms race after DB update. No production fault was injected or storage probed.

Input:

{
  "projectKey": "project_fixture_docs"
}

Response:

{
  "sdkKey": "pk_live_tc_0abc1230def4560ghi789",
  "previousSdkKey": "pk_live_tc_fixture0000000000000000",
  "config": {
    "tenantId": "org_record_fixture",
    "authOrgId": "org_fixture",
    "projectId": "project_record_fixture",
    "workspaceId": "ws_consent_fixture",
    "projectKey": "project_fixture_docs",
    "sdkKey": "pk_live_tc_0abc1230def4560ghi789",
    "mode": "global",
    "updatedAt": "2026-10-20T08:01:00.000Z",
    "cacheTtlSeconds": 300,
    "staleWhileRevalidateSeconds": 86400,
    "purposes": [],
    "vendors": [],
    "domains": [],
    "apps": [],
    "copy": {
      "title": "Fixture preferences",
      "summary": "Fictional configuration",
      "acceptAll": "Accept all",
      "rejectAll": "Reject all",
      "manage": "Manage",
      "gpcApplied": "GPC applied"
    }
  },
  "cacheStatus": "queued",
  "source": "d1"
}

Keep cache/mapping delivery UNKNOWN and reconcile this rotation. Do not rotate again as a readiness check; queued is not a durable queue receipt.

Archived project refused

Complete synthetic/unrun HTTP409. Assumes shared original authority/project/storage checks pass and the scoped project is archived. That is external fixture state, not an input flag.

Input:

{
  "projectKey": "project_fixture_docs"
}

Response:

{
  "error": "project_archived"
}

Preserve refusal; activation requires a separately approved admin status change, not an automatic rotation workaround.

Verify and recover

Preserve actual sdkKey, previousSdkKey, project identity and cacheStatus; distinguish generated values from current stored/served state under concurrent work. A public SDK key is not an admin permission or a replacement workspace resource id.

Use current supported, validated config/integration reads and the owning team to reconcile intended project/key/mapping/cache state. Preserve actual schema/permission/dependency refusals rather than fabricating a successful consumer result or bypassing them. queued calls must be reconciled, not retried by another rotation.

Apply any separately approved embed update only to the actual current integration key after reconciliation; verify normal authorized site/app behavior through the integration owner’s existing workflow. No fixture, origin spoofing, dropped-Origin retry or guessed project/context is a valid verification shortcut.

If a response is lost/uncertain, first reconcile current project config and existing mapping/cache evidence with owner. Original request may have persisted despite failure. Preserve unknown effects and current concurrent changes; never issue parallel rotations or regenerate a key merely to force synchronization.

This action has no input to restore a chosen prior SDK key. A second rotation generates another new key and cannot undo earlier mapping/cache or external embed changes. Reconcile the actual current key and rollout through authorized integration configuration; do not write config_json/KV directly, restore stale mappings, erase privacy data or rotate another principal’s credentials as rollback. Original mapping deletion is best-effort and must not be claimed as proven global revocation.

Refusals

403 / organization_context_required: Reconcile original intended organization without broadening identity.

404 / project_not_found: Reconcile actual key within original organization AND verified workspace.

409 / project_archived: Do not automatically activate a project to bypass the rotation refusal.

503 / database_unavailable: Control storage absent. For any error after write begins, reconcile current DB/cache/mapping state before another mutation.

Shared Auth/admin-grant/app-permission errors precede the handler. SQL/cache failures need actual shared-runtime/request evidence and partial-effect reconciliation. Unsupported chosen-key/force input is a local strict preflight error, not a captured HTTP branch.

Change log

  • 2026-10-21: Added admin-level public integration key scope, sequential/concurrent persistence and250ms cache limits, best-effort mapping semantics, source-derived full examples and recovery without blind repeat rotations or global revocation claims.