Create a benefit
partner:loyalty:benefits:writeAdds a benefit definition to the program. It is created unpublished and reaches no member until it is published.
AuthorizationBearer <token>WorkOS access token (JWT) for a user who is a member of the partner organization. The organization the request is scoped to is taken from the token, not from a header or query parameter.
Only a JWT is accepted in this header. A Journey jny_ API key sent as a bearer token is rejected
with 401 Invalid or expired token; send it in X-API-Key instead.
WorkOS organization API keys are not accepted yet.
programExternalId*stringExternal id of the loyalty program that owns the benefit.
application/json- body
cadence?string"one_time""annual""per_stay""every_n_stays"cadenceN?numberRequired when cadence is every_n_stays; rejected otherwise.
description?stringlength <= 2000fulfillmentStrategy?|How the property fulfils this definition. Partner-only, hence it lives
here and not on CreateBenefitBaseRequest — the Journey-admin twin
derives the strategy the same way it always has, and forbidNonWhitelisted
turns an admin sending this into a 400 rather than a silent drop.
Omitted keeps the historical by-kind default (benefit →
propertyInternal, award → requireApproval), so existing HXP callers
are unchanged. Supplied, it must be in the two-slug partner allowlist;
external gets its own message because it is display-only and excluded
from the entitlement read view.
The type includes null because that is what actually arrives: @IsOptional()
short-circuits validation for null as well as undefined, and whitelist
keeps a decorated property, so a client serialising an unset optional as
"fulfillmentStrategy": null reaches the service with null intact. Null is
treated as "no choice made" — identical to omitting the field.
"requireApproval""propertyInternal"kind*string"benefit""award"memberTitle?stringWhat the GUEST sees. Distinct from name, which is the operator's own
label and from now on writes internal_name only.
Partner-only, like fulfillmentStrategy: the Journey-admin twin derives
member_title from name exactly as it always has, and
forbidNonWhitelisted turns an admin sending this into a 400 rather than
a silent drop.
Optional so existing HXP callers keep working, but REQUIRED the moment
memberVisible is true — enforced in BenefitDefinitionService, not here,
because an update can flip visibility without resending the title.
1 <= length <= 200memberVisible?booleanWhether the benefit is published to guests. Partner-only, and defaults
to false (decision L7) — unlike the column, which defaults to true
so Journey-admin and house authoring are untouched.
False is a gate against publishing by accident, NOT a claim that partner benefits should be private: most should be shown. What the default buys is that forgetting to write guest copy yields an unpublished benefit rather than a published internal note.
name*string1 <= length <= 200organizationId?stringExternal org id — global/superadmin callers only.
rateCode?stringNegotiated booking rate code — the PMS/CRS rate PLAN this tier's members are quoted. It selects a rate; it does not discount one, so it is not a promo code.
Definition-level default. Bind the benefit to a tier with
rateCodeOverride to vary it per tier, which is the normal case: one
"Member rate" benefit whose code differs at each rung of the ladder.
1 <= length <= 64rateCodeParam?stringThe query parameter the brand's booking engine expects the rate code in —
ThinkReservations uses couponCode.
Required to BUILD a booking link; omit it and the member is shown the code
to enter themselves. Never guessed: hms_properties_view.booking_url is
not templated (0 of 2,622 populated URLs carry a placeholder), and a wrong
param silently books the public rate.
1 <= length <= 64systemType?stringWhich of the program's two SINGLETON CONFIGS this benefit is, if either.
These are not ordinary benefits: Core's resolvers look them up directly by
system_key, never by scanning a partner's benefits or matching a name.
earnMultiplier — THE tier-level points rate. The TOTAL MEMBER_DIRECT
earn rate for a member holding a tier it is bound to; "6" means 6x, not
6x on top of something. Requires kind: 'benefit' and
valueUnit: 'multiplier' (defaulted when omitted).
memberRate — THE benefit carrying the negotiated booking rate. Pair it
with rateCode / rateCodeParam, and vary the code per tier with the
binding's rateCodeOverride.
At most ONE of each per program — a second is a 409. CREATE-TIME ONLY: moving the marker after members have been promised a rate changes what they were promised. Re-author the definition instead.
One field rather than two booleans so "both at once" is unrepresentable rather than a validation error, and so an authoring UI is one control.
"earnMultiplier""memberRate"valueAmount?stringNumeric string (matches the AlloyDB numeric column).
valueUnit?string"multiplier""count""points""usd""percent"The newly created benefit.
curl -X POST 'https://api-prd.cf.journey.com/v1/partner/loyalty/programs/string/benefits' \ -H 'Content-Type: application/json' \ -d '{ "kind": "benefit", "name": "string"}'