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.
| Method | Route | Parameters | Response |
|---|---|---|---|
| GET | /loyalty/me | page (ledger entries, 25 per page) | { account, ledger{items[], page, total}, rewards[] } |
| POST | /loyalty/me/redeem | reward_id (default points), points, idempotency (required) | { coupon_code, points, balance } |
| POST | /loyalty/me/return | coupon_code | Points return ledger entry. |
| PUT | /loyalty/me/birthday | birthday (YYYY-MM-DD) | { saved: true } |
| POST | /loyalty/me/verify | — | { sent: true }: sends a verification link by email. |
redeemcreates a single-use coupon (prefixnw-) 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
| Method | Route | Use |
|---|---|---|
| GET | /loyalty/accounts | Search 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}/adjust | Manual adjustment: points (positive or negative), note (required, up to 255 characters) and idempotency. |
| POST | /loyalty/accounts/{id}/redeem | In-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}/return | Return the points of a coupon: coupon_code. |
| POST | /loyalty/accounts/{id}/verify | Without code: sends a 6-digit code to the customer. With code: verifies the account. |
| PUT | /loyalty/accounts/{id}/status | status (active or blocked) and note (required). |
| POST | /loyalty/accounts/merge | Merge accounts: source_id, target_id, note. |
| GET | /loyalty/stats | from and to (YYYY-MM-DD, up to 366 days). By default, from the start of the month. |
| GET, PUT | /settings/loyalty | Program settings. |
| POST | /loyalty/import | Import CSV (see below). |
| GET | /loyalty/export | Export CSV in pages of 100: { csv, done, page }. |
| POST | /loyalty/preview-rules | Simulates how many points an event would award: settings, context, event. |
| POST | /loyalty/apply-expiry | Schedules 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.
| Column | Aliases |
|---|---|
email | user_email, customer_email |
points | points_balance, balance |
name | first_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:
| Group | Content |
|---|---|
enabled, program_name | Activation and name. |
identity | How the customer is identified: email, account or phone. |
earn | When points are earned (completed or finalized), amount per point, rounding and whether delivery is included. |
redeem | Point value, minimum points, maximum percentage of the order and coupon validity. |
expiry | Expiry 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. |
referral | Referrals. |
notify | Channel 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
| Code | HTTP | Cause |
|---|---|---|
loyalty_invalid | 400 | Insufficient points or below the minimum, maximum percentage exceeded, reward unavailable, blocked or nonexistent account, wrong code, invalid idempotency key, CSV with errors… |
loyalty_settings | 400 | Invalid settings. |
loyalty_unavailable | 409 | Program disabled, simultaneous operation on the same account, or WhatsApp not configured. |