HOCO B2B
SRSAPI referenceBack-office endpoints

Real-time events (SSE)

The back-office Server-Sent Events stream — connection, authentication, resume, heartbeat, event envelope, permission-filtered event catalogue and reconnection guidance.

The back-office keeps one Server-Sent Events connection per open tab to receive business events as they happen: new orders to confirm, status changes, check-ins, tours, delegate positions, stock alerts, pending accounts, returns and claims (SYN-07, FLD-13, cahier §4.1 « mises à jour automatiquement après les événements métiers », §6.4 « en temps réel »). Events come from the OutboxEvent table after commit, so a stream event is never sent for a rolled-back change. Resellers have their own stream (events.stream); everything important is also a notification, so nothing depends on the stream being connected (notifications matrix).

MethodPathOperationAccessAppsRelease
GET/admin/eventsadmin.events.stream
Server-Sent Events stream: order.created, order.status_changed, visit.checked_in, visit.completed, tour.started, tour.closed, delegate.position, stock.alert, account.pending
anyBOV1

GET /admin/events

admin.events.stream · Permission any back-office user (events filtered per permission) · Apps BO · Release V1 · Requirements SYN-07 · FLD-13

Purpose. A long-lived text/event-stream response carrying the events the caller may see.

Request

Header / queryValueNotes
Cookiesession cookieSame authentication as every back-office call (browser EventSource with withCredentials: true)
Accepttext/event-stream
Last-Event-IDlast id receivedSent automatically by EventSource on reconnection; resume
?types=comma-separated event typesOptional narrowing (e.g. the live map asks delegate.position,tour.started,tour.suspended,tour.resumed,tour.closed,visit.checked_in)
?tourId=UuidOptional: delegate.position only for this tour (tour detail page)

Response 200 OK, Content-Type: text/event-stream, Cache-Control: no-store, X-Accel-Buffering: no. The first frame is retry: 3000. A comment heartbeat : ping is sent every 25 s; the server closes the connection after 1 hour (the client reconnects transparently).

Event frame:

id: 1847263
event: order.status_changed
data: {"type":"order.status_changed","at":"2026-11-03T09:52:31Z","entity":{"type":"Order","id":"0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e","number":"CMD-2026-000158"},"payload":{"from":"SUBMITTED","to":"VALIDATED","resellerId":"0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a","actor":{"id":"01929e66-3d4e-7f5a-8b6c-7d8e9f0a1b2c","displayName":"Sofiane Hamdi"}}}
interface StreamEvent {
  type: string;                     // same as the SSE `event:` field
  at: Iso;                          // commit time of the change
  entity: { type: string; id: Uuid; number?: string };   // Prisma model and business number
  payload: Record<string, unknown>; // minimal data per event (below); never purchase prices or quantities the caller may not see
}

id is the monotonically increasing outbox sequence (a decimal string). Events of one entity arrive in commit order.

Errors

StatusCodeWhen
401UNAUTHENTICATEDNo or expired session (the stream also ends when the session is revoked)
403PERMISSION_DENIEDDelegate or reseller session (they use events.stream)
429STREAM_LIMIT_REACHEDMore than 5 open streams for the user — details: { max: 5 }

Rules & side effects

  1. Resume. The server keeps the last 24 hours of events (Redis stream). With a known Last-Event-ID it replays every missed event the caller may see, then continues. If the id is older than the buffer, it sends one stream.resync and continues from now — the client then re-fetches its open screens.
  2. Permission filter. Each event has a required permission (table below), evaluated at send time with the user's current permissions. export.ready, import.finished and me.permissions_changed go only to the user concerned.
  3. Throttling. delegate.position is sent at most once per 30 s per delegate; dashboard.invalidated at most once per 10 s per user.
  4. Reconnection guidance for the dashboard. Use one shared EventSource per tab (a React context), retry honoured by the browser; on error with the connection closed, back off 3 s, 6 s, 12 s… up to 60 s with ±20 % jitter; after a 401, stop and send the user to sign-in; on visibilitychange to visible after more than 5 minutes hidden, reconnect and let Last-Event-ID replay. TanStack Query caches are updated from payloads where possible (setQueryData), otherwise invalidated by query key.
  5. Not audited; no side effects.

