HOCO B2B
SRSAPI reference

Prospects, points of sale, follow-ups & invitations

The delegate's CRM in the field — scoped list and map of points of sale, duplicate check, offline prospect creation, point-of-sale sheet and edits, notes and photos, invitations with activation tracking, and follow-up tasks.

These operations cover the field side of the CRM (cahier §4.2, §6.5, §6.6, §6.8): finding the points of sale (points de vente) the delegate works with, registering new prospects on the spot, keeping their record up to date, inviting them to install the reseller app and following up. The lifecycle rules of a reseller are owned by Lifecycles — Reseller; the back-office counterpart is CRM.

Offline behaviour. The app keeps every point of sale of its scope as a ResellerCard in its local database (kept fresh by sync.delta) and the full sheet of today's stops (from the today snapshot). Creating a prospect, editing a record, adding a note or a photo, sharing an invitation and handling tasks are offline-capable: applied locally, queued with occurredAt and an Idempotency-Key, replayed through sync.batch. Creations carry a client-generated UUIDv7 id, so items queued after a creation (photo, note, unplanned visit) reference it and declare it in dependsOn. Three operations need the network: the duplicate check (check-phone, the app falls back to its local cache), invitation creation (the code is server-generated) and the reads of fresh sheets and invitation steps.

Scope rule

A delegate D sees and acts on a point of sale R only when R is in D's delegate scope, evaluated by the server on every call (ACL-03, cahier §3.1 « Les délégués voient uniquement leur zone, leurs tournées, leurs revendeurs attribués »):

ClauseConditionAllowed
(a) AssignedR.assignedDelegateId = D — any status, any communeRead, edit, notes, photos, invitations, tasks, assisted orders, visits
(b) Unassigned prospect of my zonesR.assignedDelegateId is null and R.zoneId is one of D's zones (zone derived from the commune, BR-46) and R.status is a qualification status — NEW_PROSPECT, TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UPSame as (a) — visible so that delegates pick prospects up instead of creating duplicates
(c) Planned in my toursR is the point of sale of a visit of a tour of D dated from today − 7 days to today + 30 days (any tour status) — covers replacements during leaveSame as (a)

This is the decided rule of the permissions matrix. Everything else is out of scope — in particular a record assigned to another delegate, even inside D's zones (unless planned in D's tours), and unassigned records beyond the qualification statuses (the back-office assigns them). A prospect created by D is assigned to D at creation (BR-47), so it falls under (a); a reassignment cancels the store's future PLANNED visits in D's tours (BR-48). Out-of-scope records answer 404 NOT_FOUND (existence is not leaked); a record that was in D's scope (D created it or was assigned to it before) answers 403 OUT_OF_SCOPE, so an offline action replayed after a reassignment is explained in the sync centre. The duplicate check is the only place where D learns that a phone number exists outside the scope, without any detail.

MethodPathOperationAccessAppsRelease
GET/delegate/resellersdelegate.resellers.list
Points of sale and prospects in my scope: search, status filter, distance sort
delegateDAV1
GET/delegate/resellers/check-phonedelegate.resellers.checkPhone
Duplicate check before creating a prospect
delegateDAV1
POST/delegate/resellersdelegate.resellers.create
Create a prospect/reseller in the field (client-generated id; offline-safe)
delegate
Idempotency-Key
DAV1
GET/delegate/resellers/{resellerId}delegate.resellers.get
Point-of-sale sheet: details, status, recent visits, orders summary, open tasks, photos
delegateDAV1
PATCH/delegate/resellers/{resellerId}delegate.resellers.update
Update a prospect/reseller in my scope (status changes per allowed transitions)
delegate
Idempotency-Key
DAV1
POST/delegate/resellers/{resellerId}/notesdelegate.resellers.addNote
Add a note or call log to the interaction timeline
delegate
Idempotency-Key
DAV1
POST/delegate/resellers/{resellerId}/photosdelegate.resellers.addPhoto
Attach a façade/interior photo
delegate
Idempotency-Key
DAV1
POST/delegate/resellers/{resellerId}/invitationsdelegate.invitations.create
Create (or return the active) invitation: code, link, QR payload, share texts
delegate
Idempotency-Key
DAV1
GET/delegate/invitations/{invitationId}delegate.invitations.get
Invitation with its activation steps
delegateDAV1
POST/delegate/invitations/{invitationId}/shareddelegate.invitations.markShared
Record that the invitation was shared and through which channel
delegate
Idempotency-Key
DAV1
GET/delegate/tasksdelegate.tasks.list
My follow-ups and tasks (due today, overdue, upcoming)
delegateDAV1
PATCH/delegate/tasks/{taskId}delegate.tasks.update
Reschedule or annotate a task
delegate
Idempotency-Key
DAV1
POST/delegate/tasks/{taskId}/completedelegate.tasks.complete
Complete a task with an outcome
delegate
Idempotency-Key
DAV1

Conventions on this page

  • Caller: delegate session; mobile headers on every call.
  • Mutations require Idempotency-Key and occurredAt (device time; refused with 422 OCCURRED_AT_OUT_OF_RANGE when more than 5 minutes ahead or older than 72 hours).
  • Errors common to every operation, not repeated: 400 VALIDATION_FAILED, 400 IDEMPOTENCY_KEY_REQUIRED (mutation without the header), 401 UNAUTHENTICATED, 403 ACCOUNT_SUSPENDED, 403 PERMISSION_DENIED, 404 NOT_FOUND, 409 IDEMPOTENCY_KEY_REUSED, 409 IDEMPOTENCY_IN_PROGRESS, 422 OCCURRED_AT_OUT_OF_RANGE (mutations), 426 UPGRADE_REQUIRED, 429 RATE_LIMITED, 500 INTERNAL_ERROR, 503 MAINTENANCE.
  • Phones may be sent as typed (0558 12 34 56, 00213…, +213…); they are normalised to E.164 and returned that way.

Shared shapes of this page

type ResellerStatus = "NEW_PROSPECT" | "TO_CONTACT" | "INTERESTED" | "APP_INSTALLED" | "ACCOUNT_CREATED"
  | "FIRST_ORDER" | "ACTIVE" | "NOT_INTERESTED" | "TO_FOLLOW_UP" | "SUSPENDED";
type CustomerType = "SUPER_GROS" | "DEMI_GROS" | "DETAIL";

// What a list row, a map pin or a tour stop needs — kept for the whole scope in the local database.
interface ResellerCard extends ResellerSummary {
  addressLine: string;
  landmark: string | null;
  whatsapp: string | null;
  businessTypeCode: string | null;      // BUSINESS_TYPE item
  potential: "LOW" | "MEDIUM" | "HIGH" | null;
  customerType: CustomerType | null;    // declared or proposed; null when unknown
  hasAccount: boolean;                  // a reseller user exists for this record
  assignedToMe: boolean;
  facadeThumbnailUrl: string | null;    // signed, 5 minutes
  lastVisitAt: Iso | null;              // last visit DONE, by anyone
}

