Promo codes and seller promotions
Buyers enter promo codes on a cart. A code is either a seller promotion (a selling profile’s own offer, on its own listings only) or a United Pet Club promotion. United Pet Club works out every amount: a client never sends a discount amount, and one that tries is refused.
All amounts named *InBase or amount are decimal amounts in the cart’s (the seller’s) currency; fields ending in
Minor are minor units of the currencyCode beside them. Rates are basis points (200 = 2 %).
Add a code to a cart
Section titled “Add a code to a cart”POST /api/cart/v1/carts/{cartId}/promo-codesAuthorization: Bearer <member token>Content-Type: application/json
{ "code": "SPRING-GROOM" }{ "data": { "cart": { "id": "…", "discounts": [ … ], "summary": { … } }, "rejected": [ { "code": "WELCOME5", "reasonCode": "REPLACED", "message": "Another code from the same seller replaced this one." } ] }}(Other cart fields omitted.)
- The answer is the whole cart after United Pet Club re-priced all of its codes, plus the codes that did not apply.
A refused code is not an error: the call succeeds with the code in
rejected. - Codes are trimmed and compared without regard to case.
- Remove a code with
DELETE /api/cart/v1/carts/{cartId}/promo-codes/{code}; same answer shape. - Changing the cart’s lines re-prices its codes too; a code that no longer fits drops off.
503with the retryable platform response codeCart_Shopping_GT_OfferCheck_FLmeans the codes could not be checked right now. The cart is unchanged; retry.403Cart_Shopping_VL_Authorization_RJ: the caller is not the cart’s owner acting as that profile.POST /api/cart/v1/carts/{cartId}/discountsno longer accepts a promotion, a promotion id or a coupon code from a member (403); use the promo-code routes.
Reading the applied discounts
Section titled “Reading the applied discounts”Each applied code becomes one discount per cart line it reaches. The cart detail’s discounts[] entries for a promo
code carry:
| Field | Meaning |
|---|---|
couponCodeSnapshot | the code as the buyer entered it — group the entries by it to show each code once |
cartLineId | the line it lowers |
amountInBase | how much it takes off that line |
promotionIdLogical, promoRedemptionIdLogical | the promotion, and this cart’s use of it |
fundedBy | SELLER (a seller promotion: label it with the seller’s name) or UPC (a United Pet Club promotion) |
fundedByProfileIdLogical | the profile that funds it |
Show each code once with its total saving and a way to remove it. Taxes are calculated on the discounted lines.
How codes combine
Section titled “How codes combine”- At most one code per seller and one United Pet Club code per cart, five codes in all. A second code of the same
seller replaces the first (
REPLACED). - Seller promotions apply first, each to that seller’s own lines; the United Pet Club promotion then applies to what is left. A line never goes below zero.
- A promotion that does not combine refuses to share a line with another code (
NOT_STACKABLE). - No promotion applies to donations or gift cards; a seller promotion never applies to another seller’s items or to United Pet Club plans, add-ons or fees.
- A code counts as used when the order is paid, not when it is applied.
Refusal reasons
Section titled “Refusal reasons”reasonCode | Meaning |
|---|---|
CODE_INVALID | no such code, or it is not the caller’s (reward codes work only for the profile they were issued to) |
CODE_EXPIRED | the code has expired |
LIMIT_REACHED | the code or promotion was used as many times as it allows, or the cart already holds five codes |
NOT_ACTIVE | the promotion is not running (paused, ended, outside its dates) |
NOT_ELIGIBLE | the buyer does not meet the promotion’s conditions |
RISK_BLOCKED | refused by fraud checks |
NO_ELIGIBLE_ITEMS | nothing in the cart can take it — for a seller promotion: none of that seller’s own items |
MIN_CART_NOT_MET | the eligible items do not reach the promotion’s minimum |
CURRENCY_MISMATCH | an amount-off code in another currency than the cart |
NOT_STACKABLE | it cannot share its items with a code already applied |
REPLACED | another code from the same seller replaced it |
NOT_STARTED | the promotion is scheduled and has not started yet; message names the start date |
ENDED | the promotion’s end date has passed |
OUT_OF_REGION | the promotion is limited to a region, and the order is not going there |
REGION_UNKNOWN | the promotion is limited to a region, and neither the order nor the buyer’s profile has an address yet |
Show message to the buyer, or your own text per reasonCode.
Where an order goes. A regional promotion checks where the purchase happens, never the buyer’s IP address: for shipped goods the cart’s shipping address, for a booking the place the service is performed, otherwise the address on the buyer’s profile. Set the shipping address before checkout and a regional code is checked against it.
Reward codes
Section titled “Reward codes”GET /api/promotion/v1/me/reward-codes lists the referral reward codes issued to the acting profile:
{ "data": [ { "codeId": "…", "code": "R-7KQ2M9XA", "promoId": "…", "promoTitle": "Refer a friend", "offerSummaryText": "…", "ownerKindCode": "THIRD_PARTY", "ownerProfileId": "…", "rewardSourceCode": "REFERRAL", "sourceRoleCode": "REFERRER", "isUsed": false, "expiresAtUtc": null, "issuedAtUtc": "2026-10-15T18:02:11Z" } ]}A reward code works once, only for that profile. ownerKindCode THIRD_PARTY means it is a seller’s reward and
applies to that seller’s own items only; FIRST_PARTY means it is United Pet Club’s.
Seller promotions
Section titled “Seller promotions”A profile runs its own promotions when it is an approved commerce seller, a verified organization or a verified shelter
or rescue. Running one is free; each time a buyer uses it on a paid order, the profile pays a per-claim fee
(default 2 %, returned claimFeeRateBps) of the claimed lines — the items the promotion lowered — after its discount
and before tax (no other items, no United Pet Club discount, no tax, levies, deposits or shipping), in the order’s
currency, taken from its earnings like the Commission and returned in proportion on refunds.
All routes are under /api/promotion/v1/seller/ and act for the profile the caller is acting as. Anyone else gets 404
for a promotion that is not theirs.
| Route | Does |
|---|---|
GET eligibility | { allowed, known, basis, claimFeeRateBps }; basis is COMMERCE_SELLER, VERIFIED_ORGANIZATION or VERIFIED_SHELTER_RESCUE; known: false = could not check, retry |
GET promos | the profile’s promotions, newest first |
POST promos | create (201); starts as a draft with one code |
GET promos/{id} | one promotion |
PUT promos/{id} | edit: the draft in place, or a new draft version while the live one keeps running |
POST promos/{id}/publish | the newest draft goes live; existing codes follow it |
POST promos/{id}/pause, POST promos/{id}/resume | stop and restart its codes |
POST promos/{id}/end | end it for good; its codes stop working |
POST promos/{id}/codes | another code: { "code"?, "maxRedemptions"?, "expiresAtUtc"? } |
GET promos/{id}/stats | { promoId, applied, claims, byCurrency: [{ currencyCode, claims, discountMinor, claimedSalesMinor, feeMinor, reversedFeeMinor }] } |
GET claims?promoId= | the paid orders that used the profile’s promotions or referral rewards, with the fee of each |
Create
Section titled “Create”POST /api/promotion/v1/seller/promosAuthorization: Bearer <member token, acting as the selling profile>Content-Type: application/json
{ "title": "Spring grooming", "offerSummaryText": "A percentage off any grooming visit", "discountKindCode": "PERCENT_OFF", "percentValue": 10, "scopeCode": "ALL_MY_LISTINGS", "endsAtUtc": "2026-11-30T07:59:59Z", "maxRedemptionsPerCustomer": 1, "code": "SPRING-GROOM"}| Field | Rules |
|---|---|
title | required, up to 200 characters |
shortDescription, offerSummaryText | optional; up to 500 and 300 characters. offerSummaryText is shown to buyers |
discountKindCode | PERCENT_OFF (percentValue 1–100) or FIXED_OFF (amount > 0 and a 3-letter currencyCode: an amount off, the “store credit” reward) |
scopeCode | ALL_MY_LISTINGS, or ONE_LISTING with listingId and listingKindCode |
listingKindCode | for ONE_LISTING: OFFER (default — a store offer: products, bundles, classes sold as offers), SERVICE (a bookable service or group class) or MEMBERSHIP_TIER (one of the profile’s own organization’s membership tiers: its dues). The listing must be one the profile sells now; United Pet Club’s own tiers are never the profile’s |
duesTermsCount | optional, 1–36, default 1: how many membership dues terms the discount covers, counting the first payment (a member who joins with the code pays the discounted dues for that many terms, automatic renewals included). Above 1 only for MEMBERSHIP_TIER or ALL_MY_LISTINGS |
minCartAmount, maxDiscountAmount | optional; minimum ≥ 0, maximum > 0 |
startsAtUtc, endsAtUtc | optional; the end after the start |
maxTotalRedemptions, maxRedemptionsPerCustomer | optional; at least 1 |
code | optional; 4–32 characters A–Z 0–9 -, starting with a letter or digit, unique across United Pet Club. Generated when empty |
isStackable | optional, default true: may combine with a United Pet Club promotion on the same item |
region | optional; null = everywhere. { "mode": "INCLUDE", "regions": [{ "countryCode": "CA", "subdivisionCodes": ["CA-ON"] }] }: ISO 3166-1 countries, optional ISO 3166-2 provinces or states (an empty list = the whole country). A profile’s promotion is INCLUDE only and must lie inside where the profile sells (GET /api/promotion/v1/seller/sales-area) |
timeZoneIana | optional; the time zone your editor shows the dates in, such as America/Toronto. The dates themselves are always UTC |
localeCode | optional, default en-US |
A seller promotion can only carry this one discount: no other reward kinds, no scopes beyond the profile’s own listings, no audiences or rate limits. A promotion only ever takes money off the owner’s own lines at checkout. Create, update, publish and resume check that a one-listing promotion’s listing is still the profile’s.
Schedule ahead. Publish with a future startsAtUtc and the promotion is SCHEDULED: its codes exist and can be
handed out, but checkout refuses them with NOT_STARTED until the start. It becomes ACTIVE at the start and ENDED
after endsAtUtc on its own. A code never outlives the promotion: publishing sets every later (or open-ended) code
expiry to the promotion’s end, and adding a code that expires later is refused.
GET /api/promotion/v1/seller/sales-areaAuthorization: Bearer <member token, acting as the selling profile>answers the region the profile’s promotions may cover, in the same region shape: where its store offers can be
bought, where its services are performed, and its own address.
Bookings take the provider’s own codes at booking time (the discount lowers the booking’s price); United Pet Club codes apply to store carts only.
Membership dues. An organization’s code applies when a member joins or changes tier, not in a cart: send it as
promoCode on POST /api/membership/v1/applications or POST /api/membership/v1/memberships/{id}/change-requests
(TIER_CHANGE or RENEW). The application / change request then quotes the discounted dues (feeAmountSnapshot /
quotedAmount) with promoCode, promoDiscountAmount (per term) and promoDuesTermsCount. A code that can’t be used
answers 400 (Membership_Enrollment_VL_Request_RJ); 503 (Membership_Enrollment_HD_Promotion_FL) means it could not
be checked, retry. Nothing is filed either way. Each discounted term the member pays is a claim with its own fee.
The promotion object
Section titled “The promotion object”{ id, ownerProfileId, ownerKindCode: "THIRD_PARTY", slug, title, shortDescription, offerSummaryText, statusCode, versionStatusCode, versionId, currentPublishedVersionId, isAdminHeld, discountKindCode, percentValue, amount, currencyCode, scopeCode, listingId, listingKindCode, minCartAmount, maxDiscountAmount, startsAtUtc, endsAtUtc, maxTotalRedemptions, maxRedemptionsPerCustomer, isStackable, codes: [{ id, code, uses, maxRedemptions, expiresAtUtc, isRevoked }], claimsCount, createdAtUtc, updatedAtUtc, region, timeZoneIana }statusCode is DRAFT, SCHEDULED (published, not started), ACTIVE, PAUSED or ENDED (past its end, or
ended).
versionId is the newest version (a draft while an edit waits to be published); currentPublishedVersionId is the one
buyers use now. isAdminHeld: true means United Pet Club paused it: the profile cannot resume or publish it until
United Pet Club resumes it.
A claim
Section titled “A claim”{ id, promoId, promoTitle, orderId, ownerProfileId, rewardSourceCode: "PROMOTION" | "REFERRAL", sourceReferenceId, currencyCode, claimBaseMinor, discountMinor, feeRateBps, feeMinor, reversedFeeMinor, statusCode: "ASSESSED" | "PARTIALLY_REVERSED" | "REVERSED", assessedAtUtc, claimSourceKindCode: "ORDER" | "MEMBERSHIP_DUES", termNumber }claimBaseMinor is what the fee was calculated on (the claimed lines after the seller’s discount); reversedFeeMinor is what refunds gave back.
For MEMBERSHIP_DUES claims, orderId is the membership application or change request the code was priced on and
termNumber the dues term (1 = the first payment, 2+ = automatic renewals); one claim per discounted term.
Errors
Section titled “Errors”| HTTP | Platform response code | When |
|---|---|---|
400 | Promotion_Redemption_VL_Request_RJ | a field breaks the rules above, or a code would expire after the promotion ends (promotion.code_expiry_after_end) |
403 | Promotion_Redemption_VL_Authorization_RJ | the profile may not run promotions, the caller acts as no profile, the listing is not the profile’s (promotion.listing_not_owned), or the region reaches beyond where the profile sells (promotion.region_outside_sales_area) |
404 | Promotion_Redemption_GT_Resource_FL | not a promotion of the acting profile |
409 | Promotion_Redemption_AD_Duplicate_RJ | the code is taken |
409 | Promotion_Redemption_UP_Concurrency_RJ | eligibility, the listing’s owner or where the profile sells (promotion.sales_area_unknown) could not be checked right now (retryable) |
409 | Promotion_Redemption_UP_Transition_RJ | the action does not fit the promotion’s state (for example resuming one that is not paused), or United Pet Club holds it (promotion.admin_hold) |
Create, publish and resume check the profile’s eligibility again; the body’s error code tells the 403 and 409
causes apart.