HOCO B2B
SRSAPI reference

Notifications & announcements

The in-app inbox, push notifications and their deep links, the live event stream of the reseller and delegate apps, channel preferences and back-office announcements.

Every business event that concerns a user produces a notification: one entry in the user's in-app inbox (always) plus, depending on the event's rule and the user's preferences, a push (Firebase Cloud Messaging), an SMS or an email (NOT-01 to NOT-06). While an app or page is open, the reseller apps (and the delegate app) also keep a live event stream — GET /events — so an order status changed by the back-office appears on screen at once (ORD-15). The stream is a convenience: everything it carries is also delivered as a notification, so nothing is lost when it is disconnected.

Announcements (« Messages ») are messages written in the back-office (BO-57) for a target — all delegates, the resellers of a wilaya or a segment… (NOT-07). Each published announcement is listed by announcements.list and also creates an announcement.published notification.

How it is produced: a business change writes an OutboxEvent in its own transaction; the worker matches it against the NotificationRules (event → audience → channels, edited on BO-58), renders the NotificationTemplate in the recipient's locale, filters the channels by the recipient's preferences, stores the Notification and one NotificationDelivery per channel, publishes notification.created on the recipient's live stream and sends the push.

MethodPathOperationAccessAppsRelease
GET/notificationsnotifications.list
My notification inbox (cursor pagination, unread filter)
sessionRA · RW · DA · BOV1
GET/notifications/unread-countnotifications.unreadCount
Unread badge count
sessionRA · RW · DA · BOV1
POST/notifications/{notificationId}/readnotifications.markRead
Mark one notification as read
sessionRA · RW · DA · BOV1
POST/notifications/read-allnotifications.markAllRead
Mark all as read
sessionRA · RW · DA · BOV1
GET/eventsevents.stream
Server-Sent Events stream of the caller's own live events (order.status_changed, order.modified, assisted_order.awaiting_confirmation, ticket.replied, return.status_changed, notification.created) while the app or page is open; push notifications cover the rest
sessionRA · RW · DAV1
GET/notification-preferencesnotifications.preferences.get
My channel preferences per category
sessionRA · RW · DAV1
PUT/notification-preferencesnotifications.preferences.update
Update my preferences (transactional ones cannot be disabled)
sessionRA · RW · DAV1
GET/announcementsannouncements.list
Announcements targeted to me ('Messages')
sessionRA · DAV1
GET/announcements/{announcementId}announcements.get
One announcement targeted to me (opened from a notification)
selfRA · RW · DAV1
POST/announcements/{announcementId}/readannouncements.markRead
Acknowledge an announcement
sessionRA · DAV1

Besides the errors listed per operation, every operation can answer 401 UNAUTHENTICATED, 403 ACCOUNT_SUSPENDED, 426 UPGRADE_REQUIRED, 429 RATE_LIMITED, 500 INTERNAL_ERROR and 503 MAINTENANCE. Guests have no inbox and no stream.

Notification object

interface NotificationItem {
  id: Uuid;
  category: "TRANSACTIONAL" | "SECURITY" | "FIELD" | "PROMOTIONAL" | "ANNOUNCEMENT";   // NotificationCategory
  eventKey: string;                  // "order.validated" — see the event catalogue
  title: string;                     // rendered in the recipient's locale when created
  body: string;
  imageUrl: string | null;           // public image (promotion or announcement banner)
  data: {
    route: string;                   // deep link in the recipient's app, e.g. "/orders/01a1a2c4-…" — see Routes
    entityType: string | null;       // ORDER, RETURN, TICKET, INVOICE, CREDIT_NOTE, PAYMENT, PROMOTION,
                                     // ANNOUNCEMENT, TOUR, VISIT, TASK, RESELLER, INVITATION, OBJECTIVE,
                                     // VARIANT, EXPORT, IMPORT, SESSION
    entityId: Uuid | null;
    [key: string]: string | null;    // event-specific strings, e.g. "orderNumber", "step", "resolution"
  };
  createdAt: Iso;
  readAt: Iso | null;
}
{
  "id": "01a1a2f0-7c1d-7e2a-9b3c-4d5e6f7a8b90",
  "category": "TRANSACTIONAL",
  "eventKey": "order.validated",
  "title": "Commande confirmée",
  "body": "Votre commande CMD-2026-000158 (45 600,00 DA) a été confirmée par HOCO. Elle passe en préparation.",
  "imageUrl": null,
  "data": {
    "route": "/orders/01a1a2c4-6e80-7f12-b345-6789abcdef01",
    "entityType": "ORDER",
    "entityId": "01a1a2c4-6e80-7f12-b345-6789abcdef01",
    "orderNumber": "CMD-2026-000158",
    "status": "VALIDATED"
  },
  "createdAt": "2026-11-05T09:14:27Z",
  "readAt": null
}
FieldNotes
title, bodyFinal text, ready to display; never re-rendered later (a notification stays as it was sent).
dataAll values are strings or null, so the same object fits the FCM data payload (FCM only carries strings).
data.routeOpened when the user taps the item or the push; mapped by each app to its screen (Routes). Unknown or unauthorised route → open the inbox. The same event can carry different routes for different audiences.
entityTypeAssumption — a closed list to be added to enums.json as NotificationEntityType.

