Journey Docs
Partner API ExplorerLoyaltyBenefits

Create a benefit

Required scopepartner:loyalty:benefits:write
POST
/v1/partner/loyalty/programs/{programExternalId}/benefits

Adds a benefit definition to the program. It is created unpublished and reaches no member until it is published.

Authorization

headerAuthorizationBearer <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.

Path Parameters

programExternalId*string

External id of the loyalty program that owns the benefit.

Request Body

application/json
  1. body
cadence?string
Value in"one_time""annual""per_stay""every_n_stays"
cadenceN?number

Required when cadence is every_n_stays; rejected otherwise.

description?string
Lengthlength <= 2000
fulfillmentStrategy?|

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.

Value in"requireApproval""propertyInternal"
kind*string
Value in"benefit""award"
memberTitle?string

What 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.

Length1 <= length <= 200
memberVisible?boolean

Whether 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*string
Length1 <= length <= 200
organizationId?string

External org id — global/superadmin callers only.

rateCode?string

Negotiated 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.

Length1 <= length <= 64
rateCodeParam?string

The 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.

Length1 <= length <= 64
systemType?string

Which 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.

Value in"earnMultiplier""memberRate"
valueAmount?string

Numeric string (matches the AlloyDB numeric column).

valueUnit?string
Value in"multiplier""count""points""usd""percent"

Response Body

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"}'
Empty