publ betaReference (MCP guide)
Open publ console

publ Reference

promotion_benefit_apply

Edit one existing promotion benefit profile, activation state or connected-product discount, connect existing eligible products to a DISCOUNT benefit, or disconnect one existing product relationship. Requires the selected channel OWNER or a delegate with MANAGE_PROMOTION. Account-scoped connections do not expose this tool. It does not create a campaign, benefit or underlying product.

For profile or activation, first call promotions_get with view:{kind:"benefit",pcid,pbid} using observed opaque identities. Copy its benefitRevision, which binds the selected campaign, benefit and operator context. Send that revision with channelCode, pcid, pbid, a new requestId (8–80 letters, digits, hyphens or underscores) and one action. All input objects reject unsupported fields. Use dryRun:true to inspect the composed request; a dry run sends no business write and creates no task record.

Editable controls

action.kind:"profile" accepts one or more of the following fields. Omitted values are preserved rather than replaced with empty strings, false or current-time defaults.

Field Source control and guard
alias Management name; trimmed 4–20 UTF-16 units. Two emoji may count as four units.
schedule Complete {startAt,endAt} with ISO8601 UTC/offset instants. End must follow start. endAt:null is allowed only without a finite campaign end and outside LIMITED_TIME. Requested dates stay within the campaign. No timezone is inferred.
target {kind:"ALL"} or {kind:"SEGMENT",distinctId}. The source editor selects one segment. Its current record and conditions are read twice under the selected promotion credential. LIMITED_TIME cannot change its target type; an already-segment-based benefit can select another segment.
memberDisplayText DISCOUNT/ORDER_DISCOUNT only when the stored screen already exposes nonempty text. Trimmed 1–14 UTF-16 units.
benefitLimitOption DISCOUNT/ORDER_DISCOUNT only: ONE_TIME or UNLIMITED.
discountAmount ORDER_DISCOUNT with AMOUNT_DISCOUNT only. Plain decimal string in the freshly read channel base currency. KRW/JPY: integer multiples of 100, minimum 100, maximum 10000000. USD: multiples of 0.01, minimum 0.01, maximum 10000. No exchange, rounding or minor-unit conversion.
executionTrigger GRANT with an existing trigger only: EXECUTION_SIGNUP, EXECUTION_SIGNIN or EXECUTION_PURCHASE.
noticeStartAt LIMITED_TIME only: explicit ISO8601 instant or null. When supplied, or when the schedule changes, a retained notice must be between campaign start and benefit start. The bridge rejects conflicts instead of silently adjusting the notice.
autoToggleOnBenefitPeriod LIMITED_TIME only: boolean controlling automatic sales stopping. This can affect availability.

action:{kind:"activation",active:true|false} changes exactly the selected benefit through the source single-selection batch endpoint. It preserves the profile and connected products. Activation may affect live offer availability; obtain the user's business intent before applying it.

Benefit type, discount method, repeatability, expression trigger, overlap options, product connection and product prices are not arbitrary profile fields. Existing connected-product discounts, new eligible connections and relationship removal use their separate actions below. If a stored profile lacks a required alias or valid start date, supply its explicit replacement. The bridge does not infer values from today or silently discard multiple segments. Existing edits do not use the campaign-creation plan gate.

Save, verify and recover

The workflow compares repeated campaign/benefit/dependency reads, saves a durable prepared plan, records the sent boundary after taking the benefit pending claim, and dispatches one source request:

  • Profile: PATCH /seller/api/v1/promotion-campaigns/{pcid}/benefits/{pbid} with the complete source editor body.
  • Activation: POST /seller/api/v1/promotion-campaigns/{pcid}/bulk-activate or /bulk-deactivate, body {pbids:[pbid]}.
  • Product discount: PATCH /seller/api/v1/promotion-campaigns/{pcid}/benefits/{pbid}/target-products/{targetProductId}, containing only the selected discount field. The product receipt alone does not prove completion.
  • Product disconnect: POST to that target-product path plus /disconnect, with body {}. Only the relationship is removed; the receipt alone does not prove absence.
  • Product connect: POST /seller/api/v1/promotion-campaigns/{pcid}/benefits/{pbid}/target-products with the explicit composed targetProducts. The receipt is not authoritative evidence of new relationship identities.

