Topolo Docs Topolo Platform
Open workspace

Issue an app transfer code

Create a pending app transfer code with explicit owner access, seven-day expiry and no automatic retry.

Outcome

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

POST /api/developer-console/apps/{appId}/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.

App-specific owner access in the original source workspace is required. Issuing a code is not consent to claim into any unspecified target.

Request

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

{
  "appId": "app_docs_fixture"
}

Effects and limitations

The owner issues a pending transfer; ownership is not moved by issuance. requireDeveloperAppAccess enforces app/developer workspace match, app.canRead and canTransfer (actorRole owner). App editors/viewers cannot issue a transfer merely because their credential has apps:write.

The handler ignores the request body for issuance after routing appId. The broad declared input schema has many permissive unrelated fields; they do not define an editable target organization or expectedRevision for this route. Supply the discovered appId only.

A code is generated from12random characters using the helper alphabet, formatted4-4-4. The code hash is SHA-256 of its uppercase alphanumeric normalization, lowercase hex. Expiry is seven days from issuance. Metadata.transfer becomes pending with codeHash, issuance/expiry/issuer, current previousOrganization and null target/claimant; historical transferredAt/transferredByEmail may be preserved. Existing members are retained.

A new issuance replaces the pending transfer metadata, so an earlier code is no longer the current matching pending code. The source has no request idempotency key, expected revision or compare-and-swap for this replacement. Do not issue codes in parallel or automatically reissue after response loss.

updateDeveloperApp writes metadata and updated_at, then rereads and separately writes canonical_app_id when the record remains. These writes are not a transaction with the route’s read. A failure/refusal after a write is not proof of no effect. A null returned updated app produces404 after the attempted update.

HTTP200 returns {success:true,message,data:{transferCode,transferExpiresAt,appId,app}}. app is the full actor-decorated app record, not an ownership-move receipt. The source app carrier includes transfer metadata/hash, so no real output should be placed into public examples.

Issuance does not update developerId/organizationId/workspaceId, move submissions, reconcile Auth/service/catalog/entitlements, or complete financial/account migration. The subsequent claim route has additional known limits described in its guide.

Worked responses

Owner receives a new pending transfer code

Complete synthetic UNRUN HTTP200, fictional owner, app and code. The existing source fixture is an app owner in the source canonical workspace; there was no previous transfer and issuance time is fixed for illustration. This is a direct service carrier, not proof of a particular CLI secret-redaction behavior.

{
  "success": true,
  "message": "Developer app transfer code issued successfully",
  "data": {
    "transferCode": "ABCD-EFGH-JKLM",
    "transferExpiresAt": "2026-10-30T00:00:00.000Z",
    "appId": "app_docs_fixture",
    "app": {
      "id": "app_docs_fixture",
      "developerId": "dev_docs_fixture",
      "organizationId": "org_docs_fixture",
      "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": [],
        "transfer": {
          "status": "pending",
          "codeHash": "3f63d25fcb898ecfd6c2e2322bfdb11f44fe17dd0dc359e70b7246224bb223e9",
          "codeIssuedAt": "2026-10-23T00:00:00.000Z",
          "codeExpiresAt": "2026-10-30T00:00:00.000Z",
          "codeIssuedByEmail": "owner@example.invalid",
          "claimedByEmail": null,
          "targetOrganizationId": null,
          "targetOrganizationSlug": null,
          "previousOrganizationId": "org_docs_fixture",
          "previousOrganizationSlug": "fixture-origin",
          "transferredAt": null,
          "transferredByEmail": null
        }
      },
      "members": [],
      "transfer": {
        "status": "pending",
        "codeHash": "3f63d25fcb898ecfd6c2e2322bfdb11f44fe17dd0dc359e70b7246224bb223e9",
        "codeIssuedAt": "2026-10-23T00:00:00.000Z",
        "codeExpiresAt": "2026-10-30T00:00:00.000Z",
        "codeIssuedByEmail": "owner@example.invalid",
        "claimedByEmail": null,
        "targetOrganizationId": null,
        "targetOrganizationSlug": null,
        "previousOrganizationId": "org_docs_fixture",
        "previousOrganizationSlug": "fixture-origin",
        "transferredAt": null,
        "transferredByEmail": null
      },
      "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
    }
  }
}

A pending transfer exists; ownership and workspace remain unchanged. Preserve the actual code securely, expiry and original request identity.

Editor is not an app owner

Complete source-derived synthetic UNRUN HTTP403. The current app canRead is true but canTransfer is false.

{
  "success": false,
  "error": {
    "message": "Owner access is required to transfer this developer app."
  }
}

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

App inaccessible

Complete source-derived synthetic UNRUN HTTP404. The app is missing, not actor-readable or mismatches the selected source workspace; not global absence proof.

{
  "success": false,
  "error": {
    "message": "Developer app not found"
  }
}

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.