Push messages

Push uses the FCM HTTP v1 API with a notification message (displayed by the OS even when the app is killed) and the same data as the inbox item:

{
  "message": {
    "token": "fJ3k9L0aQ2W:APA91bH7xY4Zr2mTq8VwN1cPs5dE6gK0uB3oR9tL7yX2nM4",
    "notification": {
      "title": "Commande confirmée",
      "body": "Votre commande CMD-2026-000158 (45 600,00 DA) a été confirmée par HOCO. Elle passe en préparation."
    },
    "data": {
      "notificationId": "01a1a2f0-7c1d-7e2a-9b3c-4d5e6f7a8b90",
      "eventKey": "order.validated",
      "category": "TRANSACTIONAL",
      "route": "/orders/01a1a2c4-6e80-7f12-b345-6789abcdef01",
      "entityType": "ORDER",
      "entityId": "01a1a2c4-6e80-7f12-b345-6789abcdef01"
    },
    "android": {
      "priority": "HIGH",
      "notification": { "channel_id": "hoco_transactional", "tag": "order:01a1a2c4-6e80-7f12-b345-6789abcdef01" }
    },
    "apns": { "payload": { "aps": { "thread-id": "order:01a1a2c4-6e80-7f12-b345-6789abcdef01", "badge": 3 } } }
  }
}
  • Android channels — the apps create one channel per category at first start: hoco_transactional (Commandes et compte, high importance), hoco_security (Sécurité, high), hoco_field (Activité terrain, high), hoco_promotional (Offres et promotions, default), hoco_announcement (Annonces, default).
  • Grouping — tag / thread-id = <entityType>:<entityId>: a newer push about the same order replaces the older one in the tray.
  • Tap — open data.route, then call notifications.markRead with notificationId.
  • Foreground — no system banner: the app shows an in-app banner, increments the badge and refreshes the screen concerned (the live stream usually did already).
  • Badge — iOS badge is the unread count after this notification.
  • Promotional frequency — at most one PROMOTIONAL push per reseller per day; further promotional notifications go to the inbox only.

Event catalogue

The eventKey values clients receive in the inbox and in push data. This list follows the notifications matrix, which is the reference for triggers, recipients, channels, quiet hours, caps and the French copy (template keys such as order.submitted.staff are variants of one event key for one audience). Audience: R reseller, D delegate (assigned, source of an assisted order, or inviter), S staff holding the permission shown. SSE marks events also pushed on the recipient's live stream. An unknown eventKey must still be displayed (title and body are ready) and its route opened when known.

Accounts and security

Event keyAudience → categoryChannelsdata.route
account.pending_validationR → TRANSACTIONAL · S crm.validateR in-app · S in-app, back-office SSE account.pending, email digestR /account/status · S /crm/validation
account.approvedR → TRANSACTIONALin-app, push, SMS/
account.rejectedR → TRANSACTIONALin-app, push (data.reason)/account/status
account.customer_type_changedR → TRANSACTIONALin-app, push (data.customerType) — refresh Me and the cart/
account.suspendedthe user → TRANSACTIONALSMS, in-app — sessions are revoked, sign-in shows the message—
security.password_changedthe user → SECURITYin-app, push, SMS (phone accounts), email (staff)/settings/security
security.new_devicethe user → SECURITYin-app, push to the other devices, email (staff)/settings/security
security.phone_changedthe user → SECURITYSMS to the old number, in-app (Assumption — to add to the matrix)/settings/security
otp.code, user.inviteda phone or a new userSMS or email only — never in an inbox—

Orders and assisted orders — every order waits for an explicit back-office confirmation (ORD-14); every status change reaches the reseller as a notification and, while the app is open, as an order.status_changed live event (ORD-15).

