HOCO B2B
SRSAPI reference

Loyalty (V2)

V2 loyalty programme for resellers — points balance and level progress, points history, rewards catalogue with eligibility, redemption and vouchers.

V2 — baseline contract

Loyalty is a V2 evolution (cahier §14.2, D-16). These operations enter the contract with SRV-29 and are built in SRV-30, MRA-20 and WEB-20. Until then catalog.home returns account.loyaltyPoints: null and the « Points » tile stays disabled. Earning rules, levels and expiry are HOCO's policy (D-05); the values below are examples.

The « Fidélité » and « Récompenses » screens (reference screen 7, cahier §4.6, §5.5): points earned on orders, Bronze → Silver → Gold → VIP levels over a period, rewards exchanged for points.

  • Ledger, not a counter. The balance is the sum of LoyaltyTransaction rows — EARN, BONUS, REDEEM, EXPIRE, ADJUST, REVERSAL — never edited (LOY-05, lifecycle).
  • Earning on the order total TTC without delivery fee when the order reaches loyalty.earnOn (DELIVERED or invoice PAID), reversed exactly on cancellation or return (LOY-01, LOY-02, BR-37).
  • Levels are computed on the points earned during the current period against thresholds validated by HOCO (LOY-03); they are independent of the customer type, which alone sets prices in V1.
  • Rewards that are unavailable cannot be redeemed (cahier §5.5); eligibility is computed by the server and returned with each reward (LOY-04).
  • Signed-in resellers only; guests get 401, other users 403 PERMISSION_DENIED. When the programme is switched off (loyalty.enabled = false) every operation answers 403 FEATURE_DISABLED (details.feature = "loyalty").
MethodPathOperationAccessAppsRelease
GET/loyalty/summaryloyalty.summary
Points balance, level, progress to next level (V2)
selfRA · RWV2
GET/loyalty/transactionsloyalty.transactions
Points history (V2)
selfRA · RWV2
GET/loyalty/rewardsloyalty.rewards
Rewards catalogue with eligibility (V2)
selfRA · RWV2
POST/loyalty/rewards/{rewardId}/redeemloyalty.redeem
Redeem points for a reward (V2)
self
Idempotency-Key
RA · RWV2
GET/loyalty/redemptionsloyalty.redemptions
My redemptions and vouchers (V2)
selfRA · RWV2

Every operation can also answer 401 UNAUTHENTICATED, 403 ACCOUNT_SUSPENDED, 426 UPGRADE_REQUIRED, 429 RATE_LIMITED, 500 INTERNAL_ERROR and 503 MAINTENANCE — see the error registry.

Schemas

interface LoyaltySummary {
  points: number;                     // spendable balance
  level: LoyaltyLevel;                // BRONZE | SILVER | GOLD | VIP
  nextLevel: LoyaltyLevel | null;     // null at VIP
  pointsToNextLevel: number | null;
  periodPoints: number;               // EARN + BONUS during the current period (drives the level)
  levelProgress: { currentThreshold: number; nextThreshold: number | null; ratio: BasisPoints };   // periodPoints / nextThreshold
  period: { startsOn: string; endsOn: string };   // YYYY-MM-DD
  levels: { level: LoyaltyLevel; threshold: number; benefits: string[] }[];
  earnRate: { amountTtc: Money; points: number }; // 1 point per 100,00 DA TTC
  expiringSoon: { points: number; expiresOn: string } | null;
}

interface LoyaltyTransactionView {
  id: Uuid;
  type: LoyaltyTransactionType;       // EARN, BONUS, REDEEM, EXPIRE, ADJUST, REVERSAL
  points: number;                     // signed
  balanceAfter: number;
  label: string;                      // « Commande CMD-2027-000412 livrée »
  order: { id: Uuid; number: string } | null;
  redemptionId: Uuid | null;
  expiresOn: string | null;           // EARN and BONUS rows
  occurredAt: Iso;
}

