publ betaReference (MCP guide)
Open publ console

promotion_handoff

[Migration map](../reference/tool-name-migration.md).

# promotion_handoff

This implementation is a dependency of the documentation branch.

Return an exact human console destination for a campaign, benefit or product change. The tool observes context and targets using GET; it never performs the requested offer or financial action.

## Access and input

Requires OWNER or DELEGATEE with MANAGE_PROMOTION. Hosted source scope is the exact selected channel, not account scope. channelCode and opaque pcid/pbid follow the [reader's identity rules](publ_promotions_get.md#access-and-input). Strict inputs do not accept arbitrary URLs, financial payloads, requestId, expectedRevision or dryRun.

| Input | Contract |
| --- | --- |
| channelCode | Required selected channel code. |
| action | Required discriminated object. |
| action.kind | One of the eight destinations below. |
| action.pcid | Required except for create-campaign. |
| action.pbid | Required for benefit-edit, benefit-activation, products and product-add only. |

```json
{"channelCode":"selected-channel-code","action":{"kind":"create-campaign"}}
```

```json
{"channelCode":"selected-channel-code","action":{"kind":"campaign-edit","pcid":"observed-campaign"}}
```

```json
{"channelCode":"selected-channel-code","action":{"kind":"product-add","pcid":"observed-campaign","pbid":"observed-benefit"}}
```

## Creation policy

create-campaign reads an unfiltered page 1 with limit 15 and the selected channel's current plan. It uses pagination.totalEntries, not the current displayed/filtered row count. NORMAL_ESSENTIAL permits creation only when the existing count is zero. NORMAL_BUSINESS, NORMAL_FROM_ENTERPRISE, NORMAL_PRO, ENTERPRISE_TRIAL, ENTERPRISE_ENTERPRISE, PARTNER_TRIAL, PARTNER_PARTNER and INTERNAL_INTERNAL are eligible under the shared higher-plan rule. FREE_TRIAL, NORMAL_STARTER, NORMAL_STARTER_2023_04 and NORMAL_BASIC are denied. Unknown or missing plans remain unverified and cannot create.

creationPolicy contains currentPlan, canCreate, policyVerified, existingCampaignCount, sourceRule, sourceCountObserved:true and reserved:false. sourceRule describes Essential-or-higher with one Essential campaign slot. Using the unfiltered total is a conservative interpretation of the source button's displayed-list check. A count/plan observation is not a slot reservation, subscription change or proof an eligible creation will succeed.

Blocked or unknown creation policy returns PROMOTION_PLAN_ACTION and the exact channel-plan destination. An eligible creation or verified existing target returns PROMOTION_HUMAN_ACTION. No plan is purchased or upgraded.

## Destinations and result

All routes below are relative to `/channels/{channelCode}/promotion-campaign` and encode dynamic identities. Existing campaign/benefit targets receive repeated parent/content, selected-channel, operator and credential checks before navigation.

| action.kind | Required identity | Console suffix |
| --- | --- | --- |
| create-campaign | None | /campaigns/create; blocked policy uses channel-plan instead. |
| campaign-edit | pcid | /campaigns/{pcid}/profile/edit |
| campaign-activation | pcid | /campaigns/{pcid}/profile |
| add-benefit | pcid | /campaigns/{pcid}/benefits/add |
| benefit-edit | pcid, pbid | /campaigns/{pcid}/benefits/{pbid}/edit |
| benefit-activation | pcid, pbid | /campaigns/{pcid}/benefits/{pbid} |
| products | pcid, pbid | /campaigns/{pcid}/benefits/{pbid}/target-product |
| product-add | pcid, pbid | /campaigns/{pcid}/benefits/{pbid}/target-product/add |

The result is always action_required on a valid handoff, with writesPerformed:false, action, requiredAction.kind open_console and verification {confirmed:false,targetObserved:true,financialActionPerformed:false}. Creation alone includes creationPolicy. Demo omits live consoleUrl. An observed target or returned link is not completed work. When choosing the human console path, product price/disconnection dialogs are opened by the person from the products screen; no modal URL or generic console-home fallback is invented.

Errors use the reader's identity, pagination, stability and permission boundaries. A missing or foreign target does not fall back to a broader route. No task journal is created; task status/resume and mutation retry do not apply. This handoff performs none of the creation, activation, schedule, discount, grant, product attachment or pricing actions; use the separate implemented writers below where their exact scope applies. The separate [notification writer](publ_promotion_notifications_apply.md) supports only the two documented coupon push settings.

## Current existing-campaign editing

The earlier mandatory handoff boundary for campaign metadata/schedule/activation is superseded. promotions_get view profile now returns campaignRevision for [promotion_campaign_apply](../tools/promotion_campaign_apply.md), which saves the selected existing campaign profile or activation while preserving benefits and product prices. Existing benefit profile/activation uses [promotion_benefit_apply](promotion_benefit_apply.md). A handoff remains available for people. [promotion_campaign_create](promotion_campaign_create.md) supports a new active campaign with one initial ORDER_DISCOUNT benefit. [promotion_benefit_create](promotion_benefit_create.md) separately adds one ORDER_DISCOUNT or DISCOUNT to an existing campaign with Business-or-higher. Money movement stays human-operated.

For an existing connected relationship, use [promotion_benefit_apply product-disconnect](promotion_benefit_apply.md#disconnect-an-existing-product-relationship) with the exact products-page revision and connected row ID. This removes only the relationship, verifies complete bounded absence and preserves the underlying product. Eligible new connections now use the bounded product-connect flow below; restorationSupported remains false, with no automatic relinking or rollback. A handoff is still GET-only/action_required and never claims the change happened. Financial execution remains a human-console action.

To connect eligible existing products to an existing DISCOUNT benefit, read [product-options](publ_promotions_get.md#product-connection-options) and use [product-connect](promotion_benefit_apply.md#connect-existing-products-to-a-discount-benefit) with connectionRevision and explicit values. The active-campaign prerequisite and five supported families apply; category model ambiguity, fixed-price CP conversion and deleted-product reconnection remain gaps. This creates relationships only. Additional DISCOUNT creation uses [promotion_benefit_create](promotion_benefit_create.md#add-a-discount-benefit-with-optional-initial-products) with its own options/creation revisions and Business-or-higher gate, permitting inactive parents. Neither flow creates underlying products, restores previous identities or executes finance.

## Inputs

| Field | Required here | Contract |
| --- | --- | --- |
| `action` | Yes | one of: `create-campaign`, `campaign-edit`, `campaign-activation`, `add-benefit`, `benefit-edit`, `benefit-activation`, `products`, `product-add` (by `kind`) |
| `action.pcid` | Yes | When `action.kind` is `campaign-edit` or `campaign-activation` or `add-benefit`: string; pattern `^[A-Za-z0-9_-]{1,128}$` |
| `action.pcid` | Yes | When `action.kind` is `benefit-edit` or `benefit-activation` or `products` or `product-add`: string; pattern `^[A-Za-z0-9_-]{1,128}$` |
| `action.pbid` | Yes | When `action.kind` is `benefit-edit` or `benefit-activation` or `products` or `product-add`: string; pattern `^[A-Za-z0-9_-]{1,128}$` |