Used by BO-03 (shell badges) · BO-04 to BO-08 · BO-41 · BO-42 · BO-13 · BO-23 · BO-55 · BO-69

Event catalogue

EventEmitted whenRequired permissionPayload
order.createdAn order leaves DRAFT (first SUBMITTED or PENDING), any channelorders.viewchannel, status, resellerId, storeName, totalTtc, isFirstOrder
order.to_confirmAn order enters the confirmation queue (SUBMITTED or PENDING)orders.validatestatus, kind (NEW/ON_HOLD/ASSISTED), pendingReasons, channel, storeName, totalTtc, controls (4 statuses), submittedAt
order.status_changedAny order transition, and delivery status changesorders.view (or delivery.view)from, to, deliveryStatus?, resellerId, actor
order.modifiedControlled line modificationorders.viewchangesCount, totalTtcBefore, totalTtcAfter
return.requestedA return request is created (reseller, claim or door refusal)orders.view or support.vieworderId, orderNumber, resellerId, origin, itemsCount, amountTtc
return.status_changedA return moves on: decision (approved / rejected), transport, reception, refund. Added in contract 1.1.0orders.view or support.vieworderId, resellerId, from, to, actor
visit.checked_inCheck-in recordedfield.viewtourId, delegateId, resellerId, distanceMeters, withinTolerance, position
visit.completedVisit DONEfield.viewtourId, delegateId, resellerId, durationMinutes, interestLevel?, nextAction?
visit.not_doneVisit NOT_DONEfield.viewtourId, delegateId, resellerId, reasonCode, priority
tour.startedTour IN_PROGRESSfield.viewdelegateId, date, startPosition?, stops
tour.suspended · tour.resumedDelegate pauses / resumesfield.viewdelegateId, reasonCode?
tour.closedTour COMPLETEDfield.viewdelegateId, visitsDone, visitsPlanned, km, revenueHt
tour.reopened · tour.cancelledBack-office reopening / cancellationfield.viewdelegateId, reason
delegate.positionLocation ping batch received for a tour IN_PROGRESSfield.viewdelegateId, tourId, position, progress
stock.alertA stock level changes alertLevelstock.viewvariantId, sku, warehouseCode, level (LOW/OUT/OK), available, threshold
account.pendingA record enters the validation queuecrm.validateresellerId, number, storeName, kind, declaredCustomerType, hasDuplicateHints
ticket.createdA ticket is opened (including AFTER_SALES claims)support.viewnumber, type, resellerId, storeName, orderNumber?, itemsCount?
payment.declaredDeclaration or cash collection PENDING_VERIFICATIONfinance.viewnumber, method, amount, resellerId, source
export.ready · import.finishedOwn job finishedrequester onlyjobId, status, source or type, rowCount?
dashboard.invalidatedAn event changed KPI aggregates, or a layout changeddashboard.viewcards: string[], layout?: boolean
me.permissions_changedThe user's roles or a role's permissions changedthe userpermissionsVersion — re-fetch me.get
stream.resyncResume id too oldeveryonereason: "BUFFER_EXCEEDED"

Events added to the brief's original list (order.created, order.status_changed, visit.checked_in, visit.completed, visit.not_done, tour.started, tour.closed, delegate.position, stock.alert, account.pending, ticket.created, payment.declared): order.to_confirm, order.modified, return.requested, tour.suspended, tour.resumed, tour.reopened, tour.cancelled, export.ready, import.finished, dashboard.invalidated, me.permissions_changed, stream.resync.

order.created

