Skip to main content

Loyalty

Available since 1.25.0. Responses carry Cache-Control: private, no-store. If the program is disabled or the store is in coexistence mode, the routes respond 409 loyalty_unavailable. See the loyalty guide.

Customer routes (signed in)​

The customer must have a points account with a verified email. Otherwise, the routes respond 400 loyalty_invalid.

MethodRouteParametersResponse
GET/loyalty/mepage (ledger entries, 25 per page){ account, ledger{items[], page, total}, rewards[] }
POST/loyalty/me/redeemreward_id (default points), points, idempotency (required){ coupon_code, points, balance }
POST/loyalty/me/returncoupon_codePoints return ledger entry.
PUT/loyalty/me/birthdaybirthday (YYYY-MM-DD){ saved: true }
POST/loyalty/me/verify—{ sent: true }: sends a verification link by email.
  • redeem creates a single-use coupon (prefix nw-) restricted to the customer's email. It does not apply it to the cart: the store does that at checkout.
  • The birthday cannot be in the future and can only be changed once a year.

Public route​

GET /loyalty/preview returns what the store shows in the cart: { enabled, program_name, preview, balance, verified, logged_in, min_points, rewards[], account_url, login_url, cart_message, ... }. preview is the points the current cart would earn. If the program is off, it responds { enabled: false }.

Management routes​

MethodRouteUse
GET/loyalty/accountsSearch accounts: search (email, phone or name), level, status (active or blocked), linked (1 or 0), page, per_page (25 by default, maximum 100). Responds { items, total, page, per_page } inside data.
GET/loyalty/accounts/{id}Details, ledger and rewards.
POST/loyalty/accounts/{id}/adjustManual adjustment: points (positive or negative), note (required, up to 255 characters) and idempotency.
POST/loyalty/accounts/{id}/redeemIn-store redemption: order_id (open order with the same email), reward_id, points and idempotency. Applies the coupon to the order and recalculates the total.
POST/loyalty/accounts/{id}/returnReturn the points of a coupon: coupon_code.
POST/loyalty/accounts/{id}/verifyWithout code: sends a 6-digit code to the customer. With code: verifies the account.
PUT/loyalty/accounts/{id}/statusstatus (active or blocked) and note (required).
POST/loyalty/accounts/mergeMerge accounts: source_id, target_id, note.
GET/loyalty/statsfrom and to (YYYY-MM-DD, up to 366 days). By default, from the start of the month.
GET, PUT/settings/loyaltyProgram settings.
POST/loyalty/importImport CSV (see below).
GET/loyalty/exportExport CSV in pages of 100: { csv, done, page }.
POST/loyalty/preview-rulesSimulates how many points an event would award: settings, context, event.
POST/loyalty/apply-expirySchedules points expiry in the background.

In-store redemption​

curl -X POST https://tienda.example.test/wp-json/notiwoo/v1/loyalty/accounts/58/redeem \
-H "Authorization: Bearer <token>" \
-H "X-Notiwoo-Actor: notifier:15" \
-H "Content-Type: application/json" \
-d '{"order_id": 1240, "reward_id": "points", "points": 500, "idempotency": "caja-1240-0001"}'
{ "success": true, "data": { "coupon_code": "nw-k3j9x0q2m5v8b1n4c7z6l0p9w2e5", "points": 500, "balance": 1320, "order_total": "41000.00" } }

The X-Notiwoo-Actor header is optional and records who made the redemption in the history.

Verification by code​

The 6-digit code is valid for 15 minutes and allows 5 attempts. Only one can be sent per minute per account; if you request another one sooner, the response includes retry_after: 60.

Import CSV​

POST /loyalty/import with csv (text up to 2 MB and 5000 rows) and dry_run.

ColumnAliases
emailuser_email, customer_email
pointspoints_balance, balance
namefirst_name + last_name
phone, referral_code—

By default dry_run is true: it validates and responds { valid, count, points, errors[], preview[] } without importing anything. Send dry_run: false to import.

Objects​

Account: id, email, phone (international format), name, user_id, email_verified_at, referral_code, level_id, birthday, balance, lifetime_earned, status, whatsapp_consent, created_at, updated_at.

Ledger entry: id, type (earn, redeem, adjust, expire, restore or revert), points, remaining, expires_at, source, campaign_id, order_id, reward_id, coupon_code, note, actor, created_at.

Program settings​

GET /settings/loyalty responds { settings, log[], whatsapp_ready, currency }. log is the history of settings changes.

PUT /settings/loyalty takes the full settings object. If you change the point value or the identification mode, you must confirm it with confirm_point_value: true or confirm_identity_risk: true.

Main settings groups:

GroupContent
enabled, program_nameActivation and name.
identityHow the customer is identified: email, account or phone.
earnWhen points are earned (completed or finalized), amount per point, rounding and whether delivery is included.
redeemPoint value, minimum points, maximum percentage of the order and coupon validity.
expiryExpiry and advance notice.
level_settings, levels[]Levels.
campaigns[]Campaigns: purchase, first_order, signup, birthday, review, referral, multiplier and level_up.
rewards[]Rewards: points_discount, fixed_discount, percent_discount, free_delivery and free_product.
referralReferrals.
notifyChannel for each notice: none, email, whatsapp or both.

If validation fails, it responds 400 loyalty_settings with the paths of the fields to review.

Errors​

CodeHTTPCause
loyalty_invalid400Insufficient points or below the minimum, maximum percentage exceeded, reward unavailable, blocked or nonexistent account, wrong code, invalid idempotency key, CSV with errors…
loyalty_settings400Invalid settings.
loyalty_unavailable409Program disabled, simultaneous operation on the same account, or WhatsApp not configured.