Topolo Docs Topolo Platform
Open workspace

Import reviewed legacy entry experience

Save reviewed legacy presentation as the first unpublished authoring draft and retain its full inventory archive.

Import reviewed legacy entry experience

Save reviewed legacy presentation as the first unpublished authoring draft and retain its full inventory archive.

Action and scope

app_topolo_developers.apps.experience.import — POST /api/developer-console/apps/{appId}/experience/import. Required permission: app_topolo_developers.apps:write. Authorization mode: static; no platform principal or allowed-service override is declared.

Before using this action

Use the original authenticated organization, accessible Developers workspace and current action permission. Discover the canonical owning app, bind the workspace separately and compare the returned appId. An app identifier, static permission or canWrite projection alone is not an authorization grant.

Shared workspace context requires the app/workspace match and current app access. Reads permit a suspended profile but not revoked access; writes require app.canEdit and a writable profile. These routes do not require completed onboarding. Shared context can initialize a local workspace mirror and developer profile before a later refusal; it does not mint a canonical Auth workspace.

Use the current declared schema and actual actor policy before any authorized call. All examples use fictional identifiers, synthetic state and UNRUN outcomes; they are not receipts, evidence of deployment, a current private app read or a running login/landing UI check.

Requires app_topolo_developers.apps:write, current app.canEdit and a writable profile. Read apps.experience.get and inventory in the original app/workspace before accepting ownership. The captured action schema requires expectedRevision:0 and acceptCatalogOwnership:true.

The declared execution mode is automatic for inventory and confirmation-required for import/publish. Confirmation and current authorization must be evaluated for the selected action; these offline examples do not approve a mutation.

Inputs

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "appId": {
      "type": "string",
      "minLength": 1
    },
    "expectedRevision": {
      "type": "number",
      "const": 0
    },
    "acceptCatalogOwnership": {
      "type": "boolean",
      "const": true
    }
  },
  "required": [
    "appId",
    "expectedRevision",
    "acceptCatalogOwnership"
  ],
  "additionalProperties": false
}

Behavior and response

POST /api/developer-console/apps/{appId}/experience/import reads current entryExperience state. The body must have a safe-integer expectedRevision exactly equal to saved state.revision; the published input further constrains it to 0. An unresolved publishing/failed attempt or any existing draft/published snapshot prevents import.

The route reads the complete Auth inventory before checking acceptCatalogOwnership===true. A false/missing acceptance is refused after that dependency read; it is not a way to skip inventory. Success takes inventory.draft and retains the entire raw successful body as migrationArchive.

The draft selects login.layout, background_type, background_gradient_start, background_gradient_end, background_image_url, background_video_url and background_animation; landing selects background_gradient_start, background_gradient_end, show_topolo_one, pricing_enabled and is_enabled. Null or absent fields are omitted. For non-null landing booleans, only stored integer 1 becomes true; every other non-null value becomes false.

Legacy fields outside each surface allowlist become conflicts with resolution catalog_product_marketing_wins. Only custom_css, custom_head_html, pricing_tiers, footer_links and legal_links set requiresReview:true; false is not proof that an unrecognized field is safe. The legacy object omits id, app_id, created_at, updated_at, allowed_providers, authenticated_home_path and magic_link_delivery_mode. Excluded policy fields are not returned or changed.

Both login and landing must be objects; only the allowlisted fields are accepted. Login layout is split, centered, immersive, minimal or magazine; background_type is gradient, image, video or animated. Gradient colors are #RRGGBB; asset URLs must be HTTPS without credentials. Text must be nonempty and at most 2048 characters. Landing flags are booleans. Both the closed input validator and pinned app-manifest 0.4.0 experience validator must accept the draft; legacy values are not silently repaired.

A successful full-state compare-and-swap saves revision+1, the selected draft and migrationArchive while retaining prior state fields. HTTP200 is {success:true,message:"OK",data:{appId,kind:"entryExperience",state}}. The imported draft remains unpublished. No selected import path changes legacy Auth policy, pricing contracts or the catalog. Shared context initialization remains a separate possible effect.