interface RewardView {
  id: Uuid;
  type: RewardType;                   // VOUCHER, FREE_DELIVERY, DISCOUNT, GIFT, EARLY_ACCESS
  name: string;
  description: string;
  imageUrl: string | null;
  costPoints: number;
  minLevel: LoyaltyLevel | null;
  value: { amountTtc: Money | null; percent: BasisPoints | null };   // VOUCHER: amount · DISCOUNT: percent
  available: boolean;                 // in stock and within its dates
  perResellerLimit: number | null;    // per period
  validUntil: Iso | null;
  eligibility: {
    eligible: boolean;
    reasons: ("INSUFFICIENT_POINTS" | "LEVEL_TOO_LOW" | "UNAVAILABLE" | "LIMIT_REACHED")[];
    missingPoints: number;
  };
}

interface RedemptionView {
  id: Uuid;
  reward: { id: Uuid; type: RewardType; name: string; imageUrl: string | null };
  qty: number;
  pointsSpent: number;
  status: "REQUESTED" | "FULFILLED" | "CANCELLED";   // RedemptionStatus — to add to enums.json
  voucher: {
    id: Uuid;
    code: string;                     // "LIV-7K3M-9QX2"
    type: "VOUCHER" | "FREE_DELIVERY" | "DISCOUNT";
    value: { amountTtc: Money | null; percent: BasisPoints | null };
    expiresAt: Iso;
    status: "ACTIVE" | "USED" | "EXPIRED" | "CANCELLED";   // VoucherStatus — to add to enums.json
    usedOnOrder: { id: Uuid; number: string } | null;
  } | null;
  fulfilment: string | null;          // GIFT: « Remis avec votre prochaine commande »
  balanceAfter: number;
  createdAt: Iso;
  fulfilledAt: Iso | null;
}

Operations

GET /loyalty/summary

loyalty.summary · Access session — reseller (self) · Apps RA, RW · Idempotency-Key — · Release V2 · Requirements LOY-03

Purpose. « Mes points de fidélité » and « Mon niveau revendeur »: balance, level, progress bar to the next level.

Request — no parameters.

Response — 200 OK: a LoyaltySummary.

{
  "points": 12450,
  "level": "SILVER",
  "nextLevel": "GOLD",
  "pointsToNextLevel": 2550,
  "periodPoints": 12450,
  "levelProgress": { "currentThreshold": 5000, "nextThreshold": 15000, "ratio": 8300 },
  "period": { "startsOn": "2027-01-01", "endsOn": "2027-12-31" },
  "levels": [
    { "level": "BRONZE", "threshold": 0, "benefits": ["Cumul de points sur chaque commande"] },
    { "level": "SILVER", "threshold": 5000, "benefits": ["Bonus de 5 % sur les points gagnés"] },
    { "level": "GOLD", "threshold": 15000, "benefits": ["Bons d'achat exclusifs", "Bonus de 10 % sur les points gagnés"] },
    { "level": "VIP", "threshold": 40000, "benefits": ["Priorité sur les nouveautés", "Livraison offerte sur toutes les commandes"] }
  ],
  "earnRate": { "amountTtc": 10000, "points": 1 },
  "expiringSoon": { "points": 1200, "expiresOn": "2027-06-30" }
}
FieldNotes
pointsToNextLevel, levelProgress« Plus que 2 550 pts pour atteindre le niveau Gold » and the bar « 12 450 / 15 000 pts » of reference screen 7; ratio = periodPoints / nextThreshold in basis points.
points vs periodPointsRedeeming lowers points, never periodPoints: spending does not make a reseller lose their level.
expiringSoonPoints expiring within 60 days (loyalty.pointsValidityMonths).

Errors — 403 FEATURE_DISABLED.

Rules & side effects — read-only; the level is recomputed nightly and at each EARN; a level change sends loyalty.level_changed (push, inbox).

Used by — RA-38, RW-15; home tile through catalog.home → account.loyaltyPoints.

GET /loyalty/transactions

loyalty.transactions · Access session — reseller (self) · Apps RA, RW · Idempotency-Key — · Release V2 · Requirements LOY-05

Purpose. « Voir l'historique »: every points movement, newest first, traceable to its order or redemption (cahier §12 « Les points sont crédités, débités et historisés correctement »).

