Topolo Docs Topolo Platform
Open workspace

Update an app’s marketing metadata

Change approved app copy and media with exact merge rules, catalog side effects and safe recovery after partial failure.

Outcome

Update the approved marketing copy and media for one accessible developer app. This can also trigger catalog, Auth-projection and notification work.

PUT /api/developer-console/apps/{appId}/marketing

Action: app_topolo_developers.apps.marketing.update.

Before you start

Use the original authenticated organization with Developers apps:write. Discover the accessible canonical Developers workspace and app id, bind workspace separately, and obtain approval for the actual copy/media, catalog/Auth projection and potential notification effects before confirming this mutation.

Complete Developers onboarding and use a currently writable developer profile with edit access to this app in the selected workspace. Suspended-read access is insufficient. A workspace mismatch or unreadable app returns404; a readable app without edit access returns403. Platform, organization administrator, explicit membership and developer ownership are distinct branches.

Read apps.marketing.get and retain the exact prior metadata/subcategory before planning the edit. Marketing copy can be public-facing; do not insert secrets or private material. This technical guide contains fictional data only and does not authorize any real app change.

The shared request path can insert a local identity-only workspace mirror and, for an enabled organization without a developer profile, create the initial developer profile after resolving a platform-issued workspace. This can precede app access refusal. No canonical Auth workspace is minted, and readOnly metadata is not a promise of zero initialization writes.

All worked responses are source-derived synthetic UNRUN examples. No real developer app, declaration, provider, key, person, private record, publication or other business action was read or changed. The immutable source matches the retained declaration revision; serving-runtime adoption is not independently proven.

Plan the request

Read the original app marketing first. The following input is fictional; discover the actual app and bind its canonical workspace separately, validate the current contract and review the actual plan before confirming a real mutation.

{
  "appId": "app_docs_fixture",
  "shortBlurb": "Track warehouse stock and replenishment."
}

Fields, saved state and side effects

The handler accepts PUT /api/developer-console/apps/{appId}/marketing, calls shared profile/workspace resolution, requires onboardingState complete and app edit access, then updates metadata.marketing, marketplace_subcategory and updated_at on the app id. There is no expectedRevision, compare-and-set, operation id or idempotency key in this route; an intervening edit can be overwritten.

This is a partial input merge followed by a whole metadata write, not a pure field patch. Existing marketing keys are spread first, but the legacy visible key is removed, launcher quick links are normalized even when omitted, and missing defaults/collections are materialized. Even {appId} can write and trigger publication; it is not a safe no-op or health probe.

When description is supplied, the handler trims it and requires130–200 JavaScript string characters, domain-specific copy without its generic catalog-boilerplate patterns, and a case-insensitive trimmed exact description distinct from every other developer app. The duplicate query is not scoped by organization/workspace and is not protected by a uniqueness transaction. An omitted description is retained after trimming without revalidating old copy; this does not approve that copy. Native validation must still be used; it does not express every handler rule.

String fields are trimmed/truncated in the handler: marketplaceCategory64, status32, stageLabel64, surface120, shortBlurb400, tagline200, problemStatement1000, solutionStatement1000, docsUrl400 and appUrl400. Empty supplied strings usually fall back to previous values; category/status/stage default to operations/preview/Preview. These fields cannot generally be cleared by sending an empty string. subcategory is separate: an empty string clears it to null. The published contract admits string subcategory, not null.

benefits and gettingStarted are converted from arrays into at most12 nonempty strings, each at most400 characters. Nonarray input becomes an empty list. gallery/features/stats/worksWellWith are capped at20 entries; omitted values preserve stored values or default to []. heroMedia is retained when omitted and otherwise assigned; the published schema requires a strict media object with type image|video, URL src and nonempty alt up to240 characters, optional URL poster and caption up to400. gallery uses the same strict media shape. Media URLs are authored configuration, not proof of an uploaded, accessible or safe asset. Do not bypass native validation with handler coercions.

launcherQuickLinks is normalized to at most16 valid objects. Title is trimmed/capped80, subtitle180, group60, keywords up to12 trimmed strings capped60, rank rounded/clamped1–200. A link needs a title and derived path. Slash-prefixed strings are retained directly; other paths/URLs must resolve to the app base origin and are reduced to path/query/fragment. The direct slash branch also accepts double-slash strings, so this is not a guarantee of safe same-origin browser navigation. Base URL is websiteUrl, demoUrl, repoUrl, then the Developers app-page fallback. Omitted links are re-read through the legacy alias precedence and normalized.

The published input schema has many shared mutation fields. Presence in that schema does not make them marketing effects: this helper changes only the marketing fields/subcategory described here. In particular it does not update app pricing, app name, ownership, appStatus or permissions from similarly named shared fields. marketing.status is distinct from app lifecycle/listing/runtime state. heroImage is excluded; use the typed heroMedia field.

After the control-row write, the handler reloads the app and, when present, awaits publishDeveloperAppById. That helper reads published action data and may synchronize or retract first-party Auth projection, notify permissions, publish the application catalog, or purge ineligible application catalog projections. It does not republish the action-catalog partitions. It can attempt developers.app.published. On failure it attempts to insert a durable catalog-sync failure record; that insert can itself fail. It attempts developers.catalog.sync_failed only if that failure record was saved. Notification delivery is not established by this guide. Saved marketing can therefore affect more than copy, but the response is not a deployment, paid-entitlement, catalog receipt or delivery certificate.

