Topolo Docs Topolo Platform
Open workspace

Save an unpublished pricing draft

Save an approved commercial draft with revision checks while preserving its published snapshot and unresolved-publication boundaries.

Outcome

Save an approved commercial draft with revision checks while preserving its published snapshot and unresolved-publication boundaries.

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

Before you start

Use the original environment, organization, authorized app and platform-confirmed Developers workspace. Discover the current credential-scoped action and any separately required resource binding; never use fictional identifiers for a live operation.

These are confirmed mutations under the published apps:write permission. Target app edit access, developer-workspace status and runtime policy remain authoritative; a plan or GET canWrite flag is not a grant.

Every commercial write to topolo_first_party additionally requires the canonical platform-super-admin commercial predicate. A human must approve the actual commercial scope before a real write.

All examples are source-derived synthetic UNRUN carriers. No application, financial, billing, broker, provider or catalog operation ran. Source/declaration equality is not serving-runtime proof.

Request

Action: app_topolo_developers.apps.pricing.update. Discover current context, validate/plan the exact payload and confirm its actual commercial effect before any live operation.

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "draft": {
    "appId": "app_docs_fixture",
    "developerOrgId": "org_docs_fixture",
    "mode": "external",
    "label": "Publisher billing",
    "url": "https://example.invalid/plans"
  }
}

Input fields

FieldRequiredMeaning
appIdYesActual authorized app identity in the original organization/workspace.
expectedRevisionYesExact current revision; nonnegative for saving.
draftYesFull strict commercial or external draft with matching identity.

Effects and state transitions

Shared context authenticates, resolves a platform-issued workspace and checks app access before authoring guards. It can initialize local developer/profile metadata before a later refusal; it does not create a canonical Auth workspace. Revoked and suspended developer access is refused for mutations.

Success uses {success:true,message,data:{appId,kind:"commercial",state}}. The OK display value is reused from the accepted source-qualified pricing.get precedent. Normal errorResponse refusals use an error object with message and optional code/details; raw publication-failure503 carriers instead have a string error and data.state.

The declared output is a permissive structural projection. State carries revision,draft,published,publication; readAppAuthoring validates only a nonnegative safe integer revision and otherwise returns stored metadata. Schema shape is not complete downstream validation.

expectedRevision must be a safe integer equal to the actual current state revision. Any publishing/failed publication blocks editing before draft parsing, even when a retained publication revision is inconsistent. A draft must be a non-array object and pass strict commercialDraftInput parsing.

External mode requires appId, mode:"external" and a nonempty label of at most120characters; an optional URL must be HTTPS without credentials. developerOrgId is structurally optional but the route still requires the exact owning organization for developer-owned apps, or explicitly null for first-party apps.

Non-external drafts require meters and at least one plan. Strict schemas define allowed pricing models, tier/interval/currency, cent amounts, tiers and quotas. The locked billing-manifest2.0.0 validator also checks duplicate plan IDs, duplicate meter keys and quotas referencing undeclared meters. The artifact was read as data; its validator was not run.

Non-first-party non-external drafts first require a complete Auth commercial policy read. The top-level and each proposed plan revShareBps are compared by strict equality with current policy. Missing/null values and newly introduced plans can matter. External mode skips this broker branch; first-party writes still require commercial authority.

Successful saves set revision=current+1 and the full draft while retaining published snapshot, publication metadata and other authoring fields. No draft publication, commercial publish broker or catalog-consumer activation occurs here. Identical content still advances the revision.

writeAppAuthoring conditionally writes against prior revision, attempt ID and entire previous state. Concurrent differences yield409 App authoring changed concurrently. No complete immutable draft-history API is exposed here; retain the prior draft before editing.

Worked responses

Save an external-pricing draft

Complete source-derived synthetic UNRUN HTTP 200. Current state is revision0,draft:null,published:null,publication:null. The developer-owned app and caller are authorized. External mode skips the policy-read broker. All identifiers, times and receipt data are fictional. No API, billing, broker or provider operation ran.

Request:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "draft": {
    "appId": "app_docs_fixture",
    "developerOrgId": "org_docs_fixture",
    "mode": "external",
    "label": "Publisher billing",
    "url": "https://example.invalid/plans"
  }
}