Request — type (LoyaltyTransactionType, repeatable), from, to (YYYY-MM-DD), cursor, limit (default 20, max 100).

Response — 200 OK: { items: LoyaltyTransactionView[], nextCursor }.

{
  "items": [
    { "id": "01a57352-3900-7703-b1dc-6eb798606c23", "type": "REDEEM", "points": -3000, "balanceAfter": 12450, "label": "Échange : Livraison offerte", "order": null, "redemptionId": "01a57352-3900-72ce-8e18-68d2386178eb", "expiresOn": null, "occurredAt": "2027-05-12T10:00:00Z" },
    { "id": "01a56905-8100-7d86-9fa1-964817c87083", "type": "EARN", "points": 803, "balanceAfter": 15450, "label": "Commande CMD-2027-000412 livrée", "order": { "id": "01a55f2c-7a00-7b13-8c24-3d4e5f607182", "number": "CMD-2027-000412" }, "redemptionId": null, "expiresOn": "2028-05-31", "occurredAt": "2027-05-10T10:00:00Z" },
    { "id": "01a53886-f400-7bd1-806a-1f3535fae7b0", "type": "EXPIRE", "points": -300, "balanceAfter": 14647, "label": "Points expirés (gagnés en avril 2026)", "order": null, "redemptionId": null, "expiresOn": null, "occurredAt": "2027-05-01T00:00:00Z" },
    { "id": "01a52b39-3100-71c4-9f7a-09d750551b7e", "type": "BONUS", "points": 500, "balanceAfter": 14947, "label": "Bonus de lancement du programme fidélité", "order": null, "redemptionId": null, "expiresOn": "2028-04-30", "occurredAt": "2027-04-28T10:00:00Z" }
  ],
  "nextCursor": "eyJjIjoiMjAyNy0wNC0yOFQxMDowMDowMFoiLCJpIjoiMDFhNTJiMzkifQ"
}

The EARN row is the order of 80 380,00 DA TTC at 1 point per 100,00 DA (rounded down).

Errors — 400 VALIDATION_FAILED (unknown type, from after to), 403 FEATURE_DISABLED.

Rules & side effects — read-only; balanceAfter lets the app show the running balance without recomputing.

Used by — history of RA-38, RW-15.

GET /loyalty/rewards

loyalty.rewards · Access session — reseller (self) · Apps RA, RW · Idempotency-Key — · Release V2 · Requirements LOY-04

Purpose. « Récompenses disponibles »: the rewards catalogue with, for each reward, whether I can redeem it now and why not.

Request — eligibleOnly (boolean, default false), type (RewardType, repeatable), cursor, limit.

Response — 200 OK: { items: RewardView[], nextCursor }, cheapest first.