The adapter requires the source 200/data envelope, selected bearer and channel header, bounded response and unchanged connection context. A profile receipt is checked before independent readback. Completion requires the requested values, unchanged other benefits/parent settings/connected products (except the intended relationship additions or removal), and stable selected dependencies. Equivalent price number/string representations and date instants are compared without changing target-product price values. Explicit foreign channel identities always fail. Source records do not always include a channel identity.

These observations are not an atomic compare-and-swap or a guarantee of customer eligibility, customer-facing rendering, delivery or charging. Concurrent changes may leave a sent task pending. No automatic rollback occurs.

Keep requestId, the original input and taskType:"promotion-benefit". task_status reads the saved record without releasing pending ownership. task_resume never dispatches: known-unsent prepared work requires the identical apply request, while a prepared record with its own durable claim is treated as an uncertain attempt and observed without sending. Sent requests never repeat. A completed replay without an outstanding own claim is historical and has no current console link. Read the matching benefit or products view again for fresh state. Recover the existing task before considering another request ID.

A fresh profile/activation plan or completion supplies one exact selected benefit-detail console address, under /channels/{channelCode}/promotion-campaign/campaigns/{pcid}/benefits/{pbid}. The canonical host comes from central configuration. No generic console home or invented dialog route replaces this address.

promotion_campaign_create supports a new active campaign with one initial ORDER_DISCOUNT benefit. Use promotion_benefit_create for an additional ORDER_DISCOUNT or DISCOUNT under Business-or-higher. Payments, withdrawal, settlement and service billing remain human-console actions.

Existing connected-product discount

Read promotions_get products and select a non-deleted connected row from that exact page. Send its numeric id as action.targetProductId; targetId and catalog product IDs are different identities. Copy productRevision to expectedRevision. Neither benefitRevision nor campaignRevision substitutes for it. Keep the observed page and limit explicitly, even though their schema defaults are 1 and 15.

Action field Contract
kind product-discount.
targetProductId Positive safe integer, present and non-deleted on the selected page.
page Positive safe integer from the read.
limit 15, 30, 50 or 100, matching the read.
mode FIXED_PRICE, RATE_DISCOUNT or AMOUNT_DISCOUNT; must equal the existing benefit's discountMethod. This does not change the discount method.
value Plain decimal string, at most 40 characters, matching (0|[1-9]\d*)(\.\d+)?. No sign, exponent, grouping, whitespace or leading zeroes. It must convert to a finite source number without changing its decimal meaning.

Only existing DISCOUNT benefits support this action. SubscriptionProduct, ChannelActionProduct and PAppUnlockProduct accept FIXED_PRICE only. CommerceProduct, CommerceProductCategory and TicketEvent accept RATE_DISCOUNT or AMOUNT_DISCOUNT only. GRANT, LIMITED_TIME and unsupported legacy combinations fail closed. The existing discount editor has no campaign, benefit or product activation prerequisite; their activation flags are preserved. Product-addition restrictions do not apply to this editor.

mode Selected PATCH field Allowed value
FIXED_PRICE discountedPrice:{amount,currency} Channel-currency benefit price, at least the source promotion minimum and at most the row's original price.amount. Original price currency must match the observed channel base currency.
RATE_DISCOUNT discountRatio Percentage 1 through 99, including fractional percentages: "15.5" means 15.5%, not 0.155. The only 100% exception is channel L2NoYW5uZWxzLzI3NjQ with CommerceProduct or CommerceProductCategory, never TicketEvent.
AMOUNT_DISCOUNT discountPrice:{amount,currency} KRW/JPY 100 through 10000000; USD 0.01 through 10000. This source editor does not cap the discount amount at the original selling price.

Money values use the observed base currency. KRW/JPY must be integer strings in multiples of 100; a decimal point is rejected even in "100.0". No rounding, exchange, minor-unit conversion or checkout discount calculation is performed. Source enum member names DISCOUNT_RATE and DISCOUNT_AMOUNT are not valid wire values; use the mode strings above.

For FIXED_PRICE, the default minimum is KRW 300, JPY 300 or USD 5. These source channel exceptions are exact identifiers, not a selectable pricing policy:

Currency Exact channelCode Minimum
USD L2NoYW5uZWxzLzE4NTcx, L2NoYW5uZWxzLzIyMTM, L2NoYW5uZWxzLzE4Nzg0 0.10
KRW L2NoYW5uZWxzLzMyMDA 15000
KRW L2NoYW5uZWxzLzkz, L2NoYW5uZWxzLzExNzQ 5000
KRW L2NoYW5uZWxzLzU4MTQ 20000
KRW L2NoYW5uZWxzLzE3NzI3, L2NoYW5uZWxzLzg4Nw 100
KRW L2NoYW5uZWxzLzIzMDY0, L2NoYW5uZWxzLzE4NTU2, L2NoYW5uZWxzLzU0Mjk, L2NoYW5uZWxzLzIxMTU3, L2NoYW5uZWxzLzI3MTY, L2NoYW5uZWxzLzI5MjM, L2NoYW5uZWxzLzIwODQ5 0

Both preparation and readback bind the selected campaign, benefit, exact page, base currency and operator. Visible rows must match the selected benefit's product type. Existing embedded row projections must agree; missing embedded rows are not invented. Only the selected price field may change. Other price fields, rows, benefits, parent settings and activation flags must remain unchanged; the server's campaign updatedAt may advance. Equivalent amount strings/numbers compare by exact decimal meaning. This is one-page verification, not a complete atomic inventory or proof of customer pricing.

A fresh plan returns sourceWrite, targetProduct and changesRequired. A fresh completed result returns targetProduct and a new productRevision; a matching value returns a guarded no-op without PATCH. The single console link targets the same products tab and page/limit. It does not open an invented product-price dialog; select the row in the console manually.

Examples below are alternative dry-run shapes, not a sequence to apply to one benefit. Replace channel, parents, row ID and revision with the matching authorized read, and choose only the stored product/mode combination. Zero-filled revisions are placeholders.

{"channelCode":"selected-channel-code","pcid":"observed-campaign","pbid":"observed-benefit","requestId":"product-discount-fixed-price","expectedRevision":"0000000000000000000000000000000000000000000000000000000000000000","action":{"kind":"product-discount","targetProductId":123,"page":1,"limit":15,"mode":"FIXED_PRICE","value":"500"},"dryRun":true}
{"channelCode":"selected-channel-code","pcid":"observed-campaign","pbid":"observed-benefit","requestId":"product-discount-rate-discount","expectedRevision":"0000000000000000000000000000000000000000000000000000000000000000","action":{"kind":"product-discount","targetProductId":123,"page":1,"limit":15,"mode":"RATE_DISCOUNT","value":"15.5"},"dryRun":true}
{"channelCode":"selected-channel-code","pcid":"observed-campaign","pbid":"observed-benefit","requestId":"product-discount-amount-discount","expectedRevision":"0000000000000000000000000000000000000000000000000000000000000000","action":{"kind":"product-discount","targetProductId":123,"page":1,"limit":15,"mode":"AMOUNT_DISCOUNT","value":"100"},"dryRun":true}

Benefit-wide pending protection

Profile, activation, product discount, product connect, product disconnect and fresh matching-value or already-absent no-ops share the canonical promotion-benefit task store's pending-benefit-guard child. Within that backend/grant namespace, the resource is environment, connection, channel, pcid and pbid. Product ID, action, request ID and credential version do not create independent resources. Another pending owner rejects a fresh save with PROMOTION_BENEFIT_PENDING_CHANGE; changing row, value, action or request ID does not bypass it. Other benefits remain independent. Malformed guard state returns PROMOTION_PENDING_GUARD_UNVERIFIED.

A durable claim precedes the sent marker and HTTP dispatch. If marker persistence fails before transport, another dispatch is allowed only after the prepared record is durably restored and the claim is released. Lost claim acknowledgement, failed restoration or failed release leaves uncertainty; even a record still marked prepared may require observation only. PROMOTION_GUARD_PERSISTENCE_FAILED must not be treated as proof that nothing happened.

After requested and preserved values match fresh stable reads, completion must be durably stored before releasing only the original task's claim. Failed completion persistence or claim release withholds fresh success and keeps recovery on the original task. Status, dry runs and historical replay cannot clear somebody else's claim. See status and recovery.

Older unregistered sent tasks are not automatically enumerated or migrated; explicit original-task recovery can enroll a known task. There is no automatic expiry or rollback, and no global coordination with other grant namespaces, task types, manual console edits or external callers.

Disconnect an existing product relationship