Event keyAudience → categoryChannelsdata.route
order.submittedR → TRANSACTIONAL · S orders.validate · DR in-app, push, SSE (« Commande envoyée, elle sera confirmée par notre équipe ») · S in-app (« Commande à confirmer ») · D in-app, pushR, D /orders/{orderId} · S /orders/to-confirm
order.confirmation_overdueS orders.validatein-app, email/orders/to-confirm
order.validatedR → TRANSACTIONAL · DR in-app, push, SSE · D in-app/orders/{orderId}
order.refusedR → TRANSACTIONAL · Din-app, push, SSE (data.reason)/orders/{orderId}
order.modifiedR → TRANSACTIONAL · Din-app, push, SSE order.modified (data.changeSummary)/orders/{orderId}
order.status_changedR → TRANSACTIONAL · Din-app, push, SSE — data.status: PENDING, PREPARING, READY, SHIPPED (data.carrierName, data.trackingNumber), DELIVERED, RETURNED/orders/{orderId}
delivery.failedR → TRANSACTIONAL · Sin-app, push, SSE/orders/{orderId}
order.cancelledR → TRANSACTIONAL (when HOCO cancels) · S orders.validate (when the reseller cancels) · Din-app, push, SSE (data.reason); stock is released automatically/orders/{orderId}
assisted_order.awaiting_confirmationR → TRANSACTIONAL · SR in-app, push, SMS, SSE (data.delegateFirstName, data.expiresAt) · S in-appR /orders/{orderId}/confirm · S /orders/to-confirm
assisted_order.reminderR → TRANSACTIONALin-app, push, 24 h before expiry/orders/{orderId}/confirm
assisted_order.confirmedD → FIELD · R when HOCO confirms on the reseller's behalfin-app, push/orders/{orderId}
assisted_order.rejectedD → FIELD · Sin-app, push (data.reason)/orders/{orderId}
assisted_order.expiredD → FIELD · Rin-app, push (D), in-app (R); the order is cancelled/orders/{orderId}

Returns, after-sales and support

Event keyAudience → categoryChannelsdata.route
return.requestedS orders.validate · R → TRANSACTIONALS in-app, back-office SSE, email digest · R in-app (receipt)/returns/{returnId}
return.approvedR → TRANSACTIONAL · SR in-app, push, SSE return.status_changed (data.instructions) · S in-app (« Retour à réceptionner »)/returns/{returnId}
return.rejectedR → TRANSACTIONALin-app, push, SSE (data.reason)/returns/{returnId}
return.receivedR → TRANSACTIONAL · Sin-app, push, SSE (data.creditNoteNumber, data.creditAmount)/returns/{returnId}
ticket.createdS support.update (or the assignee) · R → TRANSACTIONALS in-app, email digest (data.ticketType; AFTER_SALES claims first) · R in-app (receipt)/support/tickets/{ticketId}
ticket.repliedR → TRANSACTIONAL · S assigneeR in-app, push, SSE · S in-app/support/tickets/{ticketId}
ticket.status_changedR → TRANSACTIONALpush + in-app for WAITING_RESELLER and RESOLVED (non-SAV), in-app for CLOSED/support/tickets/{ticketId}
ticket.resolvedR → TRANSACTIONALin-app, push, SSE ticket.resolved — data.resolution: REPLACEMENT (data.replacementOrderId), RETURN (data.returnId), CREDIT_NOTE, REPAIR/support/tickets/{ticketId}
ticket.rejectedR → TRANSACTIONALin-app, push, SSE ticket.resolved with resolution REJECTED (data.reason)/support/tickets/{ticketId}

Finance, promotions and messaging

Event keyAudience → categoryChannelsdata.route
payment.declaredS finance.validatein-app, email digest/finance/payments/{paymentId}
payment.confirmed, payment.rejectedR → TRANSACTIONALin-app, push (data.reason when rejected)/finance/payments/{paymentId}
invoice.issuedR → TRANSACTIONALin-app, email (PDF link, when an address is known)/finance/invoices/{invoiceId}
invoice.overdueR → TRANSACTIONAL — V2in-app, push, SMS/finance/invoices/{invoiceId}
promotion.startedR → PROMOTIONALin-app, push (frequency cap)/promotions/{promotionId}
daily_offer.publishedR → PROMOTIONALin-app, push/promotions
announcement.publishedR, D → ANNOUNCEMENTin-app, push when Announcement.sendPush is true/messages/{announcementId}

Field operations

Event keyAudience → categoryChannelsdata.route
tour.assignedD → FIELDin-app, push/tours/{tourId}
tour.updatedD → FIELDin-app, push, plus a data message that triggers sync.delta; also sent when a tour is cancelled (data.status: CANCELLED)/tours/{tourId}
tour.reopenedD → FIELDin-app, push (data.reason)/tours/{tourId}/closing
tour.startedS field.viewback-office SSE only by default/field/tours/{tourId}
tour.closedS field.viewin-app, back-office SSE/field/tours/{tourId}
tour.exceptionsS field.viewin-app, email digest (tours not started or not closed the day before)/field/tours
visit.not_doneS field.viewin-app — immediately for HIGH priority stops, otherwise inside the tour.closed summary/field/visits/{visitId}
task.assignedassignee → FIELDin-app, push (delegates)/tasks/{taskId}
task.dueassignee → FIELDin-app, push — one digest at 08:00 Africa/Algiers/tasks
invitation.step_completedD (inviter) → FIELDin-app for every step, push for ACCOUNT_CREATED and FIRST_ORDER (data.step)/invitations/{invitationId}