{
  "items": [
    { "id": "01a49ff6-a208-7dcb-ae4e-a7708a7fe389", "type": "FREE_DELIVERY", "name": "Livraison offerte", "description": "Une livraison offerte sur votre prochaine commande, valable 90 jours.", "imageUrl": "https://cdn.hoco.dz/rewards/livraison-offerte.webp", "costPoints": 3000, "minLevel": null, "value": { "amountTtc": null, "percent": null }, "available": true, "perResellerLimit": 4, "validUntil": null, "eligibility": { "eligible": true, "reasons": [], "missingPoints": 0 } },
    { "id": "01a49ff6-9268-7a6c-ba93-b0c8566aa0b1", "type": "GIFT", "name": "Câble USB-C HOCO", "description": "Un câble HOCO X89 offert, remis avec votre prochaine commande.", "imageUrl": "https://cdn.hoco.dz/rewards/cable-usb-c.webp", "costPoints": 6000, "minLevel": null, "value": { "amountTtc": null, "percent": null }, "available": true, "perResellerLimit": null, "validUntil": null, "eligibility": { "eligible": true, "reasons": [], "missingPoints": 0 } },
    { "id": "01a49ff6-8e80-7770-93fe-b94b355feff6", "type": "GIFT", "name": "Chargeur HOCO 18W", "description": "Un chargeur HOCO 18W offert, remis avec votre prochaine commande.", "imageUrl": "https://cdn.hoco.dz/rewards/chargeur-18w.webp", "costPoints": 10000, "minLevel": null, "value": { "amountTtc": null, "percent": null }, "available": true, "perResellerLimit": null, "validUntil": null, "eligibility": { "eligible": true, "reasons": [], "missingPoints": 0 } },
    { "id": "01a49ff6-9650-7037-9a23-56db872ec25d", "type": "GIFT", "name": "Power Bank HOCO 10000mAh", "description": "Une power bank HOCO J86A offerte, remise avec votre prochaine commande.", "imageUrl": "https://cdn.hoco.dz/rewards/power-bank.webp", "costPoints": 12000, "minLevel": null, "value": { "amountTtc": null, "percent": null }, "available": true, "perResellerLimit": null, "validUntil": null, "eligibility": { "eligible": true, "reasons": [], "missingPoints": 0 } },
    { "id": "01a49ff6-9e20-7514-be23-606e20f62354", "type": "VOUCHER", "name": "Bon d'achat 2 000 DA", "description": "2 000,00 DA déduits de votre prochaine commande, réservé aux niveaux Gold et VIP.", "imageUrl": "https://cdn.hoco.dz/rewards/bon-2000.webp", "costPoints": 15000, "minLevel": "GOLD", "value": { "amountTtc": 200000, "percent": null }, "available": true, "perResellerLimit": 2, "validUntil": "2027-12-31T22:59:59Z", "eligibility": { "eligible": false, "reasons": ["LEVEL_TOO_LOW", "INSUFFICIENT_POINTS"], "missingPoints": 2550 } },
    { "id": "01a49ff6-9a38-703a-aff9-b053b98f7e05", "type": "GIFT", "name": "Écouteurs TWS HOCO", "description": "Une paire d'écouteurs HOCO EW41 offerte.", "imageUrl": "https://cdn.hoco.dz/rewards/ecouteurs-tws.webp", "costPoints": 18000, "minLevel": null, "value": { "amountTtc": null, "percent": null }, "available": true, "perResellerLimit": null, "validUntil": null, "eligibility": { "eligible": false, "reasons": ["INSUFFICIENT_POINTS"], "missingPoints": 5550 } }
  ],
  "nextCursor": null
}

Errors — 400 VALIDATION_FAILED, 403 FEATURE_DISABLED.

Rules & side effects — read-only. Unavailable rewards (out of gift stock, outside their dates) are listed with available: false and UNAVAILABLE so the reseller sees the catalogue, but cannot redeem them (cahier §5.5). No gift stock quantity is exposed.

Used by — RA-39, « Récompenses disponibles » of reference screen 7, RW-15.

POST /loyalty/rewards/{rewardId}/redeem

loyalty.redeem · Access session — reseller (self), AccountStatus ACTIVE · Apps RA, RW · Idempotency-Key required · Release V2 · Requirements LOY-04

Purpose. Exchanges points for a reward; vouchers are issued at once, gifts are delivered with the next order.

Request

interface RedeemRequest {
  qty?: number;                       // default 1, within perResellerLimit
}
POST /v1/loyalty/rewards/01a49ff6-a208-7dcb-ae4e-a7708a7fe389/redeem
{ "qty": 1 }

Response — 201 Created: the RedemptionView.

{
  "id": "01a57352-3900-72ce-8e18-68d2386178eb",
  "reward": { "id": "01a49ff6-a208-7dcb-ae4e-a7708a7fe389", "type": "FREE_DELIVERY", "name": "Livraison offerte", "imageUrl": "https://cdn.hoco.dz/rewards/livraison-offerte.webp" },
  "qty": 1,
  "pointsSpent": 3000,
  "status": "FULFILLED",
  "voucher": {
    "id": "01a57352-3900-7ee9-b653-a32074077342",
    "code": "LIV-7K3M-9QX2",
    "type": "FREE_DELIVERY",
    "value": { "amountTtc": null, "percent": null },
    "expiresAt": "2027-08-10T22:59:59Z",
    "status": "ACTIVE",
    "usedOnOrder": null
  },
  "fulfilment": "Code à saisir au moment de valider votre commande.",
  "balanceAfter": 12450,
  "createdAt": "2027-05-12T10:00:00Z",
  "fulfilledAt": "2027-05-12T10:00:00Z"
}