// The point-of-sale sheet (DA-09).
interface ResellerSheet extends ResellerCard {
  email: string | null;
  wilayaName: string;
  communeId: Uuid;
  locationSource: "FIELD_SIGNUP" | "CHECK_IN_VALIDATED" | "BACK_OFFICE" | "IMPORT" | null;
  locationCandidate: GeoPoint | null;   // proposed by a check-in, awaiting back-office validation
  validationStatus: "PENDING" | "APPROVED" | "REJECTED";
  customerTypeConfirmed: boolean;       // false while only proposed (confirmed at validation, AUTH-11)
  source: "DELEGATE_APP" | "BACK_OFFICE" | "SELF_SIGNUP" | "IMPORT";
  accountStatus: "PENDING_VALIDATION" | "ACTIVE" | "SUSPENDED" | "DISABLED" | null;
  assignedDelegate: UserSummary | null;
  createdBy: UserSummary | null;
  createdAt: Iso;
  zone: { id: Uuid; name: string } | null;
  facadePhoto: FileRef | null;
  photos: ResellerPhotoView[];          // latest 10
  lastVisits: Array<{ visitId: Uuid; number: string; endedAt: Iso; interestLevel: "LOW" | "MEDIUM" | "HIGH" | null; nextAction: string | null; by: UserSummary }>;   // last 5
  ordersSummary: { count: number; lastOrderAt: Iso | null; totalTtc90d: Money };   // no purchase prices, no credit
  openTasks: TaskView[];
  recentInteractions: InteractionView[];   // last 10, newest first
  invitation: { id: Uuid; displayCode: string; active: boolean; currentStep: string | null } | null;
  allowedStatusTransitions: ResellerStatus[];   // what this caller may set now
  editableFields: string[];             // fields this caller may PATCH now
  scopeClause: "ASSIGNED" | "ZONE_PROSPECT" | "TOUR_STOP";
  version: number;                      // optimistic lock on the editable fields
  updatedAt: Iso;
}

interface ResellerPhotoView {
  id: Uuid;
  fileId: Uuid;
  kind: "FACADE" | "INTERIOR" | "SHELF" | "OTHER";
  url: string;
  thumbnailUrl: string;
  takenAt: Iso;
  location: GeoPoint | null;
  caption: string | null;
  isCurrentFacade: boolean;
  uploadedBy: UserSummary;
  createdAt: Iso;
}

interface InteractionView {
  id: Uuid;
  type: "CALL" | "VISIT" | "NOTE" | "WHATSAPP" | "SMS" | "EMAIL" | "ORDER" | "PAYMENT" | "CLAIM" | "STATUS_CHANGE";
  body: string | null;
  occurredAt: Iso;
  author: UserSummary | null;           // null = system
  ref: { kind: "VISIT" | "ORDER" | "TASK" | "TICKET"; id: Uuid; number: string | null } | null;
}

interface TaskView {
  id: Uuid;
  type: "CALL" | "VISIT" | "WHATSAPP" | "SEND_CATALOG" | "OTHER";
  status: "OPEN" | "DONE" | "CANCELLED";
  title: string;
  dueAt: Iso;
  isOverdue: boolean;                   // derived: OPEN and dueAt in the past
  reseller: ResellerSummary;
  sourceVisitId: Uuid | null;
  note: string | null;
  createdBy: UserSummary | null;        // null = system (inactivity scan)
  createdAt: Iso;
  completedAt: Iso | null;
  outcome: string | null;
  version: number;
}

Purchase prices, margins, credit limits and balances are never part of these shapes (ACL-04).

GET /delegate/resellers

delegate.resellers.list · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key — · Offline-capable no — offline, the app runs the same filters on its local cards · Release V1 · Requirements CRM-04, ACL-03

Purpose. Prospects: the list and the map of the points of sale in scope, with search, status filters and distance sort.

Request. Query parameters:

NameTypeRule
qstring, 2–60 charsMatches store name, owner name, REV- number and phone digits (accent- and case-insensitive)
statusResellerStatus, repeatableDefault: all
assignmentMINE · UNASSIGNEDMINE = clause (a), UNASSIGNED = clause (b); default both, plus (c)
nearlat,lngReference point for distances (device position or map centre)
radiusKmnumber, 0.1–50Requires near; default 10
sortdistance · name · lastVisitdistance requires near (default when near is set), else name; lastVisit = least recently visited first
cursor, limitDefault 20, max 100

Response 200 OK — cursor list of ResellerListItem.

interface ResellerListItem extends ResellerCard {
  distanceMeters: number | null;        // straight line from `near`
  validationStatus: "PENDING" | "APPROVED" | "REJECTED";
  openTasksCount: number;
  nextTaskDueAt: Iso | null;
}

Example: GET /v1/delegate/resellers?near=35.71143,-0.57262&radiusKm=3&sort=distance

{
  "items": [
    {
      "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b123",
      "number": "REV-000123",
      "storeName": "Magasin El Wafa",
      "ownerName": "Karim Belkacem",
      "phone": "+213555123456",
      "wilayaCode": "31",
      "communeName": "Bir El Djir",
      "status": "ACTIVE",
      "location": { "lat": 35.7112, "lng": -0.5724 },
      "addressLine": "Hai Sabah, cité 300 logements, local n° 4",
      "landmark": "En face de la mosquée El Feth",
      "whatsapp": "+213555123456",
      "businessTypeCode": "PHONE_SHOP",
      "potential": "HIGH",
      "customerType": "DEMI_GROS",
      "hasAccount": true,
      "assignedToMe": true,
      "facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b123-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=91aa",
      "lastVisitAt": "2026-11-03T08:41:00Z",
      "distanceMeters": 32,
      "validationStatus": "APPROVED",
      "openTasksCount": 1,
      "nextTaskDueAt": "2026-11-10T08:00:00Z"
    },
    {
      "id": "0193a31c-8e2f-7a40-b1c2-d3e4f5a6b342",
      "number": "REV-000342",
      "storeName": "Phone Center Nour",
      "ownerName": "Mohamed Benali",
      "phone": "+213558123456",
      "wilayaCode": "31",
      "communeName": "Bir El Djir",
      "status": "NEW_PROSPECT",
      "location": { "lat": 35.7071, "lng": -0.5802 },
      "addressLine": "Hai Sabah, rue des Frères Benaouda",
      "landmark": null,
      "whatsapp": "+213558123456",
      "businessTypeCode": "PHONE_SHOP",
      "potential": "MEDIUM",
      "customerType": "DETAIL",
      "hasAccount": false,
      "assignedToMe": true,
      "facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0193a31c-b342-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=e5c9",
      "lastVisitAt": null,
      "distanceMeters": 837,
      "validationStatus": "PENDING",
      "openTasksCount": 0,
      "nextTaskDueAt": null
    },
    {
      "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
      "number": "REV-000287",
      "storeName": "Electro Plus",
      "ownerName": "Sofiane Medjahed",
      "phone": "+213661457812",
      "wilayaCode": "31",
      "communeName": "Bir El Djir",
      "status": "INTERESTED",
      "location": { "lat": 35.7236, "lng": -0.5513 },
      "addressLine": "Cité 200 logements, bloc C",
      "landmark": null,
      "whatsapp": "+213661457812",
      "businessTypeCode": "ELECTRONICS_STORE",
      "potential": "MEDIUM",
      "customerType": "DETAIL",
      "hasAccount": false,
      "assignedToMe": true,
      "facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b287-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=41d0",
      "lastVisitAt": "2026-10-27T10:15:00Z",
      "distanceMeters": 2353,
      "validationStatus": "APPROVED",
      "openTasksCount": 1,
      "nextTaskDueAt": "2026-11-03T08:00:00Z"
    }
  ],
  "nextCursor": null
}

Errors — 400 VALIDATION_FAILED (sort=distance or radiusKm without near, near malformed, q too short).

Rules & side effects. Read-only. Distances are Haversine; records without coordinates are excluded when near is given and listed last otherwise. The map view clusters pins on the device.

Used by DA-13.

GET /delegate/resellers/check-phone