Use action.kind:"product-disconnect" with an observed connected row's positive safe-integer targetProductId, exact page and limit from promotions_get products. Copy that page's productRevision to expectedRevision and preserve pcid/pbid/channelCode. targetId or a catalog product ID is not the connected-row ID. Page is a positive safe integer; limit is 15, 30, 50 or 100. Their defaults remain 1/15, but pass the observed selection explicitly. No mode, value, replacement product or confirmation-token field is accepted for this action.

This removes the selected benefit-to-product relationship, never the underlying product. Inspect dryRun:true before an intended removal; it creates no task or business write. Eligible new connections use the bounded product-connect action below. It does not automatically restore a removed relationship or its original identity; restorationSupported remains false for disconnect.

Existing benefit type Supported connected product types
DISCOUNT or ORDER_DISCOUNT SubscriptionProduct, ChannelActionProduct, PAppUnlockProduct, CommerceProduct, CommerceProductCategory, TicketEvent.
GRANT CouponTemplate only.
LIMITED_TIME SubscriptionProduct, ChannelActionProduct, PAppUnlockProduct only.

Other combinations fail with PROMOTION_DISCONNECT_UNVERIFIED. The source disconnect control has no activation prerequisite and does not prohibit disconnecting a link whose underlying product is marked deleted. This differs from discount editing, which rejects a deleted product. No activation, new product connection, price change or financial action is composed with removal.

Complete absence and preserved relationships

The writer scans source pages at limit 100, at most 1000 connected rows, and requires two complete stable observations. It checks totals, page sizes, unique identities, product types, embedded relationships and the caller's exact selected page against the complete ordered inventory. A row found on another page is not silently selected. If the original page is already out of range before dispatch, explicitly reread a valid page and reselect; PROMOTION_PAGE_OUT_OF_RANGE does not fetch a replacement automatically.

The caller's productRevision still binds the selected page, parents, currency and operator. The additional full inventory is stored in the internal durable plan and rechecked before dispatch; it does not replace the public revision with a different hash. Prior task snapshots without this optional inventory keep their revision behavior. A larger or drifting inventory prevents a new removal; if discovered while reconciling a sent operation, it leaves that original task unresolved.

A real removal sends one selected-channel POST to /seller/api/v1/promotion-campaigns/{pcid}/benefits/{pbid}/target-products/{targetProductId}/disconnect with {}. It uses the same selected bearer/channel header and source 200/data transport envelope. Neither acknowledgement nor absence from one page proves completion.

Fresh completion requires the row to be absent from the complete stable inventory and selected-benefit embedded projections. Every remaining relationship, order, price, target, activation flag, other benefit and campaign field is preserved; only generated campaign updatedAt may advance. Page contents/totals must match the remaining inventory. Empty inventory is confirmed only after these complete checks. These are bounded repeated observations, not an atomic snapshot, proof of causality or customer benefit verification.

An already-absent ID can produce noop:true only with a fresh valid products revision, complete stable absence proof and the same pending admission as a changed request. It sends no POST and does not prove historical existence or that a removal happened now.

Result and surviving-page navigation

Both plans and results distinguish relationshipAbsent, productDeletionPerformed:false and restorationSupported:false. A plan keeps verification.confirmed:false, even when its current observation finds absence. Fresh completion returns observedProductPage, its current productRevision, and productDisconnect with the original targetProductId, observed page/limit and requestedPage. relationshipAbsent in stored historical state is not a fresh absence check.

If removing the last row collapses the requested page, reconciliation explicitly reads the surviving page 1 with the original limit. The returned single console address, nextStep and navigationExplanation identify that freshly observed products tab. It does not substitute another target into the operation or create a row/dialog URL. Otherwise the observed original page is retained. Follow the returned address; do not reconstruct a now-invalid page link.

The existing benefit-wide guard covers this action and absent no-ops. Keep the original taskId/input and taskType:"promotion-benefit"; recovery observes sent work without repeating POST. Completion must persist before releasing only its own claim. Pre-upgrade pending rollout HOLD, other-namespace limits and actual PostgreSQL/MCP/original-console/UI/LLM/hosted UNVERIFIED boundaries remain. The separate eligible product-connect action is not automatic restoration or proof that previous effects were undone.

Replace every illustrative identity and the zero-filled revision below with the authorized selection. This example validates input shape only; it is not permission to remove a live relationship.

{"channelCode":"selected-channel-code","pcid":"observed-campaign","pbid":"observed-benefit","requestId":"product-disconnect-example","expectedRevision":"0000000000000000000000000000000000000000000000000000000000000000","action":{"kind":"product-disconnect","targetProductId":123,"page":2,"limit":15},"dryRun":true}