Complete representative response:

{
  "success": true,
  "message": "OK",
  "data": {
    "appId": "app_docs_fixture",
    "kind": "commercial",
    "state": {
      "revision": 1,
      "draft": {
        "appId": "app_docs_fixture",
        "developerOrgId": "org_docs_fixture",
        "mode": "external",
        "label": "Publisher billing",
        "url": "https://example.invalid/plans"
      },
      "published": null,
      "publication": null
    }
  }
}

Read apps.pricing.get and compare actual app, revision1 and complete draft; nothing here publishes prices or changes financial bindings.

Saving identical content still advances the revision

Complete source-derived synthetic UNRUN HTTP 200. Current state is the complete published revision1 fixture shown in the response. The same approved draft is saved again; published snapshot and publication metadata are retained exactly. All identifiers, times and receipt data are fictional. No API, billing, broker or provider operation ran.

Request:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 1,
  "draft": {
    "appId": "app_docs_fixture",
    "developerOrgId": "org_docs_fixture",
    "mode": "external",
    "label": "Publisher billing",
    "url": "https://example.invalid/plans"
  }
}

Complete representative response:

{
  "success": true,
  "message": "OK",
  "data": {
    "appId": "app_docs_fixture",
    "kind": "commercial",
    "state": {
      "revision": 2,
      "draft": {
        "appId": "app_docs_fixture",
        "developerOrgId": "org_docs_fixture",
        "mode": "external",
        "label": "Publisher billing",
        "url": "https://example.invalid/plans"
      },
      "published": {
        "revision": 1,
        "draft": {
          "appId": "app_docs_fixture",
          "developerOrgId": "org_docs_fixture",
          "mode": "external",
          "label": "Publisher billing",
          "url": "https://example.invalid/plans"
        },
        "publishedAt": "2026-10-23T00:00:00.000Z"
      },
      "publication": {
        "revision": 1,
        "attemptId": "11111111-1111-4111-8111-111111111111",
        "executionId": "22222222-2222-4222-8222-222222222222",
        "draftDigest": "5e76bfe3955501184646ec4da74da5d2ec47f02fd2da4585f7998796ff201b44",
        "publishedAt": "2026-10-23T00:00:00.000Z",
        "status": "published",
        "outcomes": {
          "catalog": {
            "success": true,
            "receipt": {
              "status": "catalog_committed",
              "appId": "app_docs_fixture",
              "operationId": "app_docs_fixture:commercial:11111111-1111-4111-8111-111111111111",
              "generation": 1,
              "bindingDigest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
              "manifestDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
              "publishedAt": "2026-10-23T00:00:00.000Z",
              "operation": {
                "kind": "commercial",
                "revision": 1,
                "attemptId": "11111111-1111-4111-8111-111111111111",
                "draftDigest": "5e76bfe3955501184646ec4da74da5d2ec47f02fd2da4585f7998796ff201b44"
              }
            }
          }
        }
      }
    }
  }
}

Authoring revision becomes2; published.revision and publication.revision remain1. Content equality is not a save idempotency key.

Stale expected revision

Complete source-derived synthetic UNRUN HTTP 409. Current state is already revision1; expectedRevision is still0. All identifiers, times and receipt data are fictional. No API, billing, broker or provider operation ran.

Request:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "draft": {
    "appId": "app_docs_fixture",
    "developerOrgId": "org_docs_fixture",
    "mode": "external",
    "label": "Publisher billing",
    "url": "https://example.invalid/plans"
  }
}

Complete representative response:

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

Read actual state and reconcile the earlier save before approving a new revision.

Unfinished publication prevents edits

Complete source-derived synthetic UNRUN HTTP 409. The matching current revision has publication.status publishing or failed. This guard precedes draft parsing. All identifiers, times and receipt data are fictional. No API, billing, broker or provider operation ran.

Request:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 1,
  "draft": {
    "appId": "app_docs_fixture",
    "developerOrgId": "org_docs_fixture",
    "mode": "external",
    "label": "Publisher billing",
    "url": "https://example.invalid/plans"
  }
}

Complete representative response:

{
  "success": false,
  "error": {
    "message": "Resume or finish the current publication before editing"
  }
}

