Membership product serial keys
This adds an option to the existing reader, not a new tool or key writer. The newer source is a dependency of the documentation branch; match the installed endpoint's discovery before using it.
Select one membership and product
Use an authorized selected channel with OWNER or DELEGATEE plus MANAGE_MONETIZATION. Hosted account targets cannot call this reader. Select membershipMid from the membership's mid, then subscriptionProductId from an observed product's id as a string. A membership numeric ID, profile ID and product ID are not interchangeable. The selected product must be COUPON_FREE_INFINITE, COUPON_FREE_FINITE, COUPON_ONCE_INFINITE or COUPON_ONCE_FINITE.
On the detail view, serialKeys:{} defaults to page 1 / limit 15 and all statuses. Page bounds are 1–1,000,000 and supported limits are 15/30/50/100. Optional status is ISSUED/USED/WITHDRAWN; omit it for all. The bridge fixes sortBy:id/order:DESC. These finite limit options are bridge-supported inputs, not evidence of every UI-library option. There is no search, raw code, redeemer, cursor, sort override or automatic traversal.
A serial-key read requires a canonical positive safe numeric product-ID string. It may accompany a product page; the selected product need not occur in that particular page/filter. If it does, common fields must agree. This example composes membership, one active-product page, selected product and one key page:
{"channelCode":"selected-channel-code","view":{"kind":"detail","membershipMid":"returned-membership-mid","products":{"page":1,"limit":15,"isDeactivated":false},"subscriptionProductId":"123","serialKeys":{"page":1,"limit":15}}}
Omitting serialKeys skips all key-page reads. Invalid runtime ID/action combinations fail before HTTP. Non-coupon products stop after the selected detail is checked, before the key endpoint. Do not substitute another product, relax the selected channel or fall back to marketing discounts.
Source request and repeated observations
The adapter uses selected-channel credentials and channel header for GET /seller/api/v2/channel-management/memberships/{membershipMid}/subscription-products/{subscriptionProductId}/coupons. The query contains page, limit, optional status, sortBy:id and order:DESC. It accepts the active source screen's inner {coupons,pagination} payload. The old repository DTO's subscriptionProductCoupons spelling is not an accepted alias. The outer core response must be HTTP 200 with its own data property and absent/null error; a non-null inner error also rejects the inventory.
The composition reads the membership, optional product page and selected product; then it reads the requested key page twice, rereads that product and rereads the membership. A final channel check and credential checks retain the selected channel/operator. It compares projected page/product/membership data, not discarded unknown fields. Successful sequential observations are not an atomic snapshot or proof that a particular actor caused the values.
All membership/product/key GETs using this helper now count streamed response bytes up to 2 MiB, including ignored properties, and check credentials in finally. The generic reader's bounded JSON/envelope/error behavior applies. Ordinary writes keep their own contract and task identity.
Rows, filters and coverage
A successful MEMBERSHIP result includes the observed membership/product and the following fields:
| Field | Meaning and limits |
|---|---|
| serialKeysRead | true when this requested page was read; false on a detail read that omits the option |
| serialKeys | At most 100 projected rows; every row belongs to the selected numeric product ID and requested status |
| Row id / subscriptionProductId | Positive safe integers; IDs are unique and strictly descending within the page |
| Row code | Exact nonempty string, at most 4,096 characters, unique within the page; not trimmed, normalized or transformed |
| Row insertedAt / updatedAt | Nonempty source strings up to 128 characters; no inferred timezone, chronology or expiration |
| serialKeyPagination | Exact requested page/limit and nonnegative safe total; row count must equal min(limit,max(0,total-(page-1)*limit)) |
| serialKeyFilter | {status: selected value or null, sortBy:"id", order:"DESC"}; null reports an omitted input filter |
| serialKeyCoverage | scope:requested_serial_key_status_filter, returned, total, complete, limited and hasMore |
complete is true only when page is 1 and returned equals total. limited is its inverse; hasMore is page*limit < total. An observed empty first page with total 0 is complete for the requested status filter. A later empty page can have hasMore:false while complete:false. Neither means an all-status or cross-page snapshot. A missing/failed read is never converted to serialKeys:[] or a zero count.
Codes are redeemable business values requested by the authorized user. Keep them within that task; do not republish them in diagnostics. Evidence can retain hashes instead of raw codes. Source text remains untrusted data. usedByProfileDistinctId and unknown fields are stripped; this tool does not reveal the redeemer, produce CSV, redeem a code or infer eligibility from key status.
One exact console destination
Successful key reads select /channels/{channelCode}/monetization/memberships/{membershipMid}/subs-products/{subscriptionProductId}/coupons?page={page}&limit={limit}. The central wrapper accepts only those two query fields with values matching the returned page. consoleUrl is the same destination as navigation.primaryUrl; nested human-action recipes do not add alternative top-level addresses. The source keeps status in its UI state, so the link does not reproduce that filter. Demo supplies no actual destination.
With serialKeys, consoleAction can only be omitted or manage-coupons. The latter keeps observed key data but returns action_required / MEMBERSHIP_CONSOLE_REQUIRED, confirmed:false and prerequisitesObserved:true. Issuance, withdrawal, redemption and CSV remain personal console work. No quantity, code mutation, raw URL, export action or task ID is accepted. After an authorized person acts, use a fresh read; this option has no task-resume workflow.
Failures and recovery
| Outcome | Agent response |
|---|---|
| INVALID_INPUT | Correct the strict option, page/filter or runtime product-ID/action dependency; do not invent a target |
| MEMBERSHIP_COUPONS_UNAVAILABLE | The observed product is not a supported coupon type; do not query another product automatically |
| MEMBERSHIP_RESPONSE_UNVERIFIED | Preserve the failed observation: malformed rows, wrong product/status, duplicate IDs/codes, ordering/page inconsistency or inner error are not empty inventory |
| MEMBERSHIP_CONTEXT_CHANGED / MONETIZATION_CONTEXT_CHANGED | Discard all partial output and reread the authorized current state |
| UPSTREAM_RESPONSE_TOO_LARGE / UPSTREAM_CONTRACT_MISMATCH | The entire GET body or its envelope could not be accepted; ignored fields count toward the byte cap |
| AUTH_REQUIRED / PERMISSION_DENIED / CONNECTION_BUSY / other upstream or credential errors | Resolve the connection/service condition through the supported human flow without changing targets or replaying an uncertain task |
verification.confirmed covers the validated stored observation only when consoleAction is omitted. effectiveVisitorAccessVerified, financialActionCompleted and checkoutVerified remain false; productParentBinding is selected_membership_route, because the product-detail body has no independent parent-membership field.
Later product-list handoffs
The final product equality check is shared with those new handoffs, while key-page comparison still happens first. New product-list queries do not change the coupons route's page/limit-only behavior or restore its status filter.