Errors

StatusCodeWhen
403FEATURE_DISABLEDProgramme switched off.
403ACCOUNT_PENDING_VALIDATIONAccount not validated.
404NOT_FOUNDUnknown reward.
422INSUFFICIENT_POINTSBalance below costPoints × qty (details: { points, required, missingPoints }).
422LOYALTY_LEVEL_TOO_LOWBelow minLevel (details: { level, minLevel }).
422REWARD_UNAVAILABLEOut of gift stock or outside its dates.
422REWARD_LIMIT_REACHEDperResellerLimit for the period reached.

Rules & side effects

  1. One transaction: RewardRedemption, LoyaltyTransaction REDEEM (−pointsSpent, balance locked FOR UPDATE so two devices cannot overspend), and per type: VOUCHER, FREE_DELIVERY, DISCOUNT → a Voucher with a unique code, status FULFILLED; GIFT → status REQUESTED, the gift is added free to the next order by the back-office (admin.redemptions.list); EARLY_ACCESS → flag on the account for the period, FULFILLED.
  2. Vouchers are used at checkout: the V2 cart.checkout body gains an optional voucherCode (specified with SRV-29); a used voucher records its order.
  3. Cancelling a REQUESTED redemption (back-office) or an unused voucher writes a REVERSAL that gives the points back.
  4. Outbox loyalty.redeemed → inbox and back-office queue for gifts.

Used by — « Échanger » on RA-39, RW-15.

GET /loyalty/redemptions

loyalty.redemptions · Access session — reseller (self) · Apps RA, RW · Idempotency-Key — · Release V2 · Requirements LOY-04

Purpose. « Mes récompenses »: my redemptions, gifts to receive and vouchers with their codes.

Request — status (repeatable), cursor, limit.

Response — 200 OK: { items: RedemptionView[], nextCursor }.

{
  "items": [
    {
      "id": "01a57352-3900-72ce-8e18-68d2386178eb",
      "reward": { "id": "01a49ff6-a208-7dcb-ae4e-a7708a7fe389", "type": "FREE_DELIVERY", "name": "Livraison offerte", "imageUrl": "https://cdn.hoco.dz/rewards/livraison-offerte.webp" },
      "qty": 1,
      "pointsSpent": 3000,
      "status": "FULFILLED",
      "voucher": { "id": "01a57352-3900-7ee9-b653-a32074077342", "code": "LIV-7K3M-9QX2", "type": "FREE_DELIVERY", "value": { "amountTtc": null, "percent": null }, "expiresAt": "2027-08-10T22:59:59Z", "status": "ACTIVE", "usedOnOrder": null },
      "fulfilment": "Code à saisir au moment de valider votre commande.",
      "balanceAfter": 12450,
      "createdAt": "2027-05-12T10:00:00Z",
      "fulfilledAt": "2027-05-12T10:00:00Z"
    },
    {
      "id": "01a50206-5100-78e9-93bb-33f5aa5421be",
      "reward": { "id": "01a49ff6-9268-7a6c-ba93-b0c8566aa0b1", "type": "GIFT", "name": "Câble USB-C HOCO", "imageUrl": "https://cdn.hoco.dz/rewards/cable-usb-c.webp" },
      "qty": 1,
      "pointsSpent": 6000,
      "status": "FULFILLED",
      "voucher": null,
      "fulfilment": "Remis avec la commande CMD-2027-000398.",
      "balanceAfter": 14447,
      "createdAt": "2027-04-20T10:00:00Z",
      "fulfilledAt": "2027-04-24T09:30:00Z"
    }
  ],
  "nextCursor": null
}

Errors — 400 VALIDATION_FAILED, 403 FEATURE_DISABLED.

Rules & side effects — read-only; voucher codes are shown in full only to their owner.

Used by — RA-39, RW-15.

On this page