Preserve attempt/outcomes; changing a draft must not hide an unresolved publication.

First-party commercial authority is required

Complete source-derived synthetic UNRUN HTTP 403. The app is topolo_first_party and the actor fails hasPlatformSuperAdminAccess. The guard precedes body parsing. All identifiers, times and receipt data are fictional. No API, billing, broker or provider operation ran.

Request:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "draft": {
    "appId": "app_docs_fixture",
    "developerOrgId": "org_docs_fixture",
    "mode": "external",
    "label": "Publisher billing",
    "url": "https://example.invalid/plans"
  }
}

Complete representative response:

{
  "success": false,
  "error": {
    "message": "Platform commercial authority is required"
  }
}

Ordinary app edit or organization ownership is not enough; do not silently widen access or switch context.

Commercial identity must match the owning app

Complete source-derived synthetic UNRUN HTTP 422. The draft shape is valid but its appId differs from the authorized route app. All identifiers, times and receipt data are fictional. No API, billing, broker or provider operation ran.

Request:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "draft": {
    "appId": "app_other_fixture",
    "developerOrgId": "org_docs_fixture",
    "mode": "external",
    "label": "Publisher billing",
    "url": "https://example.invalid/plans"
  }
}

Complete representative response:

{
  "success": false,
  "error": {
    "message": "Commercial identity must match the owning app and organization"
  }
}

Use the real owning app/organization; first-party drafts require developerOrgId:null.

External drafts reject unknown fields

Complete source-derived synthetic UNRUN HTTP 422. Strict commercialDraftInput parsing rejects the extra field. All identifiers, times and receipt data are fictional. No API, billing, broker or provider operation ran.

Request:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "draft": {
    "appId": "app_docs_fixture",
    "developerOrgId": "org_docs_fixture",
    "mode": "external",
    "label": "Publisher billing",
    "url": "https://example.invalid/plans",
    "inventedSetting": true
  }
}

Complete representative response:

{
  "success": false,
  "error": {
    "message": "Draft does not match the published authoring schema"
  }
}

Correct the declared approved payload rather than dropping validation.

Commercial policy read is incomplete

Complete source-derived synthetic UNRUN HTTP 503. A structurally valid non-external third-party draft reaches the commercial read, which fails response.ok/success:true. No raw broker carrier is invented. All identifiers, times and receipt data are fictional. No API, billing, broker or provider operation ran.

Request:

{
  "appId": "app_docs_fixture",
  "expectedRevision": 0,
  "draft": {
    "appId": "app_docs_fixture",
    "developerOrgId": "org_docs_fixture",
    "meters": [],
    "plans": [
      {
        "id": "plan_fixture_free",
        "tier": "free",
        "displayName": "Free",
        "billingCurrency": "usd",
        "billingInterval": null,
        "pricing": {
          "model": "free"
        },
        "bullets": []
      }
    ]
  }
}

Complete representative response:

{
  "success": false,
  "error": {
    "message": "Commercial policy must be read before editing"
  }
}

No authoring save follows this refusal; transport or JSON exceptions instead reach the outer500 boundary.

Verify and recover

  1. Read apps.pricing.get in the same app/workspace and compare actual revision and full draft with the approved request. Distinguish a virtual reconciled GET view from persisted control state.

  2. Compare pre-snapshotted published revision/draft and publication outcomes; saving must retain them. This does not prove executable prices, payment objects or catalog adoption.

  3. After response loss or409, inspect actual state before another approved save. A landed save spends the original expectedRevision.

  4. For intentional recovery, review and save the retained prior draft as a NEW current revision only after resolving unfinished publication. This is a separate confirmed mutation, not an inverse of external effects.

Source and limits

Frozen Developers source 7b89bd3b39092cdc29f87540847bf99a243f0a40 and retained exact declared schemas were reviewed. Locked @topolo-io/billing-manifest@2.0.0 integrity was compared as data only. No target module, validator, source test or app operation ran. Auth broker implementation and live provider/consumer outcomes remain unproven.

Change log

  • 2026-10-23: Prepared source-derived draft-save guidance, strict inputs, complete synthetic unrun responses and financial/catalog/control evidence boundaries. No commercial operation executed.