Back-office platform

Event keyAudienceChannelsdata.route
stock.low, stock.outS stock.update (stock.out also orders.validate)in-app, back-office SSE stock.alert, email digest/stock?alert=low
export.readyrequesterin-app, back-office SSE, email when the job took over 2 minutes (data.status: SUCCEEDED, FAILED)/reports/exports
import.finishedrequesterin-app, back-office SSE, email/admin/imports/{importId}
digest.dailystaff with queued itemsemail only (08:00, Sunday–Thursday)—

V2 loyalty events (loyalty.*) will be added with the loyalty programme (LOY-01 to LOY-04). Visit check-ins and delegate positions are never notified: they only feed the back-office live map through /admin/events.

Routes

data.route is a path in the recipient's app. Each app maps it to a screen; an unknown or unauthorised route opens the inbox.

RouteApp → screen
/RA → RA-09 · RW → RW-02
/account/statusRA → RA-06 · RW → RW-12
/orders/{orderId}RA → RA-22 · RW → RW-09 · DA → DA-18 · BO → BO-39
/orders/{orderId}/confirmRA → RA-23 · RW → RW-09
/orders/to-confirmBO → BO-41
/returns/{returnId}RA → RA-40 · RW → RW-09 · BO → BO-69
/support/tickets/{ticketId}RA → RA-34 · RW → RW-14 · BO → BO-56
/finance/invoices/{invoiceId}RA → RA-30 · RW → RW-13 · BO → BO-44
/finance/payments/{paymentId}RA → RA-31 · RW → RW-13 · BO → BO-45
/promotions, /promotions/{promotionId}RA → RA-24, RA-25 · RW → RW-10
/messages/{announcementId}RA → RA-26 · RW → RW-11 · DA → DA-23
/settings/securityRA → RA-35 · RW → RW-12 · DA → DA-26 · BO → BO-68
/tours/{tourId}, /tours/{tourId}/closingDA → DA-06, DA-20
/tasks, /tasks/{taskId}DA → DA-19 · BO → BO-14
/invitations/{invitationId}DA → DA-16
/crm/validationBO → BO-13
/field/tours, /field/tours/{tourId}, /field/visits/{visitId}BO → BO-21, BO-23, BO-25
/stock?alert=lowBO → BO-33
/reports/exportsBO → BO-60
/admin/imports/{importId}BO → BO-67
inbox (fallback)RA → RA-26 · RW → RW-11 · DA → DA-23 · BO → BO-03

Reseller-web screens are on the reseller web page.

Operations

GET /notifications

notifications.list · Access session (any user type, own inbox) · Apps RA, RW, DA, BO · Idempotency-Key — · Release V1 · Requirements NOT-01

Purpose. Returns the user's inbox, newest first, with read state.

Request

Query parameterRequiredNotes
cursor, limitnoCursor pagination (default 20, max 100).
unreadnotrue → unread only.
categorynoRepeatable NotificationCategory.

Response — 200 OK.

{
  "items": [
    {
      "id": "01a1a2f0-7c1d-7e2a-9b3c-4d5e6f7a8b90",
      "category": "TRANSACTIONAL",
      "eventKey": "order.validated",
      "title": "Commande confirmée",
      "body": "Votre commande CMD-2026-000158 (45 600,00 DA) a été confirmée par HOCO. Elle passe en préparation.",
      "imageUrl": null,
      "data": {
        "route": "/orders/01a1a2c4-6e80-7f12-b345-6789abcdef01",
        "entityType": "ORDER",
        "entityId": "01a1a2c4-6e80-7f12-b345-6789abcdef01",
        "orderNumber": "CMD-2026-000158",
        "status": "VALIDATED"
      },
      "createdAt": "2026-11-05T09:14:27Z",
      "readAt": null
    },
    {
      "id": "01a1a1f2-3a4b-7c5d-8e6f-7a8b9c0d1e2f",
      "category": "ANNOUNCEMENT",
      "eventKey": "announcement.published",
      "title": "Nouveaux chargeurs ACEFAST 65 W",
      "body": "La gamme ACEFAST GaN 65 W est disponible dès aujourd'hui. Profitez du lancement.",
      "imageUrl": "https://cdn.hoco.dz/banners/2026/11/01a1a1f0-9d8c-7b6a-8f5e-4d3c2b1a0f9e.webp",
      "data": {
        "route": "/messages/01a1a1f0-2c3d-7e4f-9a0b-1c2d3e4f5a6b",
        "entityType": "ANNOUNCEMENT",
        "entityId": "01a1a1f0-2c3d-7e4f-9a0b-1c2d3e4f5a6b"
      },
      "createdAt": "2026-11-04T08:00:00Z",
      "readAt": "2026-11-04T12:31:09Z"
    }
  ],
  "nextCursor": "eyJ0IjoiMjAyNi0xMS0wNFQwODowMDowMFoiLCJpIjoiMDFhMWExZjIifQ"
}

