HOCO B2B
SRSAPI referenceBack-office endpoints

Reports & exports

Back-office report catalogue with the columns and filters of every essential report, report runs with totals, and asynchronous PDF, Excel and CSV exports.

These operations deliver the essential reports of cahier §4 (« Indicateurs et rapports ») and the exports of cahier §9 (« PDF et Excel/CSV pour les listes et rapports autorisés »): the reports library (BO-59) and the export centre (BO-60) — ANA-03 to ANA-05.

Definitions are those of the dashboard card catalogue: revenue is HT, net of discounts, without delivery fees, counted on validatedAt, minus credit notes; orders and revenue are attributed to the delegate snapshotted at submission (BR-49); business days in Africa/Algiers.

PermissionUnlocks on this page
reports.viewReport catalogue and runs (plus the extra permission of each report)
reports.exportExport jobs of reports; list exports also need the module's .export key
pricing.viewMargin and stock-value columns

Audit. Report runs are not journaled; every export request writes EXPORT (source, format, filters; row count on completion).

MethodPathOperationAccessAppsRelease
GET/admin/reportsadmin.reports.list
Report catalogue available to my role
reports.viewBOV1
GET/admin/reports/{reportKey}admin.reports.run
Run a report with filters (paginated rows + totals)
reports.viewBOV1
POST/admin/exportsadmin.exports.create
Request an export (PDF, XLSX, CSV) of a list or report
reports.exportBOV1
GET/admin/exportsadmin.exports.list
My exports and their status
reports.exportBOV1
GET/admin/exports/{exportId}admin.exports.get
Export status and download URL
reports.exportBOV1

Report catalogue

KeyColumnsFiltersExtra permission
sales_by_periodperiod, orders, units, revenueHt, discountHt, vat, revenueTtc, averageBasketHt, resellersfrom, to*, granularity (DAY/WEEK/MONTH), wilayaCode[], brandId[], categoryId[], channel[], delegateId[], customerType[]—
sales_by_zonezone / wilaya / commune, orders, revenueHt, shareBps, activeResellers, averageBasketHt, deltaBpsfrom, to*, level (ZONE/WILAYA/COMMUNE), brandId[], categoryId[]—
sales_by_delegatedelegate, zones, orders, assistedOrders, revenueHt, firstOrders, newResellers, averageBasketHtfrom, to*, zoneId, delegateId[]—
sales_by_resellernumber, storeName, wilaya, commune, customerType, delegate, orders, revenueHt, averageBasketHt, lastOrderAt, activity, balanceDue (finance.view)from, to*, wilayaCode[], delegateId[], segmentId[], customerType[], activity—
sales_by_brandbrand, units, revenueHt, shareBps, deltaBpsfrom, to*, wilayaCode[], customerType[]—
sales_by_productproduct, sku, brand, category, units, orders, resellers, revenueHt, avgUnitPriceHt, marginHt (pricing.view), shareBpsfrom, to*, brandId[], categoryId[], wilayaCode[]—
prospect_conversioncohort (period of creation), source, delegate, prospects, contacted, interested, appInstalled, accountsCreated, firstOrders, conversionBps, medianDaysToFirstOrderfrom, to*, delegateId[], source[], zoneIdcrm.view
first_ordersorderNumber, date, reseller, delegate, channel, totalHt, daysSinceSignup, assistedfrom, to*, delegateId[], channel[]—
stockoutssku, product, warehouse, outSince, daysOut, lastReceiptAt, refusedOrModifiedLines, availableNowfrom, to*, warehouseId, brandId[], categoryId[]stock.view
slow_moverssku, product, physical, unitsSold, lastSoldAt, daysOfCover, stockValueHt (pricing.view)from, to*, warehouseId, brandId[], categoryId[], maxUnitsSold, minDaysSinceLastSalestock.view
orders_pending_late_cancelled_returnedorderNumber, reseller, status, bucket, submittedAt, ageHours, delegate, totalTtc, reason, channelfrom, to*, bucket[] (PENDING/LATE/CANCELLED/RETURNED), channel[], wilayaCode[]orders.view
field_activitydelegate, day, toursPlanned, toursCompleted, visitsPlanned, visitsDone, visitsNotDone, completionBps, avgVisitMinutes, km, signups, installs, firstOrders, outOfToleranceCheckInsfrom, to*, delegateId[], zoneIdfield.view
payments_expectedinvoiceNumber, reseller, dueDate, totalTtc, paidAmount, balance, status, daysOverdue, methodfrom, to*, status[], resellerId, agingBucketfinance.view

