Topolo Docs Topolo Platform
Open workspace

Claim an app transfer

Read the ownership and partial-migration limits before claiming an app; the pinned source retains its original canonical workspace.

Outcome

Claim an app transfer. Read the access requirements and recovery limits before confirming a live operation.

POST /api/developer-console/apps/claim-transfer

Before you start

Use the originally authenticated production organization and current published Developers apps:write permission. Discover and bind the actual canonical Developers workspace separately from action input. Never change organization/context as a workaround for refusal.

Shared context requires a non-suspended developer profile and can initialize a local workspace mirror/profile before later refusal. Completed onboarding is separately required by both routes. Initialization is not canonical Auth workspace creation.

These actions change access/ownership state. Review the exact source and target organizations, app, authority and actual expected effect before confirming a real mutation. All examples here are complete synthetic UNRUN wire carriers with fictional identifiers, emails and transfer code; no operation was called. Source/declaration equality is not serving-runtime or completed-transfer proof.

Treat a real transfer code as sensitive authorization material. Use any protected credential reference returned by supported tooling unchanged; do not expose or copy a live code into documentation, logs or messages. This guide shows only an invented code.

Confirm the intended target organization and existing source owner’s authorization. The frozen source retains the old canonical workspaceId; complete migration is not established and needs owner qualification.

Request

Action: app_topolo_developers.apps.claim_transfer. Discover real context and bind workspace separately; validate and plan the current declared schema.

{
  "transferCode": "ABCD-EFGH-JKLM"
}

Effects and limitations

The claiming context must complete onboarding and satisfy hasPlatformAccess or organization role owner/admin/super_admin. Source checks the organization-role predicate, not just possession of a code or a true app canWrite flag.

The route parses JSON, normalizes String(body.transferCode||"").trim() to uppercase alphanumeric text, hashes it and looks up one pending code in developer_apps. The input declaration does not mark transferCode required; source still rejects missing/empty/invalid codes with422. Supply only the actual protected transferCode carrier resolved by supported tooling.

Invalid/non-pending code returns422. Missing/unparseable/elapsed expiry also returns422, but first attempts a metadata update setting transfer.status:expired and codeHash:null while preserving other app metadata and members. A refused/expired response can therefore accompany a write. The comparison expires at or before the current clock time.

An already same-organization app is rejected after expiry handling. The claimant email is trimmed/lowercased and a single owner member is validated from claimant identity; invalid email/member state gives422. Successful claim replaces all app members with that claimant owner, rather than adding the claimant to the old membership list.

The first update changes developerId and organizationId plus completed transfer metadata and claimant members. It clears codeHash and retains original issuance/expiry fields. previousOrganizationId uses current app.organizationId first, then the saved transfer.previousOrganizationId, then null; previousOrganizationSlug uses the saved slug or null, so previous-organization fields are not unconditionally preserved. It does not assign workspace_id: the app retains its old workspaceId in this source. This is a critical boundary: do not describe HTTP200/transferred:true as a verified move into the target canonical workspace.

Next, a separate SQL update changes developer_id/organization_id on all developer_app_submissions for the app. It then rereads/decorates the app and returns HTTP200 {success:true,message,data:{transferred:true,app}}. app can be null if the reread finds no record; there is no explicit null guard here. Earlier app/submission changes are not rolled back when later stages fail.

These updates are not an atomic guarded claim transaction. There is no code-status/hash predicate or revision guard on the final app update. Concurrent claims or issuance can race; do not infer exactly-once delivery, safe retry, or successful full ownership migration from a code becoming inactive.

The selected route does not reconcile canonical Auth workspace, service owner, registry/catalog, credentials, entitlements, external resources or financial/account custody. The unchanged workspaceId and split-write recovery need existing source-owner disposition before treating this as a complete operational migration. Preserve all actual app/submission/transfer evidence; never replay an uncertain claim or switch contexts to force target visibility.

Worked responses

Claim changes ownership fields but retains source workspace

Complete synthetic UNRUN HTTP200. Fictional pending code is unexpired, claimant is a target organization admin with completed onboarding, valid email and no concurrent changes. Both SQL ownership/submission stages succeed and the app reread is non-null. The full app deliberately retains ws_docs_fixture: source does not update workspace_id. No real transfer or target-workspace accessibility is proved.