Errors — 400 VALIDATION_FAILED for an unknown category or a bad cursor.

Rules & side effects

  1. The inbox keeps notifications for 12 months (Notification.expiresAt, proposal to validate with HOCO in D-11); older items are purged by a nightly job.
  2. Read state is per notification; reading the inbox does not mark anything read.

Used by — RA-26, RW-11, the notifications tab of DA-23, the bell panel of BO-03; the latest items on the home screen RA-09.

GET /notifications/unread-count

notifications.unreadCount · Access session (any user type) · Apps RA, RW, DA, BO · Idempotency-Key — · Release V1 · Requirements NOT-01

Purpose. Returns the badge counts, cheaply enough to call on every foreground.

Request — no parameters.

Response — 200 OK, Cache-Control: private, no-store.

{
  "unread": 3,
  "byCategory": { "TRANSACTIONAL": 2, "SECURITY": 0, "FIELD": 0, "PROMOTIONAL": 0, "ANNOUNCEMENT": 1 },
  "unreadAnnouncements": 1
}
FieldNotes
unreadBadge of the bell / inbox tab. Includes announcement.published items.
unreadAnnouncementsBadge of the « Messages » tab (DA-23): announcements not yet acknowledged.

Errors — none specific.

Rules & side effects — counts are cached per user in Redis and updated on every new notification and read; while the live stream is connected, each notification.created event carries the new unread value, so polling is only needed on app start and resume.

Used by — tab badges of the reseller app and the delegate app, the bell of BO-03 and the header of the reseller web.

POST /notifications/{notificationId}/read

notifications.markRead · Access session (own notifications) · Apps RA, RW, DA, BO · Idempotency-Key optional · Release V1 · Requirements NOT-01

Purpose. Marks one notification as read when the user opens it (from the inbox or from a push).

Request

Path parameterTypeNotes
notificationIdUuidNotificationItem.id or data.notificationId of the push.

Response — 200 OK.

{ "id": "01a1a2f0-7c1d-7e2a-9b3c-4d5e6f7a8b90", "readAt": "2026-11-05T09:16:02Z", "unread": 2 }

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id or another user's notification.

Rules & side effects — idempotent: an already read notification keeps its first readAt. Reading an announcement.published notification does not acknowledge the announcement (that is announcements.markRead, which marks the notification too).

Used by — tap on an inbox row or a push in every app.

POST /notifications/read-all

notifications.markAllRead · Access session (own notifications) · Apps RA, RW, DA, BO · Idempotency-Key optional · Release V1 · Requirements NOT-01

Contract 1.1.0 (SRV-41): group (a NotificationGroup) marks only the items of one Notification Centre tab; it combines with category and before.

Purpose. Marks every unread notification (optionally of one category) as read.

Request

interface MarkAllReadRequest {
  category?: "TRANSACTIONAL" | "SECURITY" | "FIELD" | "PROMOTIONAL" | "ANNOUNCEMENT";
  before?: Iso;                      // only items created up to this time (the newest item the user saw)
}
{ "before": "2026-11-05T09:14:27Z" }

Response — 200 OK.

{ "updated": 3, "unread": 0 }

Errors — 400 VALIDATION_FAILED for an unknown category or a malformed date.

Rules & side effects — before prevents marking a notification that arrived after the list was displayed. Announcements are not acknowledged by this call.

Used by — « Tout marquer comme lu » on RA-26, RW-11, DA-23 and BO-03.

GET /events

events.stream · Access session (RESELLER, DELEGATE; own events only) · Apps RA, RW, DA · Idempotency-Key — · Release V1 · Requirements ORD-15, SYN-07, NOT-01

Purpose. Keeps an open Server-Sent Events connection that pushes the caller's own live events — order status changes first of all — while the app or the page is open, so screens update without polling.

Request