The local comment calls publication best-effort, but the executable helper attempts failure recording and rethrows; the route has no catch around the await. The outer handler converts an ordinary thrown error to HTTP500 with nested error.message. A failure can occur after the marketing save or partial catalog/Auth effects. Never assume a500 or timeout means nothing changed, and never blindly repeat the PUT or restore all prior metadata.

The complete normal200 carrier is {success:true,message:"App marketing metadata updated",data:{appId,marketing,subcategory}}. marketing is the decorated refreshed metadata, then refreshed metadata, then prepared nextMarketing fallback; subcategory follows analogous nullish fallbacks. If a concurrent writer changes the record between save and reload, returned values may reflect that later state. If reload yields no app, the prepared fallback can still return200 without a publication attempt. Neither200 nor an empty collection proves public visibility or healthy runtime.

Worked responses

Save a short blurb on an empty legacy marketing object

Complete synthetic source-derived UNRUN HTTP200. The fictional authorized app has empty marketing, null subcategory and no quick links. Reload succeeds with the saved state and publication returns normally. Description was omitted, so legacy empty description is retained; this is not a copy-quality approval.

Request:

{
  "appId": "app_docs_fixture",
  "shortBlurb": "Track warehouse stock and replenishment."
}

Response:

{
  "success": true,
  "message": "App marketing metadata updated",
  "data": {
    "appId": "app_docs_fixture",
    "marketing": {
      "marketplaceCategory": "operations",
      "status": "preview",
      "stageLabel": "Preview",
      "surface": "",
      "shortBlurb": "Track warehouse stock and replenishment.",
      "description": "",
      "gallery": [],
      "benefits": [],
      "features": [],
      "stats": [],
      "gettingStarted": [],
      "worksWellWith": [],
      "launcherQuickLinks": []
    },
    "subcategory": null
  }
}

Read the actual saved result. Defaults and arrays are materialized; omitted undefined fields are absent in the JSON carrier. No real app mutation or runtime/publication test occurred.

Clear the marketplace subcategory with a supported empty string

Complete synthetic UNRUN HTTP200 using the exact prior marketing from the first fixture and a previously nonnull subcategory. Empty subcategory becomes null while the normalized marketing remains the same.

Request:

{
  "appId": "app_docs_fixture",
  "subcategory": ""
}

Response:

{
  "success": true,
  "message": "App marketing metadata updated",
  "data": {
    "appId": "app_docs_fixture",
    "marketing": {
      "marketplaceCategory": "operations",
      "status": "preview",
      "stageLabel": "Preview",
      "surface": "",
      "shortBlurb": "Track warehouse stock and replenishment.",
      "description": "",
      "gallery": [],
      "benefits": [],
      "features": [],
      "stats": [],
      "gettingStarted": [],
      "worksWellWith": [],
      "launcherQuickLinks": []
    },
    "subcategory": null
  }
}

This uses a string permitted by the contract; do not assume null is a permitted input or that empty values clear all marketing fields.

Onboarding is incomplete

Complete synthetic UNRUN route HTTP403 before app marketing write. Shared context initialization may already have occurred. The declaration annotation says409; this source branch actually uses403.

Request:

{
  "appId": "app_docs_fixture",
  "shortBlurb": "Track warehouse stock and replenishment."
}

Response:

{
  "success": false,
  "error": {
    "message": "Complete onboarding before updating app marketing content."
  }
}

Read the original onboarding state and follow the separately authorized onboarding workflow. Do not repeat the write or infer a new app-level grant.

A readable viewer cannot edit

Complete synthetic UNRUN HTTP403 after a readable app is decorated with canEdit:false. There is no error.code in this route response.

Request:

{
  "appId": "app_docs_fixture",
  "shortBlurb": "Track warehouse stock and replenishment."
}

Response:

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

Preserve the actor and workspace, and ask the legitimate owner for a reviewed edit rather than switching identities.

The app is absent or inaccessible in this workspace

Complete synthetic UNRUN HTTP404 after context resolution. Workspace mismatch, unreadable app and absence share this carrier. Even this otherwise empty request is a mutation intent if accepted.

Request:

{
  "appId": "app_docs_fixture"
}

Response:

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

Reconcile the discovered app and selected workspace. Do not guess another app or use this mutation as a discovery probe.

Verify and recover

  1. After any outcome, use the original approved principal, organization, workspace and app id with apps.marketing.get. Compare the actual normalized saved fields and subcategory to the approved intent and prior snapshot. Preserve request ids/errors and inspect current state before deciding whether any retry is needed.

  2. For separately authorized public-listing work, check the appropriate actual catalog/public app surface and owning publication evidence as well as the control metadata. A saved-copy read is insufficient to prove catalog/Auth synchronization, provider health, deployment or notification delivery.

  3. On403, complete the intended onboarding or obtain legitimate current edit access through the owning workflow; do not switch organization or expand grants automatically. On404, reconcile the discovered app and workspace without creating a replacement.

  4. After a timeout/500/partial publication, preserve the saved metadata and actual publication outcome for the owner. No automatic replay, message resend or presumed rollback. A later compensating edit must be approved against current state; omission/defaulting and fields that cannot be cleared mean the prior object is not an exact inverse.

Read current marketing and actual publication effects first. An approved compensating edit is a new mutation, not an exact rollback: defaults, removed visible, normalizations, inability to clear some fields and concurrent edits prevent blind restoration. No automatic retry or notification/catalog inverse.

Change log

  • 2026-10-22: Added source-qualified partial-merge/defaulting rules, media and launcher-link limits, true403 onboarding refusal, complete synthetic response examples, and saved-before-publication failure/recovery semantics. Examples are UNRUN; deployed application behaviour is not certified.