Invitations & attribution (public + reseller)
Public operations behind the delegates' invitation links and QR codes: resolving a code, attributing the app install to the inviting delegate, and the /i/ landing page that opens the right app or store.
A délégué (field sales representative) invites a point of sale from the delegate app (DA-16): the server creates an Invitation for the reseller record and the delegate, with a code, a link and a QR code, shared by WhatsApp, SMS or link (IVT-01, IVT-02 — operations delegate.invitations.create and delegate.invitations.markShared). This page covers what happens on the reseller's side, before they have an account: opening the link, installing the app, and attributing the install and the sign-up to the delegate (IVT-03, IVT-04, cahier §6.6: « L'ouverture du lien conduit vers la bonne application et attribue correctement l'inscription »).
All three operations are public: they never return personal data beyond the store name and the delegate's first name, and they are limited to 30 requests per minute per IP.
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET | /public/invitations/{code} | invitations.resolveResolve an invitation code: store name, delegate first name, validity (no personal data beyond that) | public | RA · RW | V1 |
| POST | /public/invitations/attribution | invitations.attributeReport first app open with install referrer or invitation code (device-scoped, rate limited) | public Idempotency-Key | RA | V1 |
| GET | /i/{code} | invitations.landingWeb landing for invitation links: opens the app, else the store, else the web sign-up (served by the reseller web app) | public | RW | V1 |
Besides the errors listed per operation, the API operations can answer 426 UPGRADE_REQUIRED, 429 RATE_LIMITED, 500 INTERNAL_ERROR and 503 MAINTENANCE.
Codes and links
| Item | Format | Example |
|---|---|---|
| Code | 8 characters from an alphabet without ambiguous characters — no 0, O, 1, I, L (Invitation model) | AB12CD34 (the documentation's illustrative code; a generated code looks like K7QM4XPA) |
| Displayed | Two groups of four | AB12-CD34 |
| Link | Reseller web domain + /i/ + code | https://app.hoco.dz/i/AB12CD34 |
| QR code | The link, error correction level M | rendered by the delegate app (qr_flutter) and the printed flyer (DQA-07) |
| Signed token | HMAC(INVITE_SECRET, invitationId), base64url; only its SHA-256 is stored (Invitation.tokenHash) | carried by the Play referrer and the app link query |
| Play Store link | Store URL with the install referrer inv=<code>&t=<token> | https://play.google.com/store/apps/details?id=dz.hoco.reseller&referrer=inv%3DAB12CD34%26t%3D9xQ2… |
Input normalisation (every operation that receives a code): upper-case, remove spaces and dashes. ab12-cd34 and AB12 CD34 both resolve to AB12CD34.
Why a token as well. A code can be typed by anyone who saw it; the token proves that the install or the app opening came from the real link. Attribution through a valid token is recorded with source INSTALL_REFERRER or APP_LINK; through a code alone with source CODE_ENTERED.
Validity. An invitation is valid invitations.validityDays days after creation (default 30). A point of sale has at most one live invitation: creating a new one revokes the previous one (revokedAt). An invitation is used once its point of sale has a user account.
Activation funnel
| Value | UI label (fr) | Meaning |
|---|---|---|
SENT | Invitation envoyée | |
APP_INSTALLED | Application téléchargée | |
ACCOUNT_CREATED | Compte créé | |
PHONE_VERIFIED | Téléphone vérifié | |
CATALOG_VIEWED | Catalogue consulté | |
FIRST_ORDER | Première commande |
| Step | Recorded by | Notes |
|---|---|---|
SENT | delegate.invitations.create / markShared | With the share channel (ShareChannel). |
APP_INSTALLED | invitations.attribute; back-filled (inferred) by a later step | Only the first device per invitation counts toward the delegate's APP_INSTALLS objective. |
ACCOUNT_CREATED | auth.signup.reseller | Also moves the reseller to ACCOUNT_CREATED. |
PHONE_VERIFIED | auth.signup.reseller (the phone is verified by OTP just before) | Same timestamp as ACCOUNT_CREATED. |
CATALOG_VIEWED | First signed-in call to catalog.home or catalog.products.list | Guest browsing before sign-up does not count. |
FIRST_ORDER | First order submitted (cart.checkout, or an assisted order confirmed by the reseller) | Attributed to the reseller and the source delegate (ORD-09). |
Each step is an InvitationEvent (step, time, device id when known, source INSTALL_REFERRER, APP_LINK, CODE_ENTERED, DELEGATE or SYSTEM); the invitation keeps the first time of each step (appInstalledAt, accountCreatedAt…) and its currentStep. The funnel is monotonic: reaching a later step back-fills the missing earlier ones with inferred: true. Every step emits invitation.step_completed to the delegate (event catalogue) and updates the funnel on DA-16 and the back-office invitation report (admin.invitations.list). The reseller statuses APP_INSTALLED and ACCOUNT_CREATED follow automatically (CRM-04), never moving a reseller backwards.
Attribution precedence
When a reseller signs up, the invitation credited to a delegate is chosen as follows (AUTH-05 R6):
| Sign-up | Order of precedence |
|---|---|
| Claim (the verified phone matches a known point of sale) | 1. the live invitation of that point of sale · 2. a valid code typed on RA-03 or carried by the link · 3. none — the creating delegate keeps the credit through createdByDelegateId |
| New record | 1. a valid code typed or carried by the link (invitationCode) · 2. the invitation recorded by invitations.attribute for the same X-Device-Id in the last 30 days · 3. none |
A code never grants ownership of a record — only the verified phone does. A code belonging to another point of sale's invitation is recorded as an InvitationEvent on that invitation for review and does not move the attribution; the sign-up reports it in invitationCodeIgnored: "OTHER_POINT_OF_SALE", and the validator sees the possible duplicate on BO-13.
Operations
GET /public/invitations/{code}
invitations.resolve · Access public · Apps RA, RW · Idempotency-Key — · Release V1 · Requirements IVT-04
Purpose. Tells the reseller app, the web sign-up and the landing page whether a code is valid, and gives the two facts shown to the invitee: the store name and the delegate's first name.
Request
| Path parameter | Type | Notes |
|---|---|---|
code | string | Any accepted spelling (see normalisation). |
Response — 200 OK, Cache-Control: private, no-store.
{
"code": "AB12CD34",
"displayCode": "AB12-CD34",
"storeName": "Magasin El Wafa",
"delegateFirstName": "Yacine",
"expiresAt": "2026-12-01T10:05:00Z"
}| Field | Notes |
|---|---|
storeName | The point of sale the invitation was created for — RA-03 shows « Invitation de Yacine pour Magasin El Wafa ». |
delegateFirstName | First name only; never the delegate's phone or last name. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Not 8 characters of the code alphabet after normalisation (field code INVALID_INVITATION_CODE). |
| 404 | INVITATION_NOT_FOUND | No invitation with this code. |
| 409 | INVITATION_ALREADY_USED | The point of sale already has an account — offer « Se connecter ». |
| 410 | INVITATION_EXPIRED | Past its validity or replaced by a newer invitation (details.reason: EXPIRED, REVOKED) — « Invitation expirée : demandez un nouveau lien à votre délégué. » |
Rules & side effects — read-only: no step is recorded and no personal data is returned. Failed lookups count toward the per-IP limit (30 per minute), which makes guessing codes impractical (31⁸ ≈ 8.5 × 10¹¹ codes).
Used by — RA-03 (code typed or pre-filled: show the inviter before « Recevoir le code »), RA-02 when the app was opened by a link, the sign-up page of RW-01, and the /i/{code} landing (server-side).
POST /public/invitations/attribution
invitations.attribute · Access public (device-scoped) · Apps RA · Idempotency-Key required · Release V1 · Requirements IVT-03, IVT-04
Purpose. Reports that the reseller app was opened for the first time thanks to an invitation — from the Play install referrer, an app link or a typed code — so the install is credited to the delegate even before sign-up.
Request
| Header | Notes |
|---|---|
X-Device-Id | Required; must equal deviceId in the body. |
X-App-Version, X-App-Platform | As on every mobile call. |
interface AttributionRequest {
code?: string; // from an app link (/i/AB12CD34) or typed on RA-03
token?: string; // the t parameter of an app link (/i/AB12CD34?t=…)
installReferrer?: string; // raw Play Install Referrer string, e.g. "inv=AB12CD34&t=9xQ2…"
referrerClickedAt?: Iso; // Install Referrer API: referrerClickTimestampSeconds
installBeganAt?: Iso; // Install Referrer API: installBeginTimestampSeconds
deviceId: string;
platform: "android" | "ios";
appVersion: string;
}{
"installReferrer": "inv=AB12CD34&t=9xQ2bW7kR4mN1pL6vT3c&utm_source=hoco_invite&utm_medium=whatsapp",
"referrerClickedAt": "2026-11-02T16:41:07Z",
"installBeganAt": "2026-11-02T16:42:30Z",
"deviceId": "5f8c2a1e-7b3d-4e9f-a6c0-1d2e3f4a5b6c",
"platform": "android",
"appVersion": "1.4.2"
}Response — 200 OK.
{
"attributed": true,
"alreadyRecorded": false,
"step": "APP_INSTALLED",
"invitation": {
"code": "AB12CD34",
"displayCode": "AB12-CD34",
"storeName": "Magasin El Wafa",
"delegateFirstName": "Yacine"
}
}An install without invitation (organic referrer such as utm_source=google-play&utm_medium=organic) answers 200 with { "attributed": false, "alreadyRecorded": false, "step": null, "invitation": null }.
| Field | Notes |
|---|---|
alreadyRecorded | true when this device was already attributed to this invitation (replay, reinstall): nothing new is recorded. |
invitation | The app keeps it locally to pre-fill and display the code on RA-03 and to send invitationCode at sign-up. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Neither code nor installReferrer; deviceId differs from X-Device-Id (DEVICE_ID_MISMATCH); malformed code. |
| 404 | INVITATION_NOT_FOUND | The code (given or found in the referrer) does not exist. |
| 409 | INVITATION_ALREADY_USED | The point of sale already has an account. |
| 410 | INVITATION_EXPIRED | Expired or revoked. |
Rules & side effects
- The code comes from
codewhen given, otherwise from theinvparameter ofinstallReferrer; the token fromtokenor the referrer'st. A token that does not matchInvitation.tokenHashis ignored (the call then counts asCODE_ENTERED). - Idempotent per device and invitation: the first call records an
InvitationEventAPP_INSTALLED(device id, sourceINSTALL_REFERRER,APP_LINKorCODE_ENTERED, and inmetadatathe raw referrer, the referrer timestamps and the app version); later calls for the same pair returnalreadyRecorded: true. The idempotency record is scoped to the device (device:<X-Device-Id>). - Effects of a first record: the invitation reaches
APP_INSTALLED; the reseller status moves toAPP_INSTALLEDif it wasNEW_PROSPECT,TO_CONTACT,INTERESTED,TO_FOLLOW_UPorNOT_INTERESTED(automaticResellerStatusChange);invitation.step_completedis sent to the delegate (« Magasin El Wafa a installé l'application »). - Only the first device of an invitation counts toward the delegate's
APP_INSTALLSobjective; every device is kept for the attribution precedence of sign-up. A gap of less than 10 seconds betweenreferrerClickedAtandinstallBeganAt, or many devices on one invitation, is flagged on the back-office invitation report (fraud hint, not a block). - Client flow: on the first launch (flag in local storage), the Android app reads the Install Referrer API once and calls this operation; an app opened through an app link calls it with
code; a code typed on RA-03 and resolved successfully is also sent here, so the install counts even if the sign-up is abandoned. Errors are not shown to the user, except on RA-03 where the typed code is concerned.
Used by — first launch of the reseller app (before RA-02), app-link handling, and RA-03 after a typed code is resolved.
GET /i/{code}
invitations.landing · Access public · Apps RW · Idempotency-Key — · Release V1 · Requirements IVT-04
Purpose. The page behind every invitation link and QR code: it opens the reseller app when installed, otherwise sends the visitor to the store with the code attached, otherwise to the web sign-up with the code pre-filled.
This is not an API route: it is a page of the reseller web app, served at https://app.hoco.dz/i/{code} (not under /v1). It calls invitations.resolve server-side.
Request
| Path parameter | Notes |
|---|---|
code | Any accepted spelling; the page redirects to the canonical /i/AB12CD34 when the spelling differs. |
Response — by platform (detected from the User-Agent):
| Visitor | Behaviour |
|---|---|
| Android, reseller app installed | The OS opens the app directly through the verified App Link (intent filter https://app.hoco.dz/i/*, autoVerify); the page is not even loaded. The app reads the code from the path (and t from the query when the shared link carries it), calls invitations.attribute and opens RA-03 pre-filled. |
| Android, app not installed | 200 HTML page « Yacine vous invite à rejoindre HOCO — Magasin El Wafa » that redirects after 300 ms to the Play Store URL with referrer=inv%3DAB12CD34%26t%3D<token> (the page rebuilds the token from the invitation id); button « Installer l'application » as fallback. After the install, the app finds inv and t in the install referrer. |
| Android, in-app browser that ignores App Links | The « Ouvrir l'application » button uses intent://i/AB12CD34#Intent;scheme=https;package=dz.hoco.reseller;S.browser_fallback_url=<Play Store URL>;end. |
| iOS | Universal Link (apple-app-site-association, path /i/*) opens the app when installed. Otherwise the page offers the App Store (when the iOS app is published — HOCO's decision) and « Continuer sur le web » → /inscription?code=AB12CD34. There is no install referrer on iOS: the page shows the code AB12-CD34 large with « Saisissez ce code lors de votre inscription ». |
| Desktop | 302 to /inscription?code=AB12CD34 (web sign-up RW-01 with the code pre-filled), which also shows a QR code of the link to continue on a phone. |
| Invalid, expired or used code | 200 page « Invitation invalide ou expirée » (or « Ce magasin a déjà un compte » with « Se connecter »), with buttons to install the app or sign up without a code; the store name is not shown. |
Response headers: Cache-Control: private, no-store, X-Robots-Tag: noindex. Open Graph tags are generic (« Invitation HOCO — rejoignez le réseau des revendeurs ») so link previews in WhatsApp never show the store name.
Errors — none as JSON; invalid codes render the page above. More than 30 requests per minute from one IP answer 429 with a plain page.
Rules & side effects
- The page records nothing: attribution happens in the app (
invitations.attribute) or at sign-up (invitationCode). - The reseller web must serve
/.well-known/assetlinks.json(packagedz.hoco.reseller, SHA-256 of the Play signing certificate) and/.well-known/apple-app-site-association(team id + bundle id,/i/*). Imad provides the fingerprints; Anis deploys the files (WEB-01, MRA-09). - Assumption — the landing page and the
.well-knownfiles go live with V1, even though the full reseller web opens at generalisation (P3): invitations are used from the pilot.
Used by — every shared invitation link and QR code (DA-16, the printed flyer); the reseller app's link handling on RA-02 and RA-03; the web sign-up RW-01.
Support tickets
Reseller support: tickets and after-sales (SAV) claims with their conversation and resolution, support contacts and FAQ, and the regional WhatsApp number resolved from the account or the device location.
Loyalty (V2)
V2 loyalty programme for resellers — points balance and level progress, points history, rewards catalogue with eligibility, redemption and vouchers.