HeaderRequiredNotes
Acceptyestext/event-stream
AuthorizationmobileBearer hs_… — Flutter has no browser EventSource; use an SSE client that sends headers (for example a dio streamed GET or a package such as flutter_client_sse).
CookiewebThe browser's EventSource cannot set headers: the reseller web opens new EventSource("https://api.hoco.dz/v1/events", { withCredentials: true }) and the session cookie authenticates it.
Last-Event-IDon reconnectId of the last event received; the server replays what was missed. Browsers send it automatically; mobile clients must send it themselves.
X-App-Version, X-App-Platform, X-Device-IdmobileAs on every call.

Response — 200 OK, Content-Type: text/event-stream, Cache-Control: no-cache, X-Accel-Buffering: no; the connection stays open. Each event has an id (UUIDv7, increasing), an event type and one line of JSON data:

retry: 5000

id: 01a1a2f0-7b00-7a11-8c22-0d1e2f3a4b5c
event: order.status_changed
data: {"orderId":"01a1a2c4-6e80-7f12-b345-6789abcdef01","number":"CMD-2026-000158","from":"SUBMITTED","to":"VALIDATED","at":"2026-11-05T09:14:26Z","reason":null}

id: 01a1a2f0-7c1d-7e2a-9b3c-4d5e6f7a8b91
event: notification.created
data: {"notification":{"id":"01a1a2f0-7c1d-7e2a-9b3c-4d5e6f7a8b90","category":"TRANSACTIONAL","eventKey":"order.validated","title":"Commande confirmée","body":"Votre commande CMD-2026-000158 (45 600,00 DA) a été confirmée par HOCO. Elle passe en préparation.","imageUrl":null,"data":{"route":"/orders/01a1a2c4-6e80-7f12-b345-6789abcdef01","entityType":"ORDER","entityId":"01a1a2c4-6e80-7f12-b345-6789abcdef01","orderNumber":"CMD-2026-000158","status":"VALIDATED"},"createdAt":"2026-11-05T09:14:27Z","readAt":null},"unread":3}

: ping

Event types

eventdataSent toClient reaction
order.status_changed{ orderId, number, from, to, at, reason } (OrderStatus values)Reseller of the order; the delegate who prepared it (assisted orders)Update the order list counters and the open order detail (RA-21, RA-22, RW-08, RW-09) without refetching the list; refetch the detail if it is open.
order.modified{ orderId, number, at, changeSummary }ResellerRefetch the open order detail (lines, totals) and show « Commande modifiée par HOCO ».
assisted_order.awaiting_confirmation{ orderId, number, delegateName, totalTtc, expiresAt }ResellerShow the confirmation banner / open RA-23 on tap.
return.status_changed{ returnId, number, orderId, from, to, at, reason } (ReturnStatus)ResellerUpdate RA-40 / the order's returns section.
ticket.replied{ ticketId, number, messageId, authorName, at }Reseller of the ticketAppend to the open conversation (RA-34, RW-14) or mark the ticket unread.
ticket.resolved{ ticketId, number, resolution, at } (AfterSalesResolution or null)Reseller of the ticketShow the resolution; refresh linked order, return or credit note.
notification.created{ notification: NotificationItem, unread }The recipientPrepend to the inbox if open, update badges; no system banner (the push is suppressed in foreground).
resync{ reason: "REPLAY_WINDOW_EXCEEDED" }—Last-Event-ID is older than the 10-minute replay window: refetch the open screen and the unread count, then continue.

A comment line : ping is sent every 25 seconds (heartbeat); a client that receives nothing for 60 s reconnects.

Errors (answered before the stream opens, as JSON)

StatusCodeWhen
401UNAUTHENTICATEDNo or expired session. The browser EventSource sees a failed connection: the web app then checks auth.session.
403PERMISSION_DENIEDStaff session (the back-office uses /admin/events).
429RATE_LIMITEDMore than 3 open streams for the same user (the oldest is closed instead when it belongs to the same device).

Rules & side effects

  1. Scope. Only the caller's own events: the reseller's orders, returns and tickets; the delegate's notifications and the assisted orders they prepared. Delegates mainly use it for notification.created.
  2. Replay. Events are kept 10 minutes in a Redis stream per user — enough to cover a network drop. On reconnect with Last-Event-ID the server sends everything after it, in order, before live events; an older id gets resync. Events missed while the app was closed or in the background are not replayed: on resume the app reloads the screen it shows, and the inbox holds the history.
  3. Lifecycle. Mobile apps open the stream when the app is in the foreground and signed in, close it in the background (push takes over) and reconnect with exponential backoff (1 s → 30 s, honouring retry). The reseller web keeps it open while a tab is visible.
  4. Session end. Sign-out, revocation, password change or suspension closes the stream from the server; the reconnection then gets 401.
  5. Delivery guarantee. At-least-once with ids: clients ignore an id they already processed. The stream never carries anything that is not also available through the regular operations (orders.get, notifications.list…).
  6. Infrastructure. The reverse proxy must not buffer (X-Accel-Buffering: no) and must allow idle connections of at least 60 s; one Node process holds the connections and reads the per-user Redis streams fed by the outbox worker.