Connect existing products to a discount benefit

First read promotions_get product-options for the selected existing DISCOUNT benefit. Copy connectionRevision into expectedRevision; neither productRevision nor benefitRevision substitutes for it. The campaign must be active. Benefit and product activation flags are preserved, with no extra activation prerequisite. This action creates relationships to existing products, not the products themselves or a new benefit.

Action field Contract
kind product-connect.
page, limit Exact selector page and size returned by product-options. Page is a positive safe integer; limit is 15, 30, 50 or 100. Schema defaults are 1/15; retain the observed selection explicitly.
search Optional string up to 500 characters, without trimming. Preserve the exact request, including omission versus an empty string. This is selector search, not an added-products filter.
mode Existing FIXED_PRICE, RATE_DISCOUNT or AMOUNT_DISCOUNT; must match the observed benefit.
targetProducts 1–100 unique {targetId,value} entries selected from that exact options page. Copy each choice's opaque distinctId to targetId, never its numeric id, a title or an existing relationship row ID.
targetProducts[].value Explicit plain decimal string, at most 40 characters, with the same exact-decimal and source price rules as the discount editor. No default price, sign, exponent, whitespace or guessed conversion.

Only these five families are connected, and only to an existing DISCOUNT benefit:

Product type Stored mode
SubscriptionProduct FIXED_PRICE; the source selector is subscription-or-once membership pricing.
ChannelActionProduct FIXED_PRICE.
PAppUnlockProduct FIXED_PRICE.
TicketEvent RATE_DISCOUNT or AMOUNT_DISCOUNT.
CommerceProduct RATE_DISCOUNT or AMOUNT_DISCOUNT.

FIXED_PRICE needs a verified original price in the channel base currency and cannot exceed it. CP prices require an unverified Channel Point conversion and cannot be used for fixed-price connection. Rate values are percentages, not ratio fractions; non-fixed ticket/commerce amounts use base currency and are not capped by an inferred original selling price. No extra activation condition is borrowed from price editing or disconnection: adding requires an active campaign, while those existing-row actions keep their own rules.

The 100-choice and resulting 1000-relationship limits are bridge verification bounds, not asserted source business limits. Choices must be from one exact selector page; use a fresh options request for another page or search. An existing relationship, including one whose underlying product is deleted, is rejected rather than duplicated or silently repriced. Duplicate, absent, stale, foreign-channel or model-mismatched choices also fail closed. There is no already-linked success/no-op for this action.

Connection save and reconciliation

The writer observes the selector, complete existing inventory, parents and currency twice and rechecks the original plan before dispatch. It composes one selected-channel POST with {targetProducts:[{type,targetId,...priceFields}]}. Each price entry contains only discountedPrice, discountRatio or discountPrice for the stored mode. The untyped success body does not supply authoritative created-row identities; no row is invented from a title or numeric guess.

Completion requires exactly one newly observed unique relationship for each requested targetId/type with the exact requested price field. Unselected discount fields must be absent/null. Every old relationship, its price/content and relative order, sibling benefits and parent fields must remain unchanged, apart from campaign updatedAt. Supplied candidate price and activation/public fields must agree with the new row when present. Partial connections, duplicate targets, unrelated changes, malformed pagination or changed selector observations after dispatch leave the task pending. Repeated observations are not atomic or proof against all external edits.

Use the existing taskType promotion-benefit and original requestId/input. The whole-benefit claim blocks fresh IDs, other actions and their matching-value no-ops while this outcome is unresolved. Resume only observes sent work and never repeats the connection POST. Completion must persist before own claim release. Pre-upgrade pending rollout HOLD and the documented backend/grant limitations remain.

A fresh plan exposes sourceWrite and observedProductPage without saving. Confirmed results include requestedRelationships, the newly observed connectedRelationships, observedProductPage and productConnect.requestedTargets, plus productCreationPerformed:false and restorationSupported:false. The single link opens the observed products page 1 at limit 100; navigationExplanation warns that added rows may be on later pages. It is not a per-row/modal URL. The result does not return a fresh connectionRevision or productRevision for another edit: read the appropriate options/products view again. Historical completion is not a fresh verification or navigation claim.

No automatic restoration is performed. A separately intended disconnect can remove a supported observed link, but it is not automatic rollback, restoration of old relationship IDs or evidence that restoration was tested.