{
  "success": true,
  "message": "Developer app transfer claimed successfully",
  "data": {
    "transferred": true,
    "app": {
      "id": "app_docs_fixture",
      "developerId": "dev_docs_target",
      "organizationId": "org_docs_target",
      "workspaceId": "ws_docs_fixture",
      "name": "Fixture app",
      "slug": "fixture-app",
      "description": "",
      "category": "",
      "ownerType": "third_party",
      "portfolio": null,
      "audience": "business",
      "tenancy": "organization",
      "surfaceModel": "web_app",
      "distributionModel": "marketplace",
      "appStatus": "draft",
      "listingStatus": "not_submitted",
      "pricing": {
        "type": "free"
      },
      "version": "0.0.0",
      "websiteUrl": "",
      "demoUrl": "",
      "repoUrl": "",
      "supportEmail": "",
      "registeredAt": null,
      "marketplaceSubcategory": null,
      "tags": [],
      "screenshots": [],
      "notes": "",
      "metadata": {
        "ownerType": "third_party",
        "portfolio": null,
        "audience": "business",
        "tenancy": "organization",
        "surfaceModel": "web_app",
        "distributionModel": "marketplace",
        "members": [
          {
            "email": "claimant@example.invalid",
            "role": "owner",
            "name": "Fixture claimant",
            "userId": "user_docs_target",
            "addedAt": "2026-10-23T00:00:00.000Z"
          }
        ],
        "transfer": {
          "status": "completed",
          "codeHash": null,
          "codeIssuedAt": "2026-10-23T00:00:00.000Z",
          "codeExpiresAt": "2026-10-30T00:00:00.000Z",
          "codeIssuedByEmail": "owner@example.invalid",
          "claimedByEmail": "claimant@example.invalid",
          "targetOrganizationId": "org_docs_target",
          "targetOrganizationSlug": "fixture-target",
          "previousOrganizationId": "org_docs_fixture",
          "previousOrganizationSlug": "fixture-origin",
          "transferredAt": "2026-10-23T00:00:00.000Z",
          "transferredByEmail": "claimant@example.invalid"
        }
      },
      "members": [
        {
          "email": "claimant@example.invalid",
          "role": "owner",
          "name": "Fixture claimant",
          "userId": "user_docs_target",
          "addedAt": "2026-10-23T00:00:00.000Z"
        }
      ],
      "transfer": {
        "status": "completed",
        "codeHash": null,
        "codeIssuedAt": "2026-10-23T00:00:00.000Z",
        "codeExpiresAt": "2026-10-30T00:00:00.000Z",
        "codeIssuedByEmail": "owner@example.invalid",
        "claimedByEmail": "claimant@example.invalid",
        "targetOrganizationId": "org_docs_target",
        "targetOrganizationSlug": "fixture-target",
        "previousOrganizationId": "org_docs_fixture",
        "previousOrganizationSlug": "fixture-origin",
        "transferredAt": "2026-10-23T00:00:00.000Z",
        "transferredByEmail": "claimant@example.invalid"
      },
      "submissionId": null,
      "submittedAt": null,
      "reviewedAt": null,
      "reviewedBy": null,
      "rejectionReason": null,
      "createdAt": "2026-10-21T08:00:00.000Z",
      "updatedAt": "2026-10-23T00:00:00.000Z",
      "actorRole": "owner",
      "canRead": true,
      "canEdit": true,
      "canTransfer": true
    }
  }
}

Compare developerId/organizationId/member/transfer, and separately qualify the retained workspace mismatch with the owner. Do not treat transferred:true as full migration or run another claim.

Expired code produces a refusal after expiry metadata write

Complete source-derived synthetic UNRUN HTTP422. Fictional pending code has reached expiry. Source first attempts status:expired/codeHash:null before responding; this example is not no-effects proof.

{
  "success": false,
  "error": {
    "message": "Transfer code has expired. Ask the current owner to issue a new code.",
    "details": {
      "transferCode": true
    }
  }
}

Preserve actual status, original context and request id; do not infer no initialization/write or broaden/replay the operation.

Missing code

Complete source-derived synthetic UNRUN HTTP422. The permissive declared schema allows this JSON, but the handler rejects it after context/onboarding/role checks.

{
  "success": false,
  "error": {
    "message": "Transfer code is required.",
    "details": {
      "transferCode": true
    }
  }
}

Preserve actual status, original context and request id; do not infer no initialization/write or broaden/replay the operation.

Already assigned to this organization

Complete source-derived synthetic UNRUN HTTP422. Code is pending and unexpired, but claimant developer organization matches app.organizationId. This message does not establish canonical workspace equality.

{
  "success": false,
  "error": {
    "message": "This developer app is already assigned to the current workspace.",
    "details": {
      "transferCode": true
    }
  }
}

Preserve actual status, original context and request id; do not infer no initialization/write or broaden/replay the operation.

Verify and recover

  1. Retain the actual full result, request identity, app id, original/target organization and transfer expiry/state. Compare stored app owner/member/transfer fields through current authorized reads, not invented ids.

  2. Do not automatically repeat after response loss, timeout or a refusal that may follow shared initialization or data writes. Existing owner must reconcile actual transfer/submission state and competing custody.

  3. Qualify canonical workspace and downstream ownership separately. The claim source does not write workspace_id or reconcile Auth/registry/catalog/resources/financial effects. No automatic transfer-back, reissue, member restoration or cross-context retry is published as a safe inverse.

Change log

  • 2026-10-23: Added source-qualified transfer access, full synthetic unrun carriers, expiry/ownership effects, non-atomic recovery and canonical-workspace limitations. No transfer code, claim, source test or business operation was executed.