Used by — reseller app shell (all screens, mainly RA-21, RA-22, RA-23, RA-34, RA-40); reseller web layout (RW-08, RW-09, RW-14); delegate app shell (badges, DA-18).

GET /notification-preferences

notifications.preferences.get · Access session (RESELLER, DELEGATE) · Apps RA, RW, DA · Idempotency-Key — · Release V1 · Requirements NOT-06

Purpose. Returns, per notification category, which channels the user accepts and which ones are locked.

Request — no parameters.

Response — 200 OK.

interface NotificationPreferences {
  categories: {
    category: "TRANSACTIONAL" | "SECURITY" | "FIELD" | "PROMOTIONAL" | "ANNOUNCEMENT";
    channels: { channel: "IN_APP" | "PUSH" | "SMS" | "EMAIL"; enabled: boolean; locked: boolean }[];
  }[];
  updatedAt: Iso | null;             // null = defaults never changed
}
{
  "categories": [
    {
      "category": "TRANSACTIONAL",
      "channels": [
        { "channel": "IN_APP", "enabled": true, "locked": true },
        { "channel": "PUSH", "enabled": true, "locked": true },
        { "channel": "SMS", "enabled": true, "locked": true }
      ]
    },
    {
      "category": "SECURITY",
      "channels": [
        { "channel": "IN_APP", "enabled": true, "locked": true },
        { "channel": "PUSH", "enabled": true, "locked": true },
        { "channel": "SMS", "enabled": true, "locked": true }
      ]
    },
    {
      "category": "PROMOTIONAL",
      "channels": [
        { "channel": "IN_APP", "enabled": true, "locked": true },
        { "channel": "PUSH", "enabled": true, "locked": false },
        { "channel": "SMS", "enabled": false, "locked": false }
      ]
    },
    {
      "category": "ANNOUNCEMENT",
      "channels": [
        { "channel": "IN_APP", "enabled": true, "locked": true },
        { "channel": "PUSH", "enabled": true, "locked": false },
        { "channel": "SMS", "enabled": false, "locked": false }
      ]
    }
  ],
  "updatedAt": null
}

Errors — 403 PERMISSION_DENIED for staff (staff notification settings are part of the back-office rules, BO-58).

Rules & side effects

  1. Categories by user type: resellers TRANSACTIONAL, SECURITY, PROMOTIONAL, ANNOUNCEMENT; delegates SECURITY, FIELD, ANNOUNCEMENT.
  2. Channels: IN_APP, PUSH, SMS; EMAIL appears only when the user has an email. WHATSAPP arrives with the WhatsApp Business integration (V2, NOT-08).
  3. Locks: TRANSACTIONAL and SECURITY are entirely locked (enum rule); IN_APP is locked on in every category (the inbox is the record).
  4. Defaults: PROMOTIONAL and ANNOUNCEMENT push on, SMS and email off (marketing SMS needs explicit opt-in); FIELD push on.
  5. A preference only removes channels: an enabled channel is used only for the events whose rule includes it. Example: transactional SMS are sent only for the few events whose rule says SMS (account.approved, order.refused, assisted_order.awaiting_confirmation…).

Used by — « Notifications » section of RA-35, RW-12 and DA-26.

PUT /notification-preferences

notifications.preferences.update · Access session (RESELLER, DELEGATE) · Apps RA, RW, DA · Idempotency-Key optional · Release V1 · Requirements NOT-06

Purpose. Saves the user's channel choices for the categories they may change.

Request — the categories to set (omitted categories keep their values; locked entries may be sent with their current value).

interface NotificationPreferencesUpdate {
  categories: {
    category: "TRANSACTIONAL" | "SECURITY" | "FIELD" | "PROMOTIONAL" | "ANNOUNCEMENT";
    channels: { channel: "IN_APP" | "PUSH" | "SMS" | "EMAIL"; enabled: boolean }[];
  }[];
}
{
  "categories": [
    {
      "category": "PROMOTIONAL",
      "channels": [
        { "channel": "PUSH", "enabled": false },
        { "channel": "SMS", "enabled": false }
      ]
    }
  ]
}

Response — 200 OK: the full NotificationPreferences as returned by the GET.

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown category or channel, a category or channel not offered to this user type.
403PERMISSION_DENIEDStaff session.
422NOTIFICATION_PREFERENCE_LOCKEDA locked channel sent with a value different from its current one (details.category, details.channel).