delegate.resellers.checkPhone · Access session, delegate scope · Apps DA · Idempotency-Key — · Offline-capable no — offline, the app checks its local cards and the server repeats the check at creation (PHONE_ALREADY_EXISTS) · Release V1 · Requirements CRM-03

Purpose. Tell the delegate, while typing the phone of a new store (« Le téléphone doit être unique », cahier §6.5), whether the number is already known — before they fill the whole form.

Request. Query phone (any accepted format). Rate limit: 30 calls per minute per user.

Response 200 OK

interface CheckPhoneResult {
  phone: string;                        // normalised E.164
  maskedPhone: string;                  // "05•• •• •• 56" — shown when the record is out of scope
  available: boolean;
  existing: ResellerSummary | null;     // only when the record is in the caller's scope
  assignedToMe: boolean;
  inMyScope: boolean;
}
{
  "phone": "+213558123456",
  "maskedPhone": "05•• •• •• 56",
  "available": true,
  "existing": null,
  "assignedToMe": false,
  "inMyScope": false
}

When the number belongs to a record outside the scope: available: false, existing: null, inMyScope: false — the app says « Ce numéro est déjà enregistré — contactez le back-office » with maskedPhone, and offers no creation (BR-04).

Errors — 400 VALIDATION_FAILED (phone is not an Algerian mobile number — 9 national digits starting with 5, 6 or 7, because the main phone receives OTPs: details.fields[0].code = "INVALID_PHONE").

Rules & side effects. Read-only; the lookup uses the normalised main phone (unique index). WhatsApp numbers are not unique and not checked. Each call is logged for abuse detection (enumeration).

Used by DA-14.

POST /delegate/resellers

