Reward definition guide reward-definition-guide

Table of contents

Get started with Loyalty Challenges

When a challenge task, milestone, or challenge completes and has a reward value configured, the platform issues a reward by calling your reward provider’s HTTP endpoint with a JSON payload. A Reward Definition describes what reward to issue and provides a JSONata expression — rewardJsonata — that shapes the exact payload your provider expects.

This guide covers how to configure a reward provider, create reward definitions, write the rewardJsonata expression, and understand what context is available to it at evaluation time.

Two-level model

Rewards are organized in two levels:

Reward Provider  (endpoint, auth, headers)
└── Reward Definition  (denomination, rewardJsonata)
└── Reward Definition
└── ...

A Reward Provider represents a single external rewards system — it holds the delivery endpoint URL, authentication, and any custom HTTP headers. One provider can hold multiple Reward Definitions, each describing a distinct reward type or denomination offered by that provider (e.g. “50 Stars”, “Double Stars”, “Free Item”).

A challenge references the provider and definition by GUID. When a reward is issued, the platform evaluates the definition’s rewardJsonata expression and POSTs the result to the provider’s endpoint.

Reward provider & definition fields

Reward provider fields
table 0-row-4 1-row-4 2-row-4 3-row-4 4-row-4 5-row-4 6-row-4 7-row-4 8-row-4 html-authored
Field Type Required Description
guid String No (system-assigned) Unique identifier. Read-only.
name String Yes Display name, unique within the org.
desc String No Human-readable description of the provider.
enabled Boolean No When false, reward delivery is
suspended for all definitions under this provider.
url String Yes HTTP endpoint that receives the reward payload.
The platform POSTs the evaluated
rewardJsonata output to this URL.
additionalHeaders Object No Custom HTTP headers to include in every
delivery request (e.g. API keys,
content-type overrides).
maxRatePerSecond Integer No Optional per-provider rate limit (1–5000).
Null means unlimited.
enableMTLS Boolean No Whether the endpoint requires mutual TLS.
Reward definition fields
table 0-row-4 1-row-4 2-row-4 3-row-4 4-row-4 5-row-4 6-row-4 7-row-4 html-authored
Field Type Required Description
guid String No (system-assigned) Unique identifier. Read-only.
name String Yes Display name, unique within the provider.
denomination String No The unit of the reward, used in display
and available in expressions as
reward.denomination
(e.g. "Stars", "Points", "Miles").
desc String No Description of the reward, available
in expressions as reward.desc.
enabled Boolean No When false, this definition is inactive
and will not issue rewards.
isDefault Boolean No Marks this as the sandbox-wide default
reward definition. Only one definition
across all providers may be default at a time;
setting a new default clears the previous one.
Used to auto-populate reward details on
personalized challenges at publish time.
rewardJsonata String Yes JSONata expression evaluated at
reward-issue time. Receives the full
reward context and must return the JSON
payload to POST to the provider.

The reward context

When rewardJsonata is evaluated, it receives a single root object containing everything known about the reward event. All paths in your expression are relative to this root.

{
  "rewardContext": {
    "rewardValue": "50",
    "source":      "challenge"
  },
  "reward": {
    "name":         "500 Stars",
    "desc":         "Issue 500 Stars to the member",
    "denomination": "Stars",
    "enabled":      true
  },
  "task": { ... },
  "milestone": { ... },
  "challenge": { ... },
  "timestamp": "2026-02-10T00:29:22.538+00:00"
}
Context fields
table 0-row-2 1-row-2 2-row-2 3-row-2 4-row-2 5-row-2 6-row-2 7-row-2 8-row-2 9-row-2 10-row-2 11-row-2 12-row-2 13-row-2 14-row-2 15-row-2 16-row-2
Field Description
rewardContext.rewardValue The reward value string configured on the challenge, task, or milestone that triggered this issuance.
rewardContext.source What triggered the reward: "task", "challenge", or "milestone".
reward The RewardDefinition itself — name, desc, denomination.
task The completing task, including its accumulators, schedule, and reward.
task.accumulators.spend Total qualifying spend accumulated by the task.
task.accumulators.qty Total qualifying item count accumulated by the task.
task.accumulators.item_list All qualifying items applied to the task. Each entry has item, transactionId, timestamp, utcOffset, locationId.
task.accumulators.item_list[-1] The most recent item applied (JSONata negative index). Useful for sourcing the last transaction ID or timestamp.
task.schedule.currentStreak Current consecutive-visit streak count (for streak challenges).
task.schedule.currentVisits Total visit count (for visit challenges).
milestone The milestone that triggered this reward, or null if not a milestone reward. Includes count and reward.rewardValue.
challenge.profileId The member’s loyalty ID.
challenge.kvpCustom Custom key-value pairs configured on the challenge. A common pattern for passing campaign IDs, product names, or provider-specific metadata.
challenge.name Challenge name.
challenge._id Challenge ID.
timestamp ISO 8601 timestamp of the reward issuance.

Writing the rewardJsonata expression

The expression receives the reward context as its input and must return a JSON object — the payload POSTed to the provider’s endpoint. The shape of that object is entirely up to the provider’s API; you map context fields onto whatever structure the provider expects.

Simple fixed payload

The simplest case: the provider needs a point count and a member ID, both known from the context.

code language-jsonata
{
  "memberId":   challenge.profileId,
  "points":     $number(rewardContext.rewardValue),
  "currency":   reward.denomination
}

Output:

code language-json
{
  "memberId": "ADB-0000030",
  "points":   50,
  "currency": "Stars"
}

rewardContext.rewardValue is always a string. Use $number() to convert it if your provider expects a numeric value.

Using kvpCustom for provider-specific metadata

Providers often require fields like campaign IDs or source system codes that are specific to each challenge run. Store these in challenge.kvpCustom when authoring the challenge, then reference them in the expression — keeping the expression reusable across campaigns.

code language-jsonata
{
  "memberId":         challenge.profileId,
  "points":           $number(rewardContext.rewardValue),
  "campaignId":       challenge.kvpCustom.campaignId,
  "transactionSource": "AJO"
}

You can also use reward.kvpCustom for constants that are fixed for a given reward type rather than per-challenge.

Using task accumulator data

Task accumulators hold a record of every qualifying event. Use item_list[-1] to access the most recently applied item — its transactionId and timestamp are useful for audit trails and deduplication on the provider side.

code language-jsonata
{
  "memberId":       challenge.profileId,
  "points":         $number(rewardContext.rewardValue),
  "transactionId":  task.accumulators.item_list[-1].transactionId,
  "transactionDate": task.accumulators.item_list[-1].timestamp
}
Constructing a text message

For notification-based providers (Slack, SMS, email), you can build a message string directly using JSONata’s & concatenation operator:

code language-jsonata
{
  "text": "You just earned " & rewardContext.rewardValue & " " & reward.denomination & "!"
}

Output:

code language-json
{
  "text": "You just earned 50 Stars!"
}

Examples

Example 1 — Simple points provider

Scenario: A basic loyalty points API expects a member ID and a point amount.

Reward Definition:

code language-json
{
  "name":         "Standard Points",
  "denomination": "Points",
  "desc":         "Award loyalty points",
  "enabled":      true,
  "rewardJsonata": "{\"memberId\": challenge.profileId, \"pointQuantity\": $number(rewardContext.rewardValue), \"denomination\": reward.denomination}"
}

Formatted expression:

code language-jsonata
{
  "memberId":      challenge.profileId,
  "pointQuantity": $number(rewardContext.rewardValue),
  "denomination":  reward.denomination
}

Payload POSTed to provider:

code language-json
{
  "memberId":      "ADB-0000030",
  "pointQuantity": 50,
  "denomination":  "Points"
}
Example 2 — Provider payload with campaign metadata

Scenario: The provider requires a structured award record that includes audit fields, campaign references, and member description. Campaign-specific values are stored in challenge.kvpCustom so the same reward definition works across campaigns without editing the expression.

Challenge kvpCustom (set when authoring the challenge):

code language-json
{
  "parentCampaignId": "CAMP-2026-Q1",
  "productName":      "Loyalty Program"
}

Reward Definition:

code language-json
{
  "name":         "Stars — Campaign Award",
  "denomination": "Stars",
  "desc":         "Issue Stars for completing a qualifying purchase",
  "enabled":      true,
  "rewardJsonata": "{\"awardPoints\":[{\"idType\":\"externalId\",\"id\":challenge.profileId,\"transactionId\":task.accumulators.item_list[-1].transactionId,\"transactionDate\":task.accumulators.item_list[-1].timestamp,\"originalTransactionId\":task.accumulators.item_list[-1].transactionId,\"transactionSource\":\"AJO\",\"channelSource\":\"Web\",\"parentCampaignId\":challenge.kvpCustom.parentCampaignId,\"productName\":challenge.kvpCustom.productName,\"memberAwardDescription\":reward.desc,\"pointQuantity\":$number(rewardContext.rewardValue)}]}"
}

Formatted expression:

code language-jsonata
{
  "awardPoints": [
    {
      "idType":                "externalId",
      "id":                    challenge.profileId,
      "transactionId":         task.accumulators.item_list[-1].transactionId,
      "transactionDate":       task.accumulators.item_list[-1].timestamp,
      "originalTransactionId": task.accumulators.item_list[-1].transactionId,
      "transactionSource":     "AJO",
      "channelSource":         "Web",
      "parentCampaignId":      challenge.kvpCustom.parentCampaignId,
      "productName":           challenge.kvpCustom.productName,
      "memberAwardDescription": reward.desc,
      "pointQuantity":         $number(rewardContext.rewardValue)
    }
  ]
}

Payload POSTed to provider:

code language-json
{
  "awardPoints": [
    {
      "idType":                "externalId",
      "id":                    "ADB-0000030",
      "transactionId":         "b4fa0e89-f4bb-41ce-b370-fb97f9c52f1a",
      "transactionDate":       "2026-02-08T00:12:00.000+00:00",
      "originalTransactionId": "b4fa0e89-f4bb-41ce-b370-fb97f9c52f1a",
      "transactionSource":     "AJO",
      "channelSource":         "Web",
      "parentCampaignId":      "CAMP-2026-Q1",
      "productName":           "Loyalty Program",
      "memberAwardDescription": "Issue Stars for completing a qualifying purchase",
      "pointQuantity":         50
    }
  ]
}
Example 3 — Milestone reward

Scenario: A streak challenge issues a milestone reward every N visits. The expression includes the milestone count and the current streak for provider-side context.

Formatted expression:

code language-jsonata
{
  "memberId":       challenge.profileId,
  "points":         $number(rewardContext.rewardValue),
  "milestoneCount": milestone.count,
  "currentStreak":  task.schedule.currentStreak,
  "denomination":   reward.denomination,
  "source":         rewardContext.source
}

Payload POSTed to provider (at 2nd visit milestone):

code language-json
{
  "memberId":       "ADB-0000030",
  "points":         20,
  "milestoneCount": 2,
  "currentStreak":  2,
  "denomination":   "Stars",
  "source":         "milestone"
}

When rewardContext.source is "milestone", the milestone object is populated with count and reward.rewardValue. When the source is "task" or "challenge", milestone is null.

API reference

Reward providers
code language-http
POST   /loyalty/metadata/config/rewards/providers
GET    /loyalty/metadata/config/rewards/providers
GET    /loyalty/metadata/config/rewards/providers/{providerId}
PUT    /loyalty/metadata/config/rewards/providers/{providerId}
DELETE /loyalty/metadata/config/rewards/providers/{providerId}

All requests require x-gw-ims-org-id and x-sandbox-name headers.

Create a provider:

code language-http
POST /loyalty/metadata/config/rewards/providers
x-gw-ims-org-id: {ORG_ID}
x-sandbox-name: {SANDBOX}
Content-Type: application/json

{
  "name":    "My Points Provider",
  "desc":    "Issues loyalty points via REST",
  "enabled": true,
  "url":     "https://rewards.example.com/award",
  "additionalHeaders": {
    "x-api-key": "YOUR_API_KEY"
  }
}
Reward definitions
code language-http
POST   /loyalty/metadata/config/rewards/definitions/{providerId}
GET    /loyalty/metadata/config/rewards/definitions/{providerId}
GET    /loyalty/metadata/config/rewards/definitions/{providerId}/{rewardId}
PUT    /loyalty/metadata/config/rewards/definitions/{providerId}/{rewardId}
DELETE /loyalty/metadata/config/rewards/definitions/{providerId}/{rewardId}

Create a reward definition:

code language-http
POST /loyalty/metadata/config/rewards/definitions/{providerId}
x-gw-ims-org-id: {ORG_ID}
x-sandbox-name: {SANDBOX}
Content-Type: application/json

{
  "name":         "50 Stars",
  "denomination": "Stars",
  "desc":         "Award 50 Stars on task completion",
  "enabled":      true,
  "rewardJsonata": "{ \"memberId\": challenge.profileId, \"points\": $number(rewardContext.rewardValue) }"
}

Expression validation

rewardJsonata expressions are validated for syntax at publish time. If the expression is invalid, the API returns a 422 error with a description of the parse failure.

To develop and test an expression before publishing, use the JSONata Exerciser. Paste the reward context JSON as the input document and your expression to verify the output matches what your provider expects. A representative reward context for each trigger type (task, milestone, challenge) is shown in the examples above.

Common mistakes

Mistake
Effect
Fix
rewardContext.rewardValue used as a number without conversion
Type mismatch if provider validates the field as numeric
Wrap with $number(rewardContext.rewardValue)
challenge.kvpCustom.someKey returns null
Key not set on the challenge at authoring time
Ensure the key is present in kvpCustom on every challenge that uses this definition
task.accumulators.item_list[-1] is null
No items were applied before reward issued (non-purchase event)
Guard with a conditional or use timestamp from context instead
milestone accessed when source is "task" or "challenge"
milestone is null; expression throws or produces null fields
Check rewardContext.source before accessing milestone, or only use milestone in definitions attached to milestone rewards
Expression returns an array instead of an object
Provider receives unexpected payload structure
Wrap array-returning expressions in an outer object: { "items": [...] }
recommendation-more-help
journey-optimizer-help