Rules & side effects — stored as NotificationPreference rows (user × category × channel); applies to notifications created after the call. Audit UPDATE (consent trail for marketing channels).

Used by — the switches of RA-35, RW-12 and DA-26.

GET /announcements

announcements.list · Access session (RESELLER, DELEGATE; announcements targeting the caller) · Apps RA, DA · Idempotency-Key — · Release V1 · Requirements NOT-07

Purpose. Lists the announcements published for the user (« Messages »), newest first, with acknowledgement state.

Request

Query parameterRequiredNotes
cursor, limitnoCursor pagination.
unreadnotrue → not yet acknowledged.

Response — 200 OK.

interface AnnouncementItem {
  id: Uuid;
  title: string;
  body: string;                      // plain text with line breaks; links are written as full URLs
  image: FileRef | null;             // Announcement.imageFileId, public banner image
  publishedAt: Iso;
  expiresAt: Iso | null;             // hidden after this date
  readAt: Iso | null;                // AnnouncementRead.readAt — acknowledgement
}
{
  "items": [
    {
      "id": "01a1a1f0-2c3d-7e4f-9a0b-1c2d3e4f5a6b",
      "title": "Nouveaux chargeurs ACEFAST 65 W",
      "body": "La gamme ACEFAST GaN 65 W est disponible dès aujourd'hui.\nPrésentez-la en priorité lors de vos visites cette semaine.",
      "image": {
        "id": "01a1a1f0-9d8c-7b6a-8f5e-4d3c2b1a0f9e",
        "url": "https://cdn.hoco.dz/banners/2026/11/01a1a1f0-9d8c-7b6a-8f5e-4d3c2b1a0f9e.webp",
        "thumbnailUrl": "https://cdn.hoco.dz/banners/2026/11/01a1a1f0-9d8c-7b6a-8f5e-4d3c2b1a0f9e_t.webp",
        "mimeType": "image/webp",
        "size": 214530,
        "purpose": "BANNER_IMAGE"
      },
      "publishedAt": "2026-11-04T08:00:00Z",
      "expiresAt": "2026-11-30T23:00:00Z",
      "readAt": null
    }
  ],
  "nextCursor": null
}

Errors — 400 VALIDATION_FAILED for a bad cursor.

Rules & side effects

  1. Targeting (targetUserTypes, targetZoneIds, targetWilayaCodes, targetSegmentIds — empty means all) is resolved when the announcement is sent (admin.announcements.send); users matching later (a new delegate in the zone) see it while it has not expired.
  2. Expired announcements disappear from the list; acknowledgements are kept for reporting (AnnouncementRead).

Used by — the « Messages » tab of DA-23 and the announcements section of RA-26; the delegate home DA-05 shows the latest unread one.

GET /announcements/{announcementId}

announcements.get · Access session — reseller, delegate · Apps RA, RW, DA · Release V1 · Requirements NOT-07 · Added in contract 1.1.0 (SRV-41)

Purpose. One announcement targeted to me, opened from its notification (/messages/{id}) without paging the list.

Response 200 OK — an AnnouncementItem, as in announcements.list; body is the Arabic one for Arabic readers when the back-office wrote it.

Errors

StatusCodeWhen
401UNAUTHENTICATEDNo session
403PERMISSION_DENIEDStaff caller
404NOT_FOUNDUnknown, unpublished, expired, or not targeted to me

Used by RW-11 · RA-26

POST /announcements/{announcementId}/read

announcements.markRead · Access session (RESELLER, DELEGATE; announcements targeting the caller) · Apps RA, DA · Idempotency-Key optional · Release V1 · Requirements NOT-07

See also announcements.get — GET /announcements/{announcementId} (contract 1.1.0): one announcement targeted to me, the same AnnouncementItem as the list (the Arabic body for Arabic readers when the back-office wrote one), 404 NOT_FOUND when it is not published, expired or not targeted to me. Used by the deep link of a notification (/messages/{id}).

Purpose. Records that the user read (acknowledged) an announcement.

Request

Path parameterTypeNotes
announcementIdUuidAnnouncementItem.id.

Response — 200 OK.

{ "id": "01a1a1f0-2c3d-7e4f-9a0b-1c2d3e4f5a6b", "readAt": "2026-11-05T07:52:40Z", "unreadAnnouncements": 0 }

Errors

StatusCodeWhen
404NOT_FOUNDUnknown announcement or not targeted to the caller.

Rules & side effects — idempotent (the first readAt is kept); creates the AnnouncementRead row and marks the matching announcement.published notification read. Read rates are shown to the sender on BO-57.

Used by — opening an announcement on DA-23 or RA-26.

On this page