delegate.resellers.create · Access session, delegate scope (the commune may be outside the caller's zones; the record is assigned to the caller) · Apps DA · Idempotency-Key required · Offline-capable yes — client-generated id, created locally (the new card is usable at once: unplanned visit, notes, photos), queued and replayed; a duplicate phone found at replay comes back as a CONFLICT · Release V1 · Requirements FLD-11, CRM-02, CRM-03, AUTH-11

Purpose. « Nouveau revendeur »: register a store on the spot (cahier §6.5) with the minimum data of cahier §15.5, its GPS position and a façade photo; the record waits for back-office validation.

Field sign-up: store, owner, phone, WhatsApp, wilaya, commune, address, business type, potential, GPS position, façade photo and initial status.
Field sign-up: store, owner, phone, WhatsApp, wilaya, commune, address, business type, potential, GPS position, façade photo and initial status. — client reference (cahier des charges)

Request. Body:

interface CreateProspectRequest {
  id: Uuid;                             // client-generated UUIDv7
  storeName: string;                    // 2–120
  ownerName: string;                    // 2–120
  phone: string;                        // main phone — unique after normalisation
  whatsapp?: string | null;
  email?: string | null;
  wilayaCode: string;                   // "31"
  communeId: Uuid;                      // must belong to the wilaya
  addressLine: string;                  // 3–200
  landmark?: string | null;             // ≤ 120
  location: GeoPoint | null;            // required unless locationUnavailableReason is given
  locationUnavailableReason?: "PERMISSION_DENIED" | "NO_FIX" | null;
  businessTypeCode: string;             // BUSINESS_TYPE item
  potential: "LOW" | "MEDIUM" | "HIGH";
  customerType?: CustomerType | null;   // proposed pricing tier, confirmed at validation
  facadePhotoFileId?: Uuid | null;      // completed upload, purpose STORE_PHOTO
  status: "NEW_PROSPECT" | "TO_CONTACT" | "INTERESTED";
  note?: string | null;                 // ≤ 1000 — first entry of the timeline
  visitId?: Uuid | null;                // the visit during which the store was registered, if any
  occurredAt: Iso;
}
{
  "id": "0193a31c-8e2f-7a40-b1c2-d3e4f5a6b342",
  "storeName": "Phone Center Nour",
  "ownerName": "Mohamed Benali",
  "phone": "0558 12 34 56",
  "whatsapp": "0558 12 34 56",
  "email": null,
  "wilayaCode": "31",
  "communeId": "0192b7c0-31aa-7a00-8b00-000000310004",
  "addressLine": "Hai Sabah, rue des Frères Benaouda",
  "landmark": null,
  "location": { "lat": 35.7071, "lng": -0.5802, "accuracy": 8, "capturedAt": "2026-11-03T09:03:40Z" },
  "businessTypeCode": "PHONE_SHOP",
  "potential": "MEDIUM",
  "customerType": "DETAIL",
  "facadePhotoFileId": "0193a31c-6d7e-7f80-9a91-b2c3d4e5f601",
  "status": "NEW_PROSPECT",
  "note": "Boutique ouverte il y a 3 mois, vend surtout des coques et des câbles sans marque.",
  "visitId": null,
  "occurredAt": "2026-11-03T09:05:00Z"
}

Response 201 Created (200 OK when the same id already exists with the same content) — the ResellerSheet.

{
  "id": "0193a31c-8e2f-7a40-b1c2-d3e4f5a6b342",
  "number": "REV-000342",
  "storeName": "Phone Center Nour",
  "ownerName": "Mohamed Benali",
  "phone": "+213558123456",
  "wilayaCode": "31",
  "communeName": "Bir El Djir",
  "status": "NEW_PROSPECT",
  "location": { "lat": 35.7071, "lng": -0.5802, "accuracy": 8, "capturedAt": "2026-11-03T09:03:40Z" },
  "addressLine": "Hai Sabah, rue des Frères Benaouda",
  "landmark": null,
  "whatsapp": "+213558123456",
  "businessTypeCode": "PHONE_SHOP",
  "potential": "MEDIUM",
  "customerType": "DETAIL",
  "hasAccount": false,
  "assignedToMe": true,
  "facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0193a31c-b342-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=e5c9",
  "lastVisitAt": null,
  "email": null,
  "wilayaName": "Oran",
  "communeId": "0192b7c0-31aa-7a00-8b00-000000310004",
  "locationSource": "FIELD_SIGNUP",
  "locationCandidate": null,
  "validationStatus": "PENDING",
  "customerTypeConfirmed": false,
  "source": "DELEGATE_APP",
  "accountStatus": null,
  "assignedDelegate": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "createdAt": "2026-11-03T09:05:00Z",
  "zone": { "id": "0192e8c4-1f2a-7d3b-8c4d-2e1f0a9b8c01", "name": "Oran" },
  "facadePhoto": { "id": "0193a31c-6d7e-7f80-9a91-b2c3d4e5f601", "url": "https://files.hoco.dz/store-photos/0193a31c-b342.webp?X-Amz-Expires=300&X-Amz-Signature=72fa", "thumbnailUrl": "https://files.hoco.dz/store-photos/0193a31c-b342-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=e5c9", "mimeType": "image/webp", "size": 191230, "purpose": "STORE_PHOTO" },
  "photos": [
    { "id": "0193a31c-6d7e-7f80-9a91-b2c3d4e5f6a1", "fileId": "0193a31c-6d7e-7f80-9a91-b2c3d4e5f601", "kind": "FACADE", "url": "https://files.hoco.dz/store-photos/0193a31c-b342.webp?X-Amz-Expires=300&X-Amz-Signature=72fa", "thumbnailUrl": "https://files.hoco.dz/store-photos/0193a31c-b342-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=e5c9", "takenAt": "2026-11-03T09:03:12Z", "location": { "lat": 35.7071, "lng": -0.5802, "accuracy": 8 }, "caption": null, "isCurrentFacade": true, "uploadedBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "createdAt": "2026-11-03T09:05:01Z" }
  ],
  "lastVisits": [],
  "ordersSummary": { "count": 0, "lastOrderAt": null, "totalTtc90d": 0 },
  "openTasks": [],
  "recentInteractions": [
    { "id": "0193a31c-8e30-7a41-b1c2-d3e4f5a6b3a1", "type": "NOTE", "body": "Boutique ouverte il y a 3 mois, vend surtout des coques et des câbles sans marque.", "occurredAt": "2026-11-03T09:05:00Z", "author": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "ref": null }
  ],
  "invitation": null,
  "allowedStatusTransitions": ["TO_CONTACT", "INTERESTED", "NOT_INTERESTED", "TO_FOLLOW_UP"],
  "editableFields": ["storeName", "ownerName", "phone", "whatsapp", "email", "wilayaCode", "communeId", "addressLine", "landmark", "location", "businessTypeCode", "potential", "customerType", "status"],
  "scopeClause": "ASSIGNED",
  "version": 1,
  "updatedAt": "2026-11-03T09:05:01Z"
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDRequired field missing or too long; commune not in the wilaya (COMMUNE_NOT_IN_WILAYA); unknown business type; file not a STORE_PHOTO of the caller
409PHONE_ALREADY_EXISTSThe normalised main phone exists (BR-04). details: { phone, maskedPhone, existing: ResellerSummary | null, assignedToMe: boolean } — existing is filled only when that record is in the caller's scope
409CLIENT_ID_CONFLICTid already used by another record
422LOCATION_REQUIREDlocation is null and no locationUnavailableReason given
422GPS_ACCURACY_TOO_LOWlocation.accuracy above field.gpsMaxAccuracyMeters (details: { accuracy, maxAccuracyMeters }) — the store's position is the reference for future check-ins
422FILE_NOT_UPLOADEDfacadePhotoFileId upload not completed
{
  "error": {
    "code": "PHONE_ALREADY_EXISTS",
    "message": "Ce numéro est déjà enregistré : Electro Plus (REV-000287).",
    "details": {
      "phone": "+213661457812",
      "maskedPhone": "06•• •• •• 12",
      "existing": {
        "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
        "number": "REV-000287",
        "storeName": "Electro Plus",
        "ownerName": "Sofiane Medjahed",
        "phone": "+213661457812",
        "wilayaCode": "31",
        "communeName": "Bir El Djir",
        "status": "INTERESTED"
      },
      "assignedToMe": true
    },
    "requestId": "req_01JBZ4N6P3"
  }
}

Rules & side effects

  1. Server-set fields: number (REV- sequence), source = DELEGATE_APP, createdByDelegateId = caller, assignedDelegateId = caller (BR-47), zone = the caller's zone covering the commune (or the zone of the commune when it lies outside the caller's zones — the record is then flagged « hors zone » for the validator), validationStatus = PENDING, locationSource = FIELD_SIGNUP (lifecycles).
  2. Customer type (AUTH-11): customerType is only a proposal (customerTypeConfirmed = false); the validator confirms or changes it in admin.resellers.validate. Until then, assisted-order prices use the proposed type (or DETAIL when none) with a warning.
  3. Façade photo: the form requires it (cahier §6.5, FLD-11), but the API accepts a record without facadePhotoFileId so that a slow upload never blocks the prospect; the photo then follows through delegate.resellers.addPhoto (kind = FACADE, dependsOn the creation), and the validation queue shows « photo de façade manquante » until it arrives.
  4. Without a position (locationUnavailableReason), the record is created with location = null and flagged for the validator; the first check-in proposes a position candidate (check-in algorithm).
  5. note becomes an Interaction NOTE; visitId links the creation to the visit for reports.
  6. Outbox reseller.created → SSE account.pending to validators (BO-13); counts toward the SIGNUPS objective and the home « Nouveaux prospects ». The record appears in the CRM with its source, delegate and position (cahier §6.5 acceptance criterion).

Used by DA-14.

GET /delegate/resellers/{resellerId}

delegate.resellers.get · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key — · Offline-capable no — today's stops are in the snapshot; other sheets show the cached card offline · Release V1 · Requirements CRM-01, CRM-07

Purpose. « Fiche point de vente »: contact actions (call, WhatsApp, itinerary), status, recent visits, orders summary, open tasks, photos, timeline and invitation state.

Request. Path resellerId.

Response 200 OK — ResellerSheet.

{
  "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
  "number": "REV-000287",
  "storeName": "Electro Plus",
  "ownerName": "Sofiane Medjahed",
  "phone": "+213661457812",
  "wilayaCode": "31",
  "communeName": "Bir El Djir",
  "status": "INTERESTED",
  "location": { "lat": 35.7236, "lng": -0.5513 },
  "addressLine": "Cité 200 logements, bloc C",
  "landmark": null,
  "whatsapp": "+213661457812",
  "businessTypeCode": "ELECTRONICS_STORE",
  "potential": "MEDIUM",
  "customerType": "DETAIL",
  "hasAccount": false,
  "assignedToMe": true,
  "facadeThumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b287-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=41d0",
  "lastVisitAt": "2026-11-03T09:58:00Z",
  "email": "electroplus.bej@gmail.com",
  "wilayaName": "Oran",
  "communeId": "0192b7c0-31aa-7a00-8b00-000000310004",
  "locationSource": "FIELD_SIGNUP",
  "locationCandidate": null,
  "validationStatus": "APPROVED",
  "customerTypeConfirmed": true,
  "source": "DELEGATE_APP",
  "accountStatus": null,
  "assignedDelegate": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "createdAt": "2026-10-27T10:02:00Z",
  "zone": { "id": "0192e8c4-1f2a-7d3b-8c4d-2e1f0a9b8c01", "name": "Oran" },
  "facadePhoto": { "id": "0192d1b0-7a1b-7c2d-8e3f-4a5b6c7d8e02", "url": "https://files.hoco.dz/store-photos/0192d1aa-b287.webp?X-Amz-Expires=300&X-Amz-Signature=5d21", "thumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b287-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=41d0", "mimeType": "image/webp", "size": 201554, "purpose": "STORE_PHOTO" },
  "photos": [
    { "id": "0192d1b0-7a1b-7c2d-8e3f-4a5b6c7d8ea2", "fileId": "0192d1b0-7a1b-7c2d-8e3f-4a5b6c7d8e02", "kind": "FACADE", "url": "https://files.hoco.dz/store-photos/0192d1aa-b287.webp?X-Amz-Expires=300&X-Amz-Signature=5d21", "thumbnailUrl": "https://files.hoco.dz/store-photos/0192d1aa-b287-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=41d0", "takenAt": "2026-10-27T10:00:30Z", "location": { "lat": 35.7235, "lng": -0.5512, "accuracy": 7 }, "caption": null, "isCurrentFacade": true, "uploadedBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "createdAt": "2026-10-27T10:02:05Z" }
  ],
  "lastVisits": [
    { "visitId": "0193a2f0-5b6c-7e8d-9f01-23456789ab12", "number": "VIS-2026-001232", "endedAt": "2026-11-03T09:58:00Z", "interestLevel": "HIGH", "nextAction": "WHATSAPP_MESSAGE", "by": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" } },
    { "visitId": "0193897b-2c3d-7e4f-9a50-6b7c8d9e0f31", "number": "VIS-2026-001108", "endedAt": "2026-10-27T10:15:00Z", "interestLevel": "MEDIUM", "nextAction": "NEW_VISIT", "by": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" } }
  ],
  "ordersSummary": { "count": 0, "lastOrderAt": null, "totalTtc90d": 0 },
  "openTasks": [
    { "id": "0193a330-1a2b-7c3d-8e4f-5a6b7c8d9e51", "type": "WHATSAPP", "status": "OPEN", "title": "Message WhatsApp — Electro Plus", "dueAt": "2026-11-04T09:00:00Z", "isOverdue": false, "reseller": { "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287", "number": "REV-000287", "storeName": "Electro Plus", "ownerName": "Sofiane Medjahed", "phone": "+213661457812", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "INTERESTED" }, "sourceVisitId": "0193a2f0-5b6c-7e8d-9f01-23456789ab12", "note": "Vérifier l'installation de l'application et la création du compte.", "createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "createdAt": "2026-11-03T09:52:10Z", "completedAt": null, "outcome": null, "version": 1 }
  ],
  "recentInteractions": [
    { "id": "0193a333-0b1c-7d2e-8f30-4a5b6c7d8e61", "type": "VISIT", "body": "Visite de prospection — intérêt élevé. Présentation des écouteurs M1 Pro et du pack chargeur + câble.", "occurredAt": "2026-11-03T09:58:00Z", "author": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" }, "ref": { "kind": "VISIT", "id": "0193a2f0-5b6c-7e8d-9f01-23456789ab12", "number": "VIS-2026-001232" } },
    { "id": "0193a333-0b1c-7d2e-8f30-4a5b6c7d8e60", "type": "STATUS_CHANGE", "body": "Intéressé (rapport de visite)", "occurredAt": "2026-10-27T10:15:00Z", "author": null, "ref": { "kind": "VISIT", "id": "0193897b-2c3d-7e4f-9a50-6b7c8d9e0f31", "number": "VIS-2026-001108" } }
  ],
  "invitation": { "id": "0193a325-5f60-7a71-8b82-9c0d1e2f3a71", "displayCode": "K7MQ-4R9T", "active": true, "currentStep": "SENT" },
  "allowedStatusTransitions": ["TO_CONTACT", "NOT_INTERESTED", "TO_FOLLOW_UP"],
  "editableFields": ["storeName", "ownerName", "phone", "whatsapp", "email", "wilayaCode", "communeId", "addressLine", "landmark", "businessTypeCode", "potential", "status"],
  "scopeClause": "ASSIGNED",
  "version": 4,
  "updatedAt": "2026-11-03T09:58:01Z"
}

Field notes. editableFields and allowedStatusTransitions are computed for the caller (here location is not editable because the record is validated, and customerType because it was confirmed). Signed URLs last 5 minutes; the app refreshes the sheet rather than the URL. The « Commandes » block shows ordersSummary and links to delegate.orders.list filtered on the reseller.

Errors — 404 NOT_FOUND (never in scope) · 403 OUT_OF_SCOPE (was in scope, reassigned since — details.assignedTo is not revealed).

Rules & side effects. Read-only.

Used by DA-09.

PATCH /delegate/resellers/{resellerId}

delegate.resellers.update · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key required · Offline-capable yes — queued with version (optimistic lock) and occurredAt; a concurrent back-office edit comes back as a CONFLICT · Release V1 · Requirements CRM-01, CRM-04

Purpose. « Modifier la fiche »: correct the store's data and move its qualification status.

Request. Body — omitted fields are unchanged, null clears an optional field:

interface UpdateResellerRequest {
  version: number;                      // ResellerSheet.version the edit is based on
  storeName?: string;
  ownerName?: string;
  phone?: string;                       // only while the record has no account
  whatsapp?: string | null;
  email?: string | null;
  wilayaCode?: string;                  // with communeId
  communeId?: Uuid;
  addressLine?: string;
  landmark?: string | null;
  location?: GeoPoint;                  // see rule 3
  businessTypeCode?: string;
  potential?: "LOW" | "MEDIUM" | "HIGH";
  customerType?: CustomerType;          // proposal, only while validationStatus = PENDING
  status?: "TO_CONTACT" | "INTERESTED" | "NOT_INTERESTED" | "TO_FOLLOW_UP";
  statusReason?: string | null;         // 3–300, required for NOT_INTERESTED
  followUp?: { type: "CALL" | "VISIT" | "WHATSAPP" | "SEND_CATALOG" | "OTHER"; dueAt: Iso; note?: string | null } | null;   // required for TO_FOLLOW_UP
  occurredAt: Iso;
}
{
  "version": 4,
  "landmark": "Au-dessus de la pharmacie Ennour",
  "potential": "HIGH",
  "status": "TO_FOLLOW_UP",
  "followUp": { "type": "CALL", "dueAt": "2026-11-06T09:00:00Z", "note": "Rappeler après l'installation de l'application." },
  "occurredAt": "2026-11-03T15:20:00Z"
}

Response 200 OK — the updated ResellerSheet (same shape as delegate.resellers.get; version incremented to 5, status TO_FOLLOW_UP, the new task in openTasks).

Errors

StatusCodeWhen
403OUT_OF_SCOPEThe record left the caller's scope after the edit was made offline (reassigned)
409VERSION_CONFLICTversion is not the current one — details: { currentVersion, current: ResellerSheet }
409PHONE_ALREADY_EXISTSNew phone already used (details as in creation)
409RESELLER_TRANSITION_NOT_ALLOWEDTarget status not allowed from the current one (details: { from, to, allowed, reason } — reason = "AUTOMATIC_STATUS" when the record reached a funnel status meanwhile, e.g. the prospect installed the app)
403FIELD_NOT_EDITABLEdetails.fields[] with { field, reason }: phone (HAS_ACCOUNT), customerType (CONFIRMED_AT_VALIDATION), location (VALIDATED_LOCATION)
422REASON_REQUIREDNOT_INTERESTED without statusReason, or TO_FOLLOW_UP without followUp
422GPS_ACCURACY_TOO_LOWNew location less precise than field.gpsMaxAccuracyMeters

Rules & side effects

  1. Edits apply only to the fields listed in editableFields; version increments on any change of an editable field (status automation and counters do not increment it, so they never cause a false conflict).
  2. Status (lifecycles — manual transitions): only between qualification statuses (NEW_PROSPECT, TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UP → TO_CONTACT, INTERESTED, NOT_INTERESTED, TO_FOLLOW_UP) and only when the current status is one of them; funnel statuses and SUSPENDED are never set by a delegate. TO_FOLLOW_UP creates the CrmTask from followUp; NOT_INTERESTED cancels the record's open tasks. Each change writes ResellerStatusChange (trigger MANUAL, app DELEGATE_APP), an Interaction STATUS_CHANGE, and outbox reseller.status_changed.
  3. Location (anti-fraud — the store's position is the reference of every check-in): a delegate may set it only while it is null, or while the record is still PENDING validation and was created by them. Otherwise 403 FIELD_NOT_EDITABLE; a moved store is reported with a note, and the next check-in proposes a candidate the back-office validates.
  4. Phone: changeable only while the record has no account (the owner changes it later through the reseller app's OTP flow); uniqueness re-checked.
  5. Every change is audited (AuditLog, before/after); outbox reseller.updated refreshes the back-office 360° page (BO-11) and reaches other devices through sync.delta.

Used by DA-15, DA-09.

POST /delegate/resellers/{resellerId}/notes

delegate.resellers.addNote · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key required · Offline-capable yes — client-generated id, queued · Release V1 · Requirements CRM-07

Purpose. Add a note or log a call, a WhatsApp exchange or an SMS to the store's timeline (cahier §4.2 « Historique des appels, visites, notes… »).

Request. Body:

interface AddNoteRequest {
  id: Uuid;                             // client-generated UUIDv7
  type: "NOTE" | "CALL" | "WHATSAPP" | "SMS";
  body: string;                         // 1–2000
  durationSeconds?: number | null;      // CALL only
  occurredAt: Iso;
}
{
  "id": "0193a3a0-2b3c-7d4e-8f50-6a7b8c9d0e71",
  "type": "CALL",
  "body": "Appel au gérant : il a installé l'application, création du compte prévue ce soir.",
  "durationSeconds": 142,
  "occurredAt": "2026-11-03T15:31:00Z"
}

Response 201 Created (200 OK on the same id) — InteractionView.

{
  "id": "0193a3a0-2b3c-7d4e-8f50-6a7b8c9d0e71",
  "type": "CALL",
  "body": "Appel au gérant : il a installé l'application, création du compte prévue ce soir.",
  "occurredAt": "2026-11-03T15:31:00Z",
  "author": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "ref": null
}

Errors — 403 OUT_OF_SCOPE (reassigned since) · 409 CLIENT_ID_CONFLICT.

Rules & side effects. Creates an Interaction (immutable — corrections are new notes); outbox interaction.created (timeline of BO-11). Notes never change the status.

Used by DA-09, DA-27 (saving the content of a conflicted visit as a note).

POST /delegate/resellers/{resellerId}/photos

delegate.resellers.addPhoto · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key required · Offline-capable yes — upload queue first (files.createUpload → PUT → files.completeUpload), then this call with the known fileId, dependsOn the creation when the store was created offline · Release V1 · Requirements CRM-10

Purpose. Attach a façade, interior or shelf photo to the store (cahier §15.5 « Photos »).

Request. Body:

interface AddResellerPhotoRequest {
  id: Uuid;                             // client-generated UUIDv7
  fileId: Uuid;                         // completed upload, purpose STORE_PHOTO, by the caller
  kind: "FACADE" | "INTERIOR" | "SHELF" | "OTHER";
  takenAt: Iso;
  location: GeoPoint | null;
  caption?: string | null;              // ≤ 140
  occurredAt: Iso;
}
{
  "id": "0193a3a4-7c8d-7e9f-8a01-2b3c4d5e6f81",
  "fileId": "0193a3a4-1d2e-7f30-9a41-5b6c7d8e9f02",
  "kind": "INTERIOR",
  "takenAt": "2026-11-03T09:40:12Z",
  "location": { "lat": 35.72362, "lng": -0.55128, "accuracy": 6, "capturedAt": "2026-11-03T09:40:12Z" },
  "caption": "Vitrine accessoires — aucune marque HOCO",
  "occurredAt": "2026-11-03T09:40:20Z"
}

Response 201 Created — ResellerPhotoView.

{
  "id": "0193a3a4-7c8d-7e9f-8a01-2b3c4d5e6f81",
  "fileId": "0193a3a4-1d2e-7f30-9a41-5b6c7d8e9f02",
  "kind": "INTERIOR",
  "url": "https://files.hoco.dz/store-photos/0193a3a4-1d2e.webp?X-Amz-Expires=300&X-Amz-Signature=b0c3",
  "thumbnailUrl": "https://files.hoco.dz/store-photos/0193a3a4-1d2e-thumb.webp?X-Amz-Expires=300&X-Amz-Signature=19ed",
  "takenAt": "2026-11-03T09:40:12Z",
  "location": { "lat": 35.72362, "lng": -0.55128, "accuracy": 6, "capturedAt": "2026-11-03T09:40:12Z" },
  "caption": "Vitrine accessoires — aucune marque HOCO",
  "isCurrentFacade": false,
  "uploadedBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "createdAt": "2026-11-03T09:40:22Z"
}

Errors — 400 VALIDATION_FAILED (wrong file purpose or owner) · 403 OUT_OF_SCOPE · 409 CLIENT_ID_CONFLICT · 422 FILE_NOT_UPLOADED · 422 PHOTO_LIMIT_REACHED (50 photos per store; details.max).

Rules & side effects. Stored as ResellerPhoto (private, signed reads — cahier §10 « Protection des documents »). A FACADE photo becomes the current façade (isCurrentFacade) and the previous one stays in the history. Outbox reseller.photo_added.

Used by DA-09, DA-14, DA-15.

POST /delegate/resellers/{resellerId}/invitations

delegate.invitations.create · Access session, delegate scope (clauses a–c) · Apps DA · Idempotency-Key required · Offline-capable no — the code and the signed link are generated by the server; offline, the screen shows « Connexion requise pour générer l'invitation » (a valid invitation already cached is shown offline) · Release V1 · Requirements IVT-01, IVT-02

Purpose. « Inviter un revendeur »: create the store's invitation — or return the active one — with its code, link, QR payload and ready-to-send texts (cahier §6.6).

Invitation: QR code, download link, WhatsApp / link / SMS sharing, referral code and installation tracking.
Invitation: QR code, download link, WhatsApp / link / SMS sharing, referral code and installation tracking. — client reference (cahier des charges)

Request. Body { "occurredAt": Iso }.

{ "occurredAt": "2026-11-03T09:44:00Z" }

Response 201 Created (new invitation) or 200 OK (the active one returned) — InvitationView.

interface InvitationView {
  id: Uuid;
  code: string;                         // 8 chars: A–Z without I, L, O, plus 0–9 (BR-03)
  displayCode: string;                  // "K7MQ-4R9T"
  url: string;                          // https://app.hoco.dz/i/<code>
  qrPayload: string;                    // = url — the app renders the QR (qr_flutter)
  shareTexts: { whatsapp: string; sms: string };   // French, contain the link and the code
  reseller: ResellerSummary;
  delegate: UserSummary;
  active: boolean;
  createdAt: Iso;
  expiresAt: Iso;
  steps: Array<{ step: "SENT" | "APP_INSTALLED" | "ACCOUNT_CREATED" | "PHONE_VERIFIED" | "CATALOG_VIEWED" | "FIRST_ORDER"; at: Iso | null; inferred: boolean }>;
  currentStep: string | null;           // last step reached
  shares: Array<{ channel: "QR" | "WHATSAPP" | "SMS" | "LINK"; at: Iso }>;
}
{
  "id": "0193a325-5f60-7a71-8b82-9c0d1e2f3a71",
  "code": "K7MQ4R9T",
  "displayCode": "K7MQ-4R9T",
  "url": "https://app.hoco.dz/i/K7MQ4R9T",
  "qrPayload": "https://app.hoco.dz/i/K7MQ4R9T",
  "shareTexts": {
    "whatsapp": "Bonjour Sofiane, c'est Yacine de HOCO. Installez l'application revendeur HOCO pour voir vos prix et commander HOCO, ACEFAST et BOROFONE en direct : https://app.hoco.dz/i/K7MQ4R9T — Votre code de parrainage : K7MQ-4R9T.",
    "sms": "HOCO : installez l'appli revendeur et commandez en direct : https://app.hoco.dz/i/K7MQ4R9T Code : K7MQ-4R9T"
  },
  "reseller": {
    "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
    "number": "REV-000287",
    "storeName": "Electro Plus",
    "ownerName": "Sofiane Medjahed",
    "phone": "+213661457812",
    "wilayaCode": "31",
    "communeName": "Bir El Djir",
    "status": "INTERESTED"
  },
  "delegate": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "active": true,
  "createdAt": "2026-11-03T09:44:01Z",
  "expiresAt": "2026-12-03T09:44:01Z",
  "steps": [
    { "step": "SENT", "at": null, "inferred": false },
    { "step": "APP_INSTALLED", "at": null, "inferred": false },
    { "step": "ACCOUNT_CREATED", "at": null, "inferred": false },
    { "step": "PHONE_VERIFIED", "at": null, "inferred": false },
    { "step": "CATALOG_VIEWED", "at": null, "inferred": false },
    { "step": "FIRST_ORDER", "at": null, "inferred": false }
  ],
  "currentStep": null,
  "shares": []
}

Field notes. The reference screen shows a 12-character code (« AB12-CD34-EF56 ») and a app.delegate.com link; the API uses the platform convention — 8 characters displayed XXXX-XXXX, link https://app.hoco.dz/i/<code> served by invitations.landing. The « Copier le lien » and « Copier » buttons copy url and displayCode. The SMS text is GSM-7 only (fits one SMS).

Errors

StatusCodeWhen
403OUT_OF_SCOPERecord reassigned since
403RESELLER_SUSPENDEDResellerStatus SUSPENDED
409RESELLER_HAS_ACCOUNTThe store already has a reseller account (details.accountCreatedAt) — nothing to invite

Rules & side effects

  1. At most one active invitation per store (lifecycles): the active one (not expired, not revoked) is returned with 200; when it belongs to a previous delegate of the store, it is revoked and a new one is created for the caller, so the sign-up is attributed to the delegate who works the store now.
  2. Code (BR-03): 8 random characters from A–Z without I, L, O, plus 0–9, unique; typed input is normalised (O read as 0, I/L as 1); the link carries no personal data. Validity invitations.validityDays (default 30).
  3. Creation does not set SENT: the step is recorded when the delegate shares (markShared). Outbox invitation.created.

Used by DA-16.

GET /delegate/invitations/{invitationId}

delegate.invitations.get · Access session, delegate scope (invitations of the caller, or of a store in scope) · Apps DA · Idempotency-Key — · Offline-capable no — the last state is cached · Release V1 · Requirements IVT-03

Purpose. « Suivi d'installation »: the activation steps, updated automatically (cahier §6.6 « Les étapes doivent être mises à jour automatiquement »).

Request. Path invitationId.

Response 200 OK — InvitationView. Example in the afternoon, after the owner installed the app from the WhatsApp link:

{
  "id": "0193a325-5f60-7a71-8b82-9c0d1e2f3a71",
  "code": "K7MQ4R9T",
  "displayCode": "K7MQ-4R9T",
  "url": "https://app.hoco.dz/i/K7MQ4R9T",
  "qrPayload": "https://app.hoco.dz/i/K7MQ4R9T",
  "shareTexts": {
    "whatsapp": "Bonjour Sofiane, c'est Yacine de HOCO. Installez l'application revendeur HOCO pour voir vos prix et commander HOCO, ACEFAST et BOROFONE en direct : https://app.hoco.dz/i/K7MQ4R9T — Votre code de parrainage : K7MQ-4R9T.",
    "sms": "HOCO : installez l'appli revendeur et commandez en direct : https://app.hoco.dz/i/K7MQ4R9T Code : K7MQ-4R9T"
  },
  "reseller": {
    "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b287",
    "number": "REV-000287",
    "storeName": "Electro Plus",
    "ownerName": "Sofiane Medjahed",
    "phone": "+213661457812",
    "wilayaCode": "31",
    "communeName": "Bir El Djir",
    "status": "APP_INSTALLED"
  },
  "delegate": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "active": true,
  "createdAt": "2026-11-03T09:44:01Z",
  "expiresAt": "2026-12-03T09:44:01Z",
  "steps": [
    { "step": "SENT", "at": "2026-11-03T09:46:00Z", "inferred": false },
    { "step": "APP_INSTALLED", "at": "2026-11-03T14:10:23Z", "inferred": false },
    { "step": "ACCOUNT_CREATED", "at": null, "inferred": false },
    { "step": "PHONE_VERIFIED", "at": null, "inferred": false },
    { "step": "CATALOG_VIEWED", "at": null, "inferred": false },
    { "step": "FIRST_ORDER", "at": null, "inferred": false }
  ],
  "currentStep": "APP_INSTALLED",
  "shares": [
    { "channel": "QR", "at": "2026-11-03T09:45:10Z" },
    { "channel": "WHATSAPP", "at": "2026-11-03T09:46:00Z" }
  ]
}

Field notes. Steps are listed in funnel order; ACCOUNT_CREATED and PHONE_VERIFIED are recorded together (an account cannot exist without the OTP). A later step back-fills earlier ones with inferred = true (a store that installed from the Play Store and typed the code at sign-up gets APP_INSTALLED inferred). The reference's « Complété / En attente » labels map to at set / null.

Errors — 404 NOT_FOUND.

Rules & side effects. Read-only. Each step reached creates the invitation.step_completed notification for the delegate (in-app for every step, push for ACCOUNT_CREATED and FIRST_ORDER — notifications); while the app is open the matching notification.created arrives on the live stream (events.stream) and the screen refetches without polling.

Used by DA-16.

POST /delegate/invitations/{invitationId}/shared

delegate.invitations.markShared · Access session, delegate scope (caller's invitation) · Apps DA · Idempotency-Key required · Offline-capable yes — a share made offline (QR shown, SMS sent) is queued with occurredAt · Release V1 · Requirements IVT-02

Purpose. Record that the invitation was shared and through which channel — the app calls it when the QR is shown full screen, when the WhatsApp or SMS intent returns, and when the link or the code is copied.

Request. Body { "channel": "QR" | "WHATSAPP" | "SMS" | "LINK", "occurredAt": Iso }.

{ "channel": "WHATSAPP", "occurredAt": "2026-11-03T09:46:00Z" }

Response 200 OK — InvitationView with the share appended and SENT set at the first share (see the example of delegate.invitations.get).

Errors — 404 NOT_FOUND · 409 INVITATION_INACTIVE (expired or revoked — create a new one; details.expiredAt).

Rules & side effects. Appends an InvitationEvent share entry; the first share sets the step SENT (at = its occurredAt); repeated shares only add entries. Outbox invitation.shared (funnel report admin.invitations.list). Delivery of WhatsApp and SMS messages happens on the delegate's phone (share intents); the platform does not send them in V1.

Used by DA-16.

GET /delegate/tasks

delegate.tasks.list · Access session — tasks assigned to the caller · Apps DA · Idempotency-Key — · Offline-capable no — open tasks are kept locally through sync.delta · Release V1 · Requirements CRM-08

Purpose. « Relances »: the caller's follow-ups — due today, overdue, upcoming (cahier §6.8 « Planifier une prochaine action et une date de relance »).

Request. Query parameters: bucket (TODAY · OVERDUE · UPCOMING; default all open), status (OPEN · DONE · CANCELLED, repeatable; default OPEN), resellerId, type (repeatable TaskType), cursor, limit. Buckets use Africa/Algiers days: TODAY = dueAt within today, OVERDUE = OPEN and dueAt before now, UPCOMING = after today. Sorted by dueAt ascending.

Response 200 OK — cursor list of TaskView plus counters for the tabs.

{
  "items": [
    {
      "id": "0193a0c1-8e9f-7a10-8b21-3c4d5e6f7a91",
      "type": "CALL",
      "status": "OPEN",
      "title": "Relance inactivité — Librairie Moderne",
      "dueAt": "2026-11-03T15:00:00Z",
      "isOverdue": false,
      "reseller": {
        "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b198",
        "number": "REV-000198",
        "storeName": "Librairie Moderne",
        "ownerName": "Rachid Ouali",
        "phone": "+213662781903",
        "wilayaCode": "31",
        "communeName": "Oran",
        "status": "TO_FOLLOW_UP"
      },
      "sourceVisitId": null,
      "note": "Aucune commande validée depuis 60 jours.",
      "createdBy": null,
      "createdAt": "2026-11-03T01:00:12Z",
      "completedAt": null,
      "outcome": null,
      "version": 1
    },
    {
      "id": "0193a31b-77aa-7b10-8c21-3d4e5f6a7b81",
      "type": "CALL",
      "status": "OPEN",
      "title": "Relance téléphonique — Magasin El Wafa",
      "dueAt": "2026-11-10T08:00:00Z",
      "isOverdue": false,
      "reseller": {
        "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b123",
        "number": "REV-000123",
        "storeName": "Magasin El Wafa",
        "ownerName": "Karim Belkacem",
        "phone": "+213555123456",
        "wilayaCode": "31",
        "communeName": "Bir El Djir",
        "status": "ACTIVE"
      },
      "sourceVisitId": "0193a2f0-5b6c-7e8d-9f01-23456789ab11",
      "note": "Intéressé par les écouteurs true wireless. Souhaite recevoir le catalogue HOCO.",
      "createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
      "createdAt": "2026-11-03T08:35:02Z",
      "completedAt": null,
      "outcome": null,
      "version": 1
    }
  ],
  "nextCursor": null,
  "counts": { "today": 1, "overdue": 0, "upcoming": 1 }
}

Errors — 400 VALIDATION_FAILED (unknown bucket or type).

Rules & side effects. Read-only. Tasks come from visit reports, the back-office (admin.tasks.create) and the inactivity scan; a task stays visible to its assignee even if the store is reassigned (the store details then show only the summary).

Used by DA-19, DA-05 (followUps counters), DA-09.

PATCH /delegate/tasks/{taskId}

delegate.tasks.update · Access session — assignee only · Apps DA · Idempotency-Key required · Offline-capable yes — queued with version and occurredAt · Release V1 · Requirements CRM-08

Purpose. Reschedule a follow-up or annotate it.

Request. Body { "version": number, "dueAt"?: Iso, "note"?: string | null, "occurredAt": Iso } — dueAt from now to 90 days ahead; note ≤ 500.

{
  "version": 1,
  "dueAt": "2026-11-09T08:00:00Z",
  "note": "Le gérant sera à Alger jusqu'au 8/11.",
  "occurredAt": "2026-11-03T16:02:00Z"
}

Response 200 OK — TaskView (dueAt and note updated, version 2).

{
  "id": "0193a31b-77aa-7b10-8c21-3d4e5f6a7b81",
  "type": "CALL",
  "status": "OPEN",
  "title": "Relance téléphonique — Magasin El Wafa",
  "dueAt": "2026-11-09T08:00:00Z",
  "isOverdue": false,
  "reseller": {
    "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b123",
    "number": "REV-000123",
    "storeName": "Magasin El Wafa",
    "ownerName": "Karim Belkacem",
    "phone": "+213555123456",
    "wilayaCode": "31",
    "communeName": "Bir El Djir",
    "status": "ACTIVE"
  },
  "sourceVisitId": "0193a2f0-5b6c-7e8d-9f01-23456789ab11",
  "note": "Le gérant sera à Alger jusqu'au 8/11.",
  "createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
  "createdAt": "2026-11-03T08:35:02Z",
  "completedAt": null,
  "outcome": null,
  "version": 2
}

Errors — 404 NOT_FOUND (not the caller's task) · 409 TASK_NOT_OPEN (task DONE or CANCELLED; details.status) · 409 VERSION_CONFLICT (changed by the back-office; details.current: TaskView).

Rules & side effects. Rescheduling or annotating never changes the status (lifecycles); the due-date reminder moves with dueAt; audited; outbox task.updated.

Used by DA-19.

POST /delegate/tasks/{taskId}/complete

delegate.tasks.complete · Access session — assignee only · Apps DA · Idempotency-Key required · Offline-capable yes — queued with occurredAt · Release V1 · Requirements CRM-08

Purpose. Close a follow-up with its outcome and, optionally, plan the next one in the same gesture.

Request. Body:

interface CompleteTaskRequest {
  outcome: string;                      // 3–500, required
  nextTask?: { type: "CALL" | "VISIT" | "WHATSAPP" | "SEND_CATALOG" | "OTHER"; dueAt: Iso; note?: string | null } | null;
  occurredAt: Iso;
}
{
  "outcome": "Commande prévue jeudi ; souhaite une visite pour voir les nouveautés.",
  "nextTask": { "type": "VISIT", "dueAt": "2026-11-05T08:00:00Z", "note": "Présenter les nouveautés ACEFAST." },
  "occurredAt": "2026-11-03T14:20:00Z"
}

Response 200 OK

{
  "task": {
    "id": "0193a0c1-8e9f-7a10-8b21-3c4d5e6f7a91",
    "type": "CALL",
    "status": "DONE",
    "title": "Relance inactivité — Librairie Moderne",
    "dueAt": "2026-11-03T15:00:00Z",
    "isOverdue": false,
    "reseller": {
      "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b198",
      "number": "REV-000198",
      "storeName": "Librairie Moderne",
      "ownerName": "Rachid Ouali",
      "phone": "+213662781903",
      "wilayaCode": "31",
      "communeName": "Oran",
      "status": "TO_FOLLOW_UP"
    },
    "sourceVisitId": null,
    "note": "Aucune commande validée depuis 60 jours.",
    "createdBy": null,
    "createdAt": "2026-11-03T01:00:12Z",
    "completedAt": "2026-11-03T14:20:00Z",
    "outcome": "Commande prévue jeudi ; souhaite une visite pour voir les nouveautés.",
    "version": 2
  },
  "nextTask": {
    "id": "0193a3c8-4d5e-7f60-8a71-9b0c1d2e3f01",
    "type": "VISIT",
    "status": "OPEN",
    "title": "Nouvelle visite — Librairie Moderne",
    "dueAt": "2026-11-05T08:00:00Z",
    "isOverdue": false,
    "reseller": {
      "id": "0192d1aa-3b4c-7d5e-8f60-718293a4b198",
      "number": "REV-000198",
      "storeName": "Librairie Moderne",
      "ownerName": "Rachid Ouali",
      "phone": "+213662781903",
      "wilayaCode": "31",
      "communeName": "Oran",
      "status": "TO_FOLLOW_UP"
    },
    "sourceVisitId": null,
    "note": "Présenter les nouveautés ACEFAST.",
    "createdBy": { "id": "0192f0a1-7c3e-7b10-9a4e-5d2c1b0a9f01", "displayName": "Yacine Belkacem" },
    "createdAt": "2026-11-03T14:20:01Z",
    "completedAt": null,
    "outcome": null,
    "version": 1
  }
}

Errors — 404 NOT_FOUND · 409 TASK_NOT_OPEN · 422 REASON_REQUIRED (outcome missing or shorter than 3 characters; details.field = "outcome").

Rules & side effects

  1. OPEN → DONE; completedAt = occurredAt; an Interaction is written on the timeline: CALL → CALL, WHATSAPP → WHATSAPP, SEND_CATALOG and OTHER → NOTE, VISIT → none (the visit itself is the interaction).
  2. A VISIT task of a store is also completed automatically when a visit to that store ends (outcome « Visite réalisée VIS-… »).
  3. nextTask creates a new OPEN task for the caller; a VISIT task becomes a stop proposal in the planner (BO-21) — here Librairie Moderne appears in the 5 November tour.
  4. Outbox task.completed (and task.created); the inactivity status TO_FOLLOW_UP is left to the order rules (it returns to ACTIVE when an order is validated).

Used by DA-19.

On this page