Errors and recovery

HTTP409 — Read current saved state; stale revision, existing authoring or unresolved publication is not permission to overwrite/reset it. A concurrent full-state change also refuses the save.

HTTP422 — Explicit ownership acceptance and both draft validators are required. Review the actual inventory/invalid fields. Do not silently discard conflicting legacy presentation or claim it was migrated.

HTTP503 — Legacy experience must be read completely before import. Preserve uncertainty and do not treat an unavailable service as empty.

HTTP403 — Preserve current app/workspace ownership and read-only restrictions.

Error code labels in the typed layer are explanatory classifications unless explicitly present in the actual response. Shared authentication, permission, validation and rate-limit refusals may occur before these branches. Ordinary console errors use an error object; inventory forwards raw Auth errors; publication partial-failure responses can use an error string with data.state.

Verification

Retain the original organization/workspace, actual canonical appId, action inputs, status/error and request identifier. Never substitute a fixture identifier, revision, attempt or digest for actual read-back.

The captured output schema is structural and open. The detailed source shapes below explain reviewed branches without replacing the published schema or guaranteeing that every response has every illustrated field.

Read apps.experience.get under separate authorized read scope. Compare the exact revision increment, selected draft, full migrationArchive and unchanged published/publication state. A saved draft or retained archive is not a catalog commit or live UI migration.

Recovery

Read back before another write after response loss. If the import already saved revision1, do not replay expectedRevision0 or reset authoring. The source provides no automatic import inverse; use a separately authorized edit/recovery decision with the actual retained archive and owner.

Synthetic examples — UNRUN

First import with no legacy rows

Complete source-derived synthetic UNRUN HTTP200. Initial saved state is exactly revision0/draft:null/published:null/publication:null; both rows are absent, validators accept the empty surface objects and full-state save succeeds.

Input:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "acceptCatalogOwnership": true
}

Output:

{
  "success": true,
  "message": "OK",
  "data": {
    "appId": "app_docs_fixture",
    "kind": "entryExperience",
    "state": {
      "revision": 1,
      "draft": {
        "login": {},
        "landing": {}
      },
      "published": null,
      "publication": null,
      "migrationArchive": {
        "success": true,
        "appId": "app_docs_fixture",
        "draft": {
          "login": {},
          "landing": {}
        },
        "legacy": {
          "login": {},
          "landing": {}
        },
        "conflicts": [],
        "policy": "Auth provider and access policy is excluded and preserved."
      }
    }
  }
}

Read back revision1 and the complete archive; nothing is published.

Response-loss replay uses a stale revision

Complete source-derived synthetic UNRUN HTTP409. The first import already saved revision1. This refusal occurs before another inventory read or authoring save.

Input:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "acceptCatalogOwnership": true
}

Output:

{
  "success": false,
  "error": {
    "message": "Read the current revision before saving or publishing"
  }
}

Read current authoring and retain the earlier result; do not reset state to make the request pass.

Source provenance and limits

Reviewed 2026-10-23 against immutable Developers source 7b89bd3b39092cdc29f87540847bf99a243f0a40 and retained production declarations. The inventory receiver is independently pinned to Auth e2ea647f225b1842f629aa74f732fbfb6601b3e3. Declaration/source association is not evidence that either revision is serving or that the cross-service pair is deployed together.

The documented application action and examples were not executed for this guide. No private app, Auth inventory, provider, signup/login or runtime UI was accessed. Source imports, tests and builds were not run. Shared prerequisites reuse the accepted source qualification for experience.get/update; transitive service implementations beyond the listed boundaries were not audited.

Automatic Knowledge invocation during relevant builds has not been verified; its reliability remains UNKNOWN. Manual documentation review does not establish automatic build integration.

Change log

  • 2026-10-23: Added and published this guide with source-qualified behavior and synthetic, unrun examples. This is a documentation change; no application behavior change is asserted.