Product connection gaps

Commerce category selection exposes Category, while the promotion relationship DTO uses CommerceProductCategory and the modal copies the former model. This unresolved mapping is not normalized; category connection is unavailable. Existing category relationship discount editing/disconnection retain their separately supported contracts.

FIXED_PRICE CP conversion remains unavailable. Deleted-product reconnection is not inferred from the source UI's filtered maintained IDs; resolve any existing relationship explicitly, and deleted candidates remain ineligible. This item does not add ORDER_DISCOUNT/GRANT/LIMITED_TIME product connections, underlying product creation, or additional benefit creation. Use promotion_benefit_create separately for a new DISCOUNT benefit and optional initial products; its Business-or-higher gate permits inactive campaigns.

The example is a dry-run shape for a matching RATE_DISCOUNT ticket/commerce benefit. Replace every identity and revision with the exact product-options result, taking targetId from choice.distinctId and preserving page/limit/search.

{"channelCode":"selected-channel-code","pcid":"observed-campaign","pbid":"observed-benefit","requestId":"product-connect-example","expectedRevision":"0000000000000000000000000000000000000000000000000000000000000000","action":{"kind":"product-connect","page":1,"limit":15,"search":"summer","mode":"RATE_DISCOUNT","targetProducts":[{"targetId":"observed-selector-distinct-id","value":"15.5"}]},"dryRun":true}

Inputs

Field Required here Contract
pcid Yes string; pattern ^[A-Za-z0-9_-]{1,128}$
pbid Yes string; pattern ^[A-Za-z0-9_-]{1,128}$
action Yes one of: profile, activation, product-discount, product-disconnect, product-connect (by kind)
action.alias No When action.kind is profile: string; min length 4; max length 20
action.schedule No When action.kind is profile: object
action.schedule.startAt Yes When action.kind is profile: string; pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$; format date-time
action.schedule.endAt Yes When action.kind is profile: string; pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$; format date-time or null
action.target No When action.kind is profile: one of: ALL, SEGMENT (by kind)
action.target.distinctId Yes When action.kind is profile and action.target.kind is SEGMENT: string; pattern ^[A-Za-z0-9_-]{1,128}$
action.memberDisplayText No When action.kind is profile: string; min length 1; max length 14
action.discountAmount No When action.kind is profile: string; pattern ^(0|[1-9]\d{0,7})(\.\d{1,2})?$
action.benefitLimitOption No When action.kind is profile: ONE_TIME, UNLIMITED
action.executionTrigger No When action.kind is profile: EXECUTION_SIGNUP, EXECUTION_SIGNIN, EXECUTION_PURCHASE
action.noticeStartAt No When action.kind is profile: string; pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$; format date-time or null
action.autoToggleOnBenefitPeriod No When action.kind is profile: boolean
action.active Yes When action.kind is activation: boolean
action.targetProductId Yes When action.kind is product-discount: integer; greater than 0; The connected row id returned by promotions_get products, not its targetId or a catalog product ID.
action.mode No When action.kind is product-discount: FIXED_PRICE, RATE_DISCOUNT, AMOUNT_DISCOUNT
action.value Yes When action.kind is product-discount: string; max length 40; pattern ^(0|[1-9]\d*)(\.\d+)?$; Plain base-currency amount or percentage, never a ratio fraction. No rounding, exchange or minor-unit conversion.
action.targetProductId Yes When action.kind is product-disconnect: integer; greater than 0; Observed connected row id, not the underlying targetId. Only the benefit relationship is removed.
action.mode No When action.kind is product-connect: FIXED_PRICE, RATE_DISCOUNT, AMOUNT_DISCOUNT
action.targetProducts Yes When action.kind is product-connect: array; min items 1; max items 100
action.targetProducts[].targetId Yes When action.kind is product-connect: string; pattern ^[A-Za-z0-9_-]{1,128}$
action.targetProducts[].value No When action.kind is product-connect: string; max length 40; pattern ^(0|[1-9]\d*)(\.\d+)?$; Discount value for a DISCOUNT benefit (plain decimal amount or percentage). Omit it for coupon-grant (GRANT) and limited-time (LIMITED_TIME) benefits, which connect without a value, as in the console.
confirmToken No string; pattern ^ct[a-f\d]{32}$; The confirmToken from this tool's preview result. Omit it to preview; repeat the same input with it to execute.