* required. LATE = waiting for confirmation longer than orders.confirmationReminderHours, or validated and not shipped within delivery.shippingSlaHours. Column types: STRING, INTEGER, MONEY, BPS, DATE, DATETIME, DECIMAL.

GET /admin/reports

admin.reports.list · Permission reports.view · Apps BO · Release V1 · Requirements ANA-03

Purpose. The reports available to the caller, with their columns and filters (the library renders forms from it).

Response 200 OK

{
  "items": [
    {
      "key": "sales_by_zone", "title": "Chiffre d'affaires par zone", "category": "SALES", "exportFormats": ["XLSX", "CSV", "PDF"],
      "columns": [{ "key": "label", "type": "STRING" }, { "key": "orders", "type": "INTEGER" }, { "key": "revenueHt", "type": "MONEY", "sortable": true }, { "key": "shareBps", "type": "BPS" }, { "key": "activeResellers", "type": "INTEGER" }, { "key": "averageBasketHt", "type": "MONEY" }, { "key": "deltaBps", "type": "BPS" }],
      "filters": [{ "key": "from", "type": "DATE", "required": true }, { "key": "to", "type": "DATE", "required": true }, { "key": "level", "type": "ENUM", "values": ["ZONE", "WILAYA", "COMMUNE"] }, { "key": "brandId", "type": "UUID", "multiple": true }],
      "defaultSort": { "field": "revenueHt", "order": "desc" }
    }
  ]
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks reports.view

Rules & side effects

  1. Reports whose extra permission the caller lacks are omitted; gated columns are omitted from columns. Titles are localised by Accept-Language (report keys are not enum values).

Used by BO-59

GET /admin/reports/{reportKey}

admin.reports.run · Permission reports.view (+ the report's extra permission) · Apps BO · Release V1 · Requirements ANA-03 · ANA-04

Purpose. Runs a report with filters: paginated rows plus totals.

Request — the report's filters as query parameters, plus sort, order, page, pageSize (max 200).

Response 200 OK — sales_by_zone?from=2026-11-01&to=2026-11-15&level=WILAYA

{
  "report": { "key": "sales_by_zone", "columns": ["label", "orders", "revenueHt", "shareBps", "activeResellers", "averageBasketHt", "deltaBps"] },
  "filters": { "from": "2026-11-01", "to": "2026-11-15", "level": "WILAYA" },
  "items": [
    { "key": "31", "label": "Oran", "orders": 61, "revenueHt": 112840000, "shareBps": 5263, "activeResellers": 88, "averageBasketHt": 1849836, "deltaBps": 1210 },
    { "key": "16", "label": "Alger", "orders": 38, "revenueHt": 71650000, "shareBps": 3342, "activeResellers": 54, "averageBasketHt": 1885526, "deltaBps": -340 }
  ],
  "total": 9, "page": 1, "pageSize": 25,
  "totals": { "orders": 118, "revenueHt": 214400000, "activeResellers": 172 },
  "generatedAt": "2026-11-15T09:40:00Z", "dataAsOf": "2026-11-15T09:39:52Z"
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDMissing required filter, unknown filter or sort column
403PERMISSION_DENIEDMissing reports.view or the report's extra permission
404NOT_FOUNDUnknown report key
422REPORT_RANGE_TOO_LARGERange over 366 days (DAY granularity: 92) — details: { maxDays }

Rules & side effects

  1. Read-only; served from analytics aggregates (dataAsOf); totals cover the whole filtered set. Results cached 60 s per parameters and permission set.

Used by BO-59 · BO-05

Exports

ExportJob

interface ExportJob {
  id: Uuid;
  source: { kind: "LIST" | "REPORT"; key: string };   // LIST: resellers, orders, products, stock-levels, stock-movements, invoices, payments, visits, tours, tickets, audit-logs, delegates · REPORT: catalogue keys
  format: "PDF" | "XLSX" | "CSV";                    // ExportFormat
  status: "QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED";   // JobStatus
  filters: Record<string, unknown>;
  columns?: string[];
  rowCount?: number;
  progress?: number;                                  // 0–100
  file?: FileRef;                                     // GENERATED_DOCUMENT; signed URL (5 min) re-issued on every GET
  error?: { code: string; message: string };
  requestedBy: UserSummary;
  createdAt: Iso; startedAt?: Iso; finishedAt?: Iso;
  expiresAt?: Iso;                                    // finishedAt + exports.retentionDays (7)
}

Limits: XLSX/CSV ≤ 100 000 rows, PDF ≤ 2 000 rows (422 EXPORT_TOO_LARGE); at most 3 running exports per user (429 RATE_LIMITED). Lifecycle: JobStatus.

POST /admin/exports

admin.exports.create · Permission reports.export (LIST sources: the module's .export) · Apps BO · Release V1 · Requirements ANA-05

Purpose. Requests a PDF, Excel or CSV export of a list or a report.

Request

{ "source": { "kind": "REPORT", "key": "sales_by_zone" }, "format": "XLSX", "filters": { "from": "2026-11-01", "to": "2026-11-15", "level": "WILAYA" } }

Response 202 Accepted — an ExportJob (status QUEUED).

LIST keyPermission
resellerscrm.export
ordersorders.export
productscatalog.export
stock-levels, stock-movementsstock.export
invoices, paymentsfinance.export
visits, tours, delegatesfield.export
ticketsreports.export
audit-logsaudit.export

Errors

StatusCodeWhen
400VALIDATION_FAILEDUnknown source, filter or column
403PERMISSION_DENIEDMissing export permission, or a gated column requested
422EXPORT_TOO_LARGEdetails: { rowCount, maxRows, format }
429RATE_LIMITED3 exports already running

Rules & side effects

  1. The file contains only what the requester may see. Audit EXPORT. On completion: export.ready (in-app; email when it took over 2 minutes) and SSE export.ready.

Used by BO-59 · BO-60 · every list screen's export button

GET /admin/exports

admin.exports.list · Permission reports.export · Apps BO · Release V1 · Requirements ANA-05

Purpose. My exports and their status (the export centre). Request — status[], page / pageSize. Response 200 OK — Page<ExportJob> (own jobs only).

Errors

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks reports.export

Rules & side effects

  1. Read-only. Any user who can export (module .export keys) sees their own jobs — Assumption.

Used by BO-60

GET /admin/exports/{exportId}

admin.exports.get · Permission reports.export · Apps BO · Release V1 · Requirements ANA-05

Purpose. Export status and a fresh download URL.

Response 200 OK

{
  "id": "01930b40-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "source": { "kind": "LIST", "key": "resellers" }, "format": "XLSX", "status": "SUCCEEDED",
  "filters": { "wilayaCode": ["31"], "activity": "INACTIVE" }, "rowCount": 94, "progress": 100,
  "file": { "id": "01930b41-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "url": "https://files.hoco.dz/s/export-resellers-20261104.xlsx?sig=…", "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "size": 28412, "purpose": "GENERATED_DOCUMENT" },
  "requestedBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" },
  "createdAt": "2026-11-04T09:12:00Z", "startedAt": "2026-11-04T09:12:01Z", "finishedAt": "2026-11-04T09:12:04Z", "expiresAt": "2026-11-11T09:12:04Z"
}

Errors

StatusCodeWhen
403PERMISSION_DENIEDNot the requester
404NOT_FOUNDUnknown export
410EXPORT_EXPIREDFile purged after expiresAt

Rules & side effects

  1. file is present only when SUCCEEDED; each GET issues a new 5-minute signed URL.

Used by BO-60

On this page