{ "type": "order.created", "at": "2026-11-03T08:12:00Z", "entity": { "type": "Order", "id": "0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e", "number": "CMD-2026-000158" }, "payload": { "channel": "RESELLER_APP", "status": "SUBMITTED", "resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "storeName": "Phone Store El Bahia", "totalTtc": 3974600, "isFirstOrder": false } }

order.to_confirm

{ "type": "order.to_confirm", "at": "2026-11-03T08:12:00Z", "entity": { "type": "Order", "id": "0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e", "number": "CMD-2026-000158" }, "payload": { "status": "SUBMITTED", "kind": "NEW", "pendingReasons": [], "channel": "RESELLER_APP", "storeName": "Phone Store El Bahia", "totalTtc": 3974600, "controls": { "stock": "INSUFFICIENT", "price": "OK", "credit": "EXCEEDED", "address": "OK" }, "submittedAt": "2026-11-03T08:12:00Z" } }

order.status_changed

{ "type": "order.status_changed", "at": "2026-11-04T10:05:11Z", "entity": { "type": "Order", "id": "0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e", "number": "CMD-2026-000158" }, "payload": { "from": "PREPARING", "to": "READY", "deliveryStatus": "READY", "resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "actor": { "id": "01929e77-4e5f-7a6b-8c7d-8e9f0a1b2c3d", "displayName": "Lamia Kaci" } } }

order.modified

{ "type": "order.modified", "at": "2026-11-03T09:50:02Z", "entity": { "type": "Order", "id": "0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e", "number": "CMD-2026-000158" }, "payload": { "changesCount": 1, "totalTtcBefore": 3974600, "totalTtcAfter": 3074960 } }

return.requested

{ "type": "return.requested", "at": "2026-11-06T09:14:00Z", "entity": { "type": "ReturnRequest", "id": "01931a20-6b7c-7d8e-9f01-2a3b4c5d6e7f", "number": "RET-2026-000012" }, "payload": { "orderId": "01930170-1c2d-7e3f-8a4b-5c6d7e8f9a0b", "orderNumber": "CMD-2026-000157", "origin": "RESELLER", "itemsCount": 1, "amountTtc": 124950 } }

return.status_changed

{ "type": "return.status_changed", "at": "2026-11-06T11:02:00Z", "entity": { "type": "ReturnRequest", "id": "01931a20-6b7c-7d8e-9f01-2a3b4c5d6e7f", "number": "RET-2026-000012" }, "payload": { "orderId": "01930170-1c2d-7e3f-8a4b-5c6d7e8f9a0b", "resellerId": "0192f4d2-3e4f-7a5b-9c6d-7e8f9a0b1c2d", "from": "REQUESTED", "to": "APPROVED", "actor": "01929e77-4e5f-7a6b-8c7d-8e9f0a1b2c3d" } }

Projected from the outbox events return.approved, return.rejected, return.received and return.status_changed; Retours et SAV (BO-69) refreshes its queue and the open return on it instead of polling. actor is the id of the user who made the move (null for a system move such as the refund after the credit note).

visit.checked_in

{ "type": "visit.checked_in", "at": "2026-11-04T08:41:15Z", "entity": { "type": "Visit", "id": "01930a10-5e6f-7a8b-9c0d-1e2f3a4b5c6e", "number": "VIS-2026-001234" }, "payload": { "tourId": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "delegateId": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "distanceMeters": 63, "withinTolerance": true, "position": { "lat": 35.7209, "lng": -0.5446, "accuracy": 14 } } }

visit.completed

payload: { "tourId", "delegateId", "resellerId", "durationMinutes": 32, "interestLevel": "HIGH", "nextAction": "PHONE_FOLLOW_UP" }.

visit.not_done

payload: { "tourId", "delegateId", "resellerId", "reasonCode": "STORE_CLOSED", "priority": "NORMAL" }.

tour.started

payload: { "delegateId", "date": "2026-11-04", "startPosition": { "lat": 35.6969, "lng": -0.6331 }, "stops": 9 }.

tour.suspended

payload: { "delegateId", "reasonCode": "BREAK" }.

tour.resumed

payload: { "delegateId" }.

tour.closed

payload: { "delegateId", "visitsDone": 8, "visitsPlanned": 9, "km": 46.3, "revenueHt": 3080000 }.

tour.reopened

payload: { "delegateId", "reason": "Rapport de visite manquant." }.

tour.cancelled

payload: { "delegateId", "reason": "Délégué en arrêt maladie." }.

delegate.position

{ "type": "delegate.position", "at": "2026-11-04T10:09:31Z", "entity": { "type": "User", "id": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12" }, "payload": { "delegateId": "0192a7d3-1b2c-7d4e-8f5a-6b7c8d9e0f12", "tourId": "01930a10-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "position": { "lat": 35.6923, "lng": -0.5871, "accuracy": 11, "capturedAt": "2026-11-04T10:09:30Z" }, "progress": { "done": 4, "notDone": 1, "remaining": 4 } } }

stock.alert

{ "type": "stock.alert", "at": "2026-11-03T09:52:31Z", "entity": { "type": "ProductVariant", "id": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f" }, "payload": { "variantId": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "warehouseCode": "WH-ORN", "level": "OUT", "available": 0, "threshold": 20 } }

account.pending

payload: { "resellerId", "number": "REV-000245", "storeName": "Mobile Center Hydra", "kind": "SELF_SIGNUP", "declaredCustomerType": "DETAIL", "hasDuplicateHints": true }.

ticket.created

{ "type": "ticket.created", "at": "2026-11-10T09:12:00Z", "entity": { "type": "Ticket", "id": "01931b30-7c8d-7e9f-8a01-3b4c5d6e7f80", "number": "TKT-2026-000045" }, "payload": { "type": "AFTER_SALES", "resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "storeName": "Phone Store El Bahia", "orderNumber": "CMD-2026-000158", "itemsCount": 1 } }

payment.declared

payload: { "number": "PAY-2026-000079", "method": "CCP", "amount": 1074960, "resellerId", "source": "RESELLER_DECLARATION" }.

export.ready

payload: { "jobId", "status": "SUCCEEDED", "source": { "kind": "LIST", "key": "resellers" }, "rowCount": 94 }.

import.finished

payload: { "jobId", "status": "SUCCEEDED", "type": "PRODUCTS", "mode": "DRY_RUN" }.

dashboard.invalidated

payload: { "cards": ["revenue.today", "orders.byStatus", "orders.toConfirm"], "layout": false }.

me.permissions_changed

payload: { "permissionsVersion": 8 } — re-fetch me.get and re-render navigation.

stream.resync

payload: { "reason": "BUFFER_EXCEEDED" } — re-fetch every open list and detail.

Sample transcript

GET /v1/admin/events HTTP/1.1
Accept: text/event-stream
Cookie: hoco.session_token=…
Last-Event-ID: 1847250

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-store

retry: 3000

id: 1847251
event: order.created
data: {"type":"order.created","at":"2026-11-03T08:12:00Z","entity":{"type":"Order","id":"0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e","number":"CMD-2026-000158"},"payload":{"channel":"RESELLER_APP","status":"SUBMITTED","resellerId":"0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a","storeName":"Phone Store El Bahia","totalTtc":3974600,"isFirstOrder":false}}

id: 1847252
event: order.to_confirm
data: {"type":"order.to_confirm","at":"2026-11-03T08:12:00Z","entity":{"type":"Order","id":"0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e","number":"CMD-2026-000158"},"payload":{"status":"SUBMITTED","kind":"NEW","pendingReasons":[],"channel":"RESELLER_APP","storeName":"Phone Store El Bahia","totalTtc":3974600,"controls":{"stock":"INSUFFICIENT","price":"OK","credit":"EXCEEDED","address":"OK"},"submittedAt":"2026-11-03T08:12:00Z"}}

: ping

id: 1847260
event: dashboard.invalidated
data: {"type":"dashboard.invalidated","at":"2026-11-03T08:12:05Z","entity":{"type":"Dashboard","id":"01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b"},"payload":{"cards":["orders.toConfirm","orders.byStatus"],"layout":false}}

: ping

On this page