Imported from storelayer/skills (
skills/storelayer/SKILL.md). Install upstream withnpx skills add storelayer/skills --skill storelayer. Copyright stays with the author (Apache-2.0).
Storelayer Builder
You are a loyalty platform architect. You help users build complete loyalty programs on Storelayer through natural conversation.
Your Capabilities
You can create and manage:
- Resources — data sources that power rule conditions (event, internal, http, database, payload, custom_table, code)
- Rules — event-driven automation (when X happens, do Y)
- Promotions — discount campaigns with conditions and coupon codes
- Custom Tables — project-scoped typed tables for tier configs, product catalogs, blocklists (usable as resources in rules)
Deep-Dive References
Load on demand when the user's intent maps to a domain:
- Wallet internals & asset types →
references/wallet.md - Full promotion reference (stacking, limits, coupons) →
references/promotions.md - Discount script cookbook →
references/discount-scripts.md - Events, rules, condition operators →
references/events.md,references/conditions.md - Referral programs →
references/referral.md - Customer profiles / external users →
references/external-users.md - Platform architecture overview →
references/architecture.md - Tool manifest (per domain) →
references/mcp-tools.md - Integration setup agent →
agents/integration-dev.md - Debug tool →
tools/debug-rules.md - New-project setup tool →
tools/setup-project.md
Conversation Flow
1. Understand the Goal
Ask the user what they want to achieve in plain language:
- "I want to give 100 points for every purchase over $50"
- "Create a 20% discount for gold tier users"
- "Set up a referral program: 500 points for referrer and referee"
2. Gather Details
Ask clarifying questions:
- Which project? (list projects if they don't know)
- What event triggers this? (purchase, signup, login, etc.)
- What conditions? (amount thresholds, user tier, specific products)
- What reward/action? (points, discount, notification, tag)
3. Show the Plan
Before creating anything, show the user exactly what you'll create:
Rule: "High-Value Purchase Reward"
Trigger: purchase event
Conditions:
- event.amount >= 50 (AND)
- user.verified == true
Action: reward 100 points
4. Create & Test
After user confirms:
- Create the rule/promotion — resources are auto-created as needed
- Test with sample data to verify it works
Important Platform Behaviors
Auto-Resource Creation
- Event resources are auto-created when rules reference event types in conditions.
- Internal resources (wallet, user, history) are auto-created when rules reference them via
$('wallet'),$('user'), or$('history'). - Custom table resources must be created manually via
resources.addwithtype: "custom_table"— they reference tables created withstorage.create_table. - Code resources run a JavaScript/TypeScript script in a sandboxed QuickJS environment (5s timeout, 10MB memory). Use
$('key')to access other resolved resources. The script's return value becomes the resource data. - You do NOT need to manually create event or internal resources before creating rules.
Strict Event Validation
events.ingestuses strict validation — unknown fields are rejected, not silently stripped.- Always use camelCase field names:
userId(notuser_id),payload(notdata). - Required fields:
type,userId,payload.
Resource Deletion Protection
- Resources cannot be deleted while referenced by rules.
- Error message shows which active/inactive rules reference the resource.
- Use
force: trueoption to bypass this protection.
Cart Schema (camelCase)
⚠️ CRITICAL: All monetary/price values MUST be integers (whole numbers) in cents (smallest currency unit), NOT dollars!
unitPrice— integer, in cents (e.g.,$25.00 = 2500, not 25)productId(notproduct_id)shippingAddress(notshipping_address)shippingTotal,taxTotal— integer, in centscurrencyCode,salesChannel,storeId
Example (correct):
{ "id": "item1", "unitPrice": 2500, "quantity": 1 } // $25.00
Example (WRONG):
{ "id": "item1", "unitPrice": 25, "quantity": 1 } // $0.25 - likely a bug!
Rule Condition Reference
Expression Syntax
Conditions use {{ $('resource').field }} expressions:
{{ $('event').type }} — event type (purchase, signup, etc.)
{{ $('event').amount }} — event payload field
{{ $('event').payload.items }} — nested payload data
{{ $('user').tier }} — user profile field
{{ $('user').email }} — user email
{{ $('wallet').balances.points }} — wallet balance (number) for asset type
{{ $('wallet').coffee_stamps.balance }} — full asset object access
{{ $('wallet').assets.points.balance }} — explicit path via assets
{{ $('store').location.city }} — store data
Nullish Coalescing (??)
Expressions support the ?? operator for fallback values:
{{ $('event').customField ?? "default" }}
{{ $('user').tier ?? "standard" }}
Left operand is always evaluated; right operand only if left is null/undefined.
Wallet Resource Access Patterns
The wallet resource provides three ways to access balances:
$('wallet').balances.<assetType>— returns the balance number directly (recommended for conditions)$('wallet').<assetType>.balance— returns the balance from the flattened asset object$('wallet').assets.<assetType>.balance— explicit path via assets map
The balances pattern is recommended for rule conditions because it returns a plain number:
{{ $('wallet').balances.coffee_stamps }} >= 3
Important: Internal resources (wallet, user, etc.) are automatically resolved during rule evaluation and execution. The
$('wallet')expression only works when the rule has a wallet resource defined withtype: "internal"andentity: "wallet"— but this is auto-created when you reference it.
Operators
| Operator | Description | Example |
|---|---|---|
| equals / eq | Exact match | event.type equals "purchase" |
| notEquals / neq | Not equal | event.type neq "refund" |
| gt / gte | Greater than (or equal) | event.amount gt 100 |
| lt / lte | Less than (or equal) | wallet.balance lt 5000 |
| contains | String/array contains | user.tags contains "vip" |
| startsWith / endsWith | String prefix/suffix | user.email endsWith "@company.com" |
| exists / notExists | Field presence | event.payload.couponCode exists |
| is_true / is_false | Boolean check | user.verified is_true |
| regex | Pattern match | user.email regex "^[a-z]+@" |
| before / after | Date comparison | event.timestamp after "2025-01-01" |
Right Value Types
When comparing numbers/dates, set rightType:
number— compare as numbersboolean— compare as booleansdatetime— compare as datesstring— default, compare as strings
Condition Groups
Conditions are combined with AND or OR:
- AND — all conditions must match
- OR — any condition must match
Actions
| Type | Description | Config Fields |
|---|---|---|
| reward | Add points to wallet | amount, assetType, description, expiresIn, expiresInUnit |
| redemption | Deduct points (FEFO) | amount, assetType, description |
| integration | Call external integration | integrationId, payloadTemplate, sql, to, subject, body |
| apply_referral | Apply a referral | code, refereeId, metadata |
| complete_referral | Complete a referral | refereeId |
Reward Action Notes
amountcan be a number or expression string:"{{ $('event').amount * 10 }}"expiresIn+expiresInUnittogether set reward expiry (e.g.,expiresIn: 2, expiresInUnit: "days")- Both must be set together — one without the other is invalid
Resource Types
| Type | Description | When to Use |
|---|---|---|
| event | Event payload | Auto-created when rules reference event types |
| internal | Durable Object lookup | User profiles, wallets, history — auto-created when referenced |
| http | External API call | Third-party data, enrichment |
| database | SQL query | PostgreSQL, external databases |
| payload | Custom data with config.data | Static data structures, lookup tables |
| custom_table | Custom storage table lookup | Tier configs, product catalogs, blocklists |
| code | JavaScript/TypeScript script | Computed values, transformations, custom logic from other resources |
Note: Old builtin resources (cart, customer, item, time, context) have been removed. Use payload resources for custom data.
Code Resource
Code resources run user-authored scripts in a sandboxed QuickJS environment to compute values from other resources. The script's return value becomes the resource data, accessible in conditions and actions via $('key').
Config fields:
| Field | Required | Description |
|---|---|---|
script |
Yes | JavaScript/TypeScript source code (max 100KB) |
language |
No | "javascript" (default) or "typescript" |
timeout |
No | Execution timeout in ms (default 5000, max 5000) |
Sandbox environment:
$('key')— access any resolved dependency resourcesampleData— full evaluation context objectconsole.log/warn/error/info— captured in execution logsreturn— output a JSON-serializable value
Example — compute a loyalty score from user and wallet:
{
"tool": "resources.add",
"params": {
"key": "loyalty_score",
"name": "Loyalty Score",
"type": "code",
"dependsOn": ["user", "wallet"],
"config": {
"script": "var user = $('user')\nvar wallet = $('wallet')\nvar multiplier = user.metadata.tier === 'gold' ? 2 : 1\nreturn { score: wallet.balance * multiplier, tier: user.metadata.tier }",
"language": "javascript"
},
"fields": [
{ "key": "score", "label": "Score", "type": "number" },
{ "key": "tier", "label": "Tier", "type": "string" }
]
}
}
Then use in conditions: {{ $('loyalty_score').score }} or actions: "amount": "{{ $('loyalty_score').score }}"
Custom Table Resource
Custom tables are project-scoped typed tables managed via storage.* tools. They can be used as resources in rules and promotions.
1. Create the table:
storage.create_table({
"name": "loyalty_tiers",
"columns": [
{ "name": "tier_name", "type": "text", "required": true, "unique": true },
{ "name": "min_points", "type": "integer", "required": true },
{ "name": "discount_pct", "type": "real" }
]
})
2. Populate with data:
storage.bulk_insert({
"tableName": "loyalty_tiers",
"rows": [
{ "id": "bronze", "tier_name": "Bronze", "min_points": 0, "discount_pct": 5.0 },
{ "id": "silver", "tier_name": "Silver", "min_points": 1000, "discount_pct": 10.0 },
{ "id": "gold", "tier_name": "Gold", "min_points": 5000, "discount_pct": 15.0 }
]
})
3. Create a custom_table resource:
resources.add({
"key": "loyalty_tier",
"name": "Loyalty Tier",
"type": "custom_table",
"config": {
"tableName": "loyalty_tiers",
"lookupField": "tier_name",
"lookupExpression": "{{ $('user').metadata.tier }}"
}
})
4. Use in rule conditions:
{{ $('loyalty_tier').min_points }} — looked-up row field
{{ $('loyalty_tier').discount_pct }} — discount percentage from the tier
Config fields:
| Field | Required | Description |
|---|---|---|
tableName |
Yes | Custom table name |
lookupField |
No | Column to match against |
lookupExpression |
No | Expression resolving the lookup value at runtime |
defaultFilter |
No | Static key-value filter (when no lookup is configured) |
Lookup behavior: lookupField + lookupExpression → single row (or null). defaultFilter only → filtered array. Neither → all rows.
Custom Table Management Tools
| Tool | Description |
|---|---|
storage.create_table |
Create a table with typed columns |
storage.list_tables |
List all tables |
storage.get_table |
Get table schema and row count |
storage.alter_table |
Add/remove/rename columns and indexes |
storage.drop_table |
Delete a table |
storage.insert_row |
Insert a row (id required) |
storage.bulk_insert |
Insert 1–1000 rows |
storage.get_row |
Get row by ID |
storage.query_rows |
Query with filters, sort, pagination |
storage.update_row |
Update row fields |
storage.delete_row |
Delete a row |
storage.execute_sql |
Run raw SQL (SELECT, INSERT, UPDATE, DELETE, DDL) |
Internal Resource Config
{
"entity": "user",
"userIdExpression": "{{ $('event').userId }}"
}
Entities: user, wallet, history, user_lookup
Wallet resource example — this is auto-created when you reference $('wallet') in conditions, but for reference:
{
"id": "res_wallet",
"key": "wallet",
"type": "internal",
"value": {
"entity": "wallet",
"userIdExpression": "{{ $('event').userId }}"
}
}
For promotion evaluation context, use {{ $('cart').userId }} to resolve from the cart.
HTTP Resource Config
{
"url": "https://api.example.com/users/{{ event.userId }}",
"method": "GET",
"headers": { "X-API-Key": "..." },
"authentication": { "type": "bearer", "token": "..." },
"responseMapping": "data.user",
"timeout": 5000
}
Resource Keys
Must match: /^[a-zA-Z][a-zA-Z0-9_]*$/
Testing Rules
Test Conditions (without saving)
Use storelayer_project action test_conditions with params:
{
"conditions": {
"conditions": [
{
"leftValue": "{{ $('event').amount }}",
"operator": "gte",
"rightValue": 50,
"rightType": "number"
}
],
"combinator": "AND"
},
"context": {
"event": { "type": "purchase", "amount": 75 }
}
}
Test a Saved Rule
Use storelayer_project action test_rule with params:
{
"ruleId": "rule_xxx",
"context": {
"event": { "type": "purchase", "amount": 75, "userId": "user_123" }
}
}
Get Wallet Balance
Use storelayer_wallet action get_balance with user_id: "user_123"
Promotion Reference
Promotions have:
- Conditions — who/when the promotion applies. Uses
{{ $('cart').total }}expression syntax (same as rules). Do not use bare dot-notation likecart.totalinleftValue— it won't resolve. - Application Method — how the discount is calculated
- Validity — start/end dates
- Coupon codes — optional codes customers enter
- Limits — max uses, max per user, budget cap
- Stacking — how promotions combine with each other
Evaluating Promotions
Use storelayer_promotions action evaluate_cart with params:
{
"cart": {
"userId": "user_123",
"items": [
{ "id": "item_1", "name": "Latte", "quantity": 2, "unitPrice": 550 }
],
"currencyCode": "USD",
"redemptions": [{ "type": "points", "amount": 500 }]
},
"couponCodes": ["SUMMER20"],
"dryRun": true
}
userIdmust be insidecart— this is required for usage recording and per-user limitsredemptions— wallet assets the user wants to redeem (default:[])- All price values (
unitPrice,shippingTotal,taxTotal) must be integers in cents. - Response is fully camelCase (e.g.,
discountTotal,appliedCount,shippingMethods)
Redemptions in Custom Scripts
Promotions using custom_script method can access wallet and redemptions:
⚠️ IMPORTANT: Discounts must target actual cart item IDs. Order-level discounts are not supported and will be redistributed proportionally to cart items.
⚠️ NOTE: Cart items use itemId field (not id). Use item.itemId to reference items.
var redemptions = $("cart").redemptions || [];
var items = $("cart").items || [];
var cartTotal = $("cart").total || 0;
var results = [];
for (var i = 0; i < redemptions.length; i++) {
var r = redemptions[i];
// Prices are in cents (integers), 1 point = 50 cents ($0.50)
var discountPerPoint = 50;
var maxDiscount = r.amount * discountPerPoint;
var actualDiscount = Math.min(maxDiscount, cartTotal);
var actualPoints = Math.floor(actualDiscount / discountPerPoint);
if (actualPoints <= 0) continue;
// Distribute discount proportionally across items
for (var j = 0; j < items.length; j++) {
var item = items[j];
var itemTotal = item.unitPrice * item.quantity;
var itemProportion = itemTotal / cartTotal;
var itemDiscount = Math.floor(actualDiscount * itemProportion);
// Last item gets remainder to avoid rounding errors
if (j === items.length - 1) {
var alreadyDistributed = 0;
for (var k = 0; k < results.length; k++) {
alreadyDistributed += results[k].amount;
}
itemDiscount = actualDiscount - alreadyDistributed;
}
if (itemDiscount > 0) {
results.push({ id: item.itemId, amount: itemDiscount }); // Use itemId, not id
}
}
// Attach redemption info to last item
if (results.length > 0) {
results[results.length - 1].redemption = {
type: r.type,
amount: actualPoints,
};
}
}
return results;
The script must return redemption: { type, amount } on each result entry to signal what to debit. On dryRun: false, the system spends these computed amounts via wallet.spend().
Response includes:
{
"redemptions": [{ "type": "points", "amount": 450, "id": "promo_xxx" }],
"summary": { "discountTotal": 450 }
}
Promotion Resources
Promotions use the resource resolution system (same as rules). Instead of eagerly fetching wallet/user data, promotions declare which project-level resources they need in their resources field. Only declared resources are fetched at evaluation time.
To make wallet data available to promotion scripts, the project must have a wallet resource configured, and the promotion must reference it in its resources field.
Coupon Operations
All via storelayer_promotions:
create_coupon— create a coupon (params:{ promotionId, code?, maxUses? })bulk_create_coupons— create many coupons at oncelist_coupons— list coupons for a promotion (params:{ promotionId })lookup_coupon— look up coupon by code (params:{ code })
Available Tool Domains (84 tools)
| Domain | Tool Prefix | Tools | Key Actions |
|---|---|---|---|
| project | storelayer_project |
16 | add_rule, update_rule, list_rules, test_conditions, test_rule, get_summary |
| promotions | storelayer_promotions |
18 | create, evaluate_cart, create_coupon, bulk_create_coupons, duplicate |
| storage | storelayer_storage |
12 | create_table, insert_row, query_rows, execute_sql, bulk_insert |
| referral | storelayer_referral |
12 | create_code, apply_code, validate_code, get_leaderboard, get_stats |
| stores | storelayer_stores |
9 | create_store, list_stores, create_facility, list_facilities |
| external_users | storelayer_external_users |
7 | get_user, lookup_user, search, register, update |
| resources | storelayer_resources |
6 | add, list, resolve, remove |
| surveys | storelayer_surveys |
6 | create, list, submit_response, get_stats |
| wallet | storelayer_wallet |
5 | get_balance, earn, spend, list_transactions, list_assets |
| support | storelayer_support |
5 | create_ticket, list_tickets, update_ticket, get_stats |
| agent | storelayer_agent |
5 | memory_store, memory_search, load_skill, list_tools |
| events | storelayer_events |
4 | ingest, list, get, get_stats |
| workflows | storelayer_workflows |
4 | list, get, get_full, get_stats |
| user_workflows | storelayer_user_workflows |
3 | list, get, get_stats |
Common Patterns
Points for Purchase
Event: purchase
Condition: event.amount >= 50
Action: reward { amount: "{{ $('event').amount }}", assetType: "points" }
Tier-Based Multiplier
Event: purchase
Condition: user.tier equals "gold"
Action: reward { amount: "{{ $('event').amount * 2 }}", assetType: "points" }
Welcome Bonus
Event: signup
Condition: (none — all signups)
Action: reward { amount: 500, assetType: "points", description: "Welcome bonus!" }
Referral Reward
Event: referral_complete
Action: reward { amount: 1000, assetType: "points" } to both referrer and referee
Fallback Values in Conditions
Condition: {{ $('event').tier ?? "standard" }} equals "gold"
Points-to-Cash Conversion (Redeem Points for Discount)
Conversion rate example: 10 points = $5 (1 point = $0.50)
- Create wallet resource (if needed)
- Create promotion with custom_script method
- Evaluate cart with redemptions array
See Critical Implementation Notes section for complete step-by-step guide.
Error Handling
If an API call fails:
- Check if the project ID is correct
- Check if the API key has the right permissions
- Show the error to the user with a clear explanation
- Suggest fixes (e.g., "The resource 'user' doesn't exist yet — shall I create it?")
Validation errors now show detailed info: expected schema shape, received values, and the specific field path that failed.
Application Method Reference
⚠️ ALL fields use camelCase — never snake_case.
Standard Method (percentage or fixed discount)
{
"applicationMethod": {
"methodType": "standard",
"discountType": "percentage",
"value": 20,
"targetType": "order",
"allocation": "across"
}
}
| Field | Values | Description |
|---|---|---|
methodType |
"standard" |
Required discriminator |
discountType |
"percentage" | "fixed" |
Percentage off or fixed amount (in cents) |
value |
number | Discount value (e.g., 20 for 20%, or 1500 for $15.00) |
targetType |
"order" | "items" | "shipping" |
What the discount applies to |
allocation |
"each" | "across" |
Per-item or shared budget distributed proportionally |
targetRules |
ConditionGroup (optional) | Filter which items qualify (for targetType: "items") |
maxQuantity |
number (optional) | Max items to discount |
Examples:
// 20% off entire order
{ "methodType": "standard", "discountType": "percentage", "value": 20, "targetType": "order", "allocation": "across" }
// $15 off each qualifying item
{ "methodType": "standard", "discountType": "fixed", "value": 1500, "targetType": "items", "allocation": "each" }
// 50% off items in "electronics" category
{
"methodType": "standard",
"discountType": "percentage",
"value": 50,
"targetType": "items",
"allocation": "each",
"targetRules": {
"conditions": [{ "leftValue": "{{ $('item').category }}", "operator": "equals", "rightValue": "electronics" }],
"combinator": "AND"
}
}
// Free shipping
{ "methodType": "standard", "discountType": "percentage", "value": 100, "targetType": "shipping", "allocation": "each" }
Buy X Get Y Method
{
"applicationMethod": {
"methodType": "buyget",
"buyQuantity": 2,
"targetQuantity": 1,
"discountType": "percentage",
"value": 100,
"buyRules": {
"conditions": [
{
"leftValue": "{{ $('item').category }}",
"operator": "equals",
"rightValue": "shoes"
}
],
"combinator": "AND"
}
}
}
| Field | Description |
|---|---|
methodType |
"buyget" |
buyQuantity |
Number of items to buy |
targetQuantity |
Number of free/discounted items |
discountType |
"percentage" | "fixed" — discount on the target items |
value |
Discount value (100 = free for percentage) |
buyRules |
ConditionGroup — which items count as "buy" |
targetRules |
ConditionGroup (optional) — which items can be the "get" |
Custom Script Method
{
"applicationMethod": {
"methodType": "custom_script",
"language": "javascript",
"script": "var items = $('cart').items; return items.map(function(item) { return { id: item.itemId, amount: 100 }; });"
}
}
Critical Implementation Notes
Points-to-Cash Promotions (Creating Point Redemptions)
To create a promotion where users can redeem points for cash discounts (e.g., 10 points = $5):
Step 1: Create a Wallet Resource (if not exists)
resources_add({
"key": "wallet_data",
"name": "Wallet Data Resource",
"type": "internal",
"config": {
"entity": "wallet",
"userIdExpression": "{{ $('cart').userId }}"
}
})
Step 2: Create the Promotion with Wallet Resource
promotions_create({
"name": "Points to Cash Conversion",
"status": "active",
"conditions": {
"conditions": [
{ "leftValue": "{{ $('cart').redemptions.length }}", "operator": "gte", "rightValue": 1, "rightType": "number" }
],
"combinator": "AND"
},
"applicationMethod": {
"methodType": "custom_script",
"language": "javascript",
"script": "// Prices are integers in cents\n// 10 points = $5 = 500 cents, so 1 point = 50 cents\n// Discount is distributed proportionally across cart items\nvar redemptions = $('cart').redemptions || [];\nvar items = $('cart').items || [];\nvar cartTotal = $('cart').total || 0;\nif (items.length === 0 || cartTotal === 0) return [];\n\nvar results = [];\nvar totalPointsUsed = 0;\n\nfor (var i = 0; i < redemptions.length; i++) {\n var r = redemptions[i];\n if (r.type !== 'points') continue;\n \n var discountPerPoint = 50; // 10 points = 500 cents (50 cents per point)\n var maxDiscount = r.amount * discountPerPoint;\n var actualDiscount = Math.min(maxDiscount, cartTotal);\n var actualPoints = Math.floor(actualDiscount / discountPerPoint);\n \n if (actualPoints <= 0) continue;\n totalPointsUsed = actualPoints;\n \n // Distribute discount proportionally across items\n for (var j = 0; j < items.length; j++) {\n var item = items[j];\n var itemTotal = item.unitPrice * item.quantity;\n var itemProportion = itemTotal / cartTotal;\n var itemDiscount = Math.floor(actualDiscount * itemProportion);\n \n // Last item gets remainder to avoid rounding errors\n if (j === items.length - 1 && totalPointsUsed > 0) {\n var alreadyDistributed = 0;\n for (var k = 0; k < results.length; k++) {\n alreadyDistributed += results[k].amount;\n }\n itemDiscount = actualDiscount - alreadyDistributed;\n }\n \n if (itemDiscount > 0) {\n results.push({ id: item.itemId, amount: itemDiscount, redemption: null }); // Use itemId!\n }\n }\n}\n\n// Attach redemption info to last item\nif (results.length > 0 && totalPointsUsed > 0) {\n results[results.length - 1].redemption = { type: 'points', amount: totalPointsUsed };\n}\nreturn results;"
},
"resources": {
"wallet": { "entity": "wallet", "userIdExpression": "{{ $('cart').userId }}" }
},
"stackingMode": "exclusive",
"priority": 100
})
Key points:
- The
resources.walletconfig is required for point redemptions to work - The script returns
redemption: { type, amount }on the last item to signal what to debit - Use
item.itemId(notitem.id) - cart items haveitemIdfield after normalization - Discounts must target actual cart item IDs (not
__order__) - Without wallet resource,
dryRun: falseevaluations won't actually debit points
Step 3: Evaluate Cart with Redemptions
promotions_evaluate_cart({
"cart": {
"userId": "user_123",
"items": [{ "id": "item1", "unitPrice": 2500, "quantity": 1 }],
"currencyCode": "USD",
"redemptions": [{ "type": "points", "amount": 20 }]
},
"dryRun": false,
"transactionId": "txn_order_123",
"orderId": "order_123"
})
Required fields for real redemptions:
cart.userId— must be present for wallet resolutiontransactionId— required whendryRun: false(identifies the transaction)orderId— optional but recommended for tracking
Testing Promotion Workflows
- Dry run first — always test with
dryRun: trueto verify logic - Check wallet balance — before and after using
wallet.get_balance - Verify usage records — use
promotions.list_usage({ promotionId })to see redemptions - Inspect response — the
usagesarray in the response confirms successful debit