HOCO B2B
Ways of working

API contract workflow

Contract first: how an operation goes from a Zod schema in packages/contracts to the OpenAPI document, a Dart client, a mock server and an implementation — and how the contract changes without breaking the apps.

The API contract is the interface between Anis's track and Imad's track. One rule holds it together: no operation is implemented or consumed before it exists in packages/contracts on dev. The mock server lets the apps be built against the contract while the server implementation is still being written (ADR-002).

  • SRV-03 writes contract v1 — every V1 operation, the OpenAPI export and the mock server — and reaches the milestone API contract v1 published (OpenAPI + mock server). Imad reviews it as the first consumer.
  • MOB-03 generates hoco_api from that document, with a regeneration script, the Prism workflow and a pinned contract version.
  • SRV-29 repeats the loop for the V2 operations, so V2 mobile work starts against the mock too.

After contract v1, every change follows the loop below, however small.

Where the contract lives

ArtefactLocationProduced byRead by
The contract — oRPC routes, Zod schemas, declared errors, metadata (permission, scope, idempotency, release)hoco → packages/contracts/src/ (contract.ts, routers/<module>.ts, schemas/)Anis, in contract PRsapps/server (implements it), packages/api-client (Next apps)
The OpenAPI 3.1 documentapps/docs/public/openapi/hoco-v1.json, served by this site at /openapi/hoco-v1.jsonpnpm openapi:exportImad's generator, Prism, reviewers
The live document/openapi.json on each environment — what that environment runsThe serverDebugging; Scalar at /reference outside production
Contract tagscontracts-vX.Y.Z on the merge commit in hocoAnisThe pin in hoco-mobile
The Dart clienthoco-mobile → packages/hoco_api, with the pinned copy of the documentImad's generatorBoth apps
Catalogue and reference pagesapps/docs/content/spec/endpoints.json, /docs/srs/apiAnis, in the contract PREveryone
Error registryErrorsThe contract PREvery app

The loop

Propose (Anis). Branch feature/<TASK-ID>-contract-<topic>, for example feature/SRV-21-contract-visits. For each operation, declare in packages/contracts:

  • the op id (the contract key path, e.g. delegate.visits.arrive) and .route({ method, path }) relative to /v1;
  • the Zod input and output, with the field conventions of the API reference (camelCase, centimes, UTC, E.164, UUIDv7);
  • the declared errors with their status and details shape;
  • the metadata: permission key, scope, idempotency requirement, release (V1 or V2);
  • a summary, and an example for every response (Prism serves the examples).

Then run pnpm openapi:export, update endpoints.json and the operation's section on its API page, add the changelog entry and bump the contract version. Open the PR with the label contract and a title like feat(contracts): delegate visit check-in operations [SRV-21].

Review (Imad, within 4 working hours). Imad checks that the shapes generate clean Dart (no anonymous unions, explicit nullability, enums as UPPER_SNAKE_CASE strings), that every field a screen needs is there and nothing it does not, the pagination style, the Idempotency-Key and occurredAt requirements on offline mutations, error codes the UI can map, and realistic examples (French copy, centimes, E.164 phones). He approves, or leaves blocking: comments. Akram is consulted when a shape decides what a screen shows.

Merge and publish. Squash-merge into dev; Anis tags the merge commit contracts-vX.Y.Z. CI checks that the committed document equals a fresh export and that oasdiff finds no undeclared breaking change. The docs deploy publishes /openapi/hoco-v1.json; staging serves /openapi.json once the server foundation (SRV-04) is live. Anis posts in the team group: "Contract 1.N.0 — operations … — PR #…".

Regenerate (Imad, same or next working day). Copy the tagged document into hoco-mobile as the pinned copy, run the generator, commit chore(api): regenerate client from contract 1.N.0 [MOB-03]. The pinned version is the document's info.version.

Build in parallel. Imad builds the screens against Prism. Anis implements behind the same contract; until he does, a contracted operation answers 501 NOT_IMPLEMENTED with the standard error envelope on staging.

Implement (Anis). feat(server): implement the visit check-in operations [SRV-21]. This PR must not change the contract. If implementing reveals a contract problem, stop and open a contract PR — back to step 1.

Integrate. When the operation is live on staging, Imad switches that feature from the mock to staging (the full switch is MOB-12). A mismatch is a bug against whichever side differs from the contract; if the contract itself is wrong, a contract PR fixes it.

The description of a contract PR:

**Operations:** delegate.visits.arrive (changed: response adds `distanceMeters`), delegate.visits.start (changed: accepts an optional `note`)
**Class:** additive — contract 1.4.0 → 1.5.0
**Consumers:** DA-10 (MDA-03, MDA-05), BO-23 (DSH-08)
**New error codes:** none
**Examples:** updated for both responses

Mock server

Prism serves the OpenAPI document as a mock API, on the developer's machine:

# in hoco — the latest contract on your branch (regenerate it with pnpm openapi:export)
pnpm mock    # prism mock … --port 4010 --dynamic --cors

# in hoco-mobile — the pinned copy (the path is set in MOB-03)
npx @stoplight/prism-cli mock packages/hoco_api/openapi/hoco-v1.json --port 4010

# Android emulator: http://10.0.2.2:4010 · a phone on the same Wi-Fi: http://<laptop-ip>:4010
  • Dynamic mode (--dynamic, what pnpm mock runs) generates data from the schemas. Response objects are closed (additionalProperties: false), so the generated data has exactly the documented fields. Contract v1.0.0 has no response examples yet; until they land, use dynamic mode.
  • Static mode (without --dynamic) returns the response example; contract PRs add examples operation by operation.
  • Error states: send Prefer: code=422 (or Prefer: example=<name>) to get the declared error example and build the error screens.
  • Prism also validates the app's requests against the document: a request Prism rejects is a client bug to fix before integration. Prism's own validation answers are not HOCO's error envelope — build error UIs from the declared error examples, not from Prism's validation messages.

The Next apps do not need the mock: they live in the same repository as the server and run against it locally.

Additive and breaking changes

ChangeClassWhat to do
New operationAdditive — minorLoop as above
New optional request field, new query parameter with a defaultAdditive — minor
New response fieldAdditive — minorClients ignore unknown fields
New error code on an existing operation, same status familyAdditive — minorRegistry row, translations, app mapping
New enum value in a responseAdditive — minor, announced firstChangelog entry at least one mobile release before the server emits it; apps render unknown values with a neutral label
Relaxed validation (longer limits, more accepted values)Additive — minor
Descriptions and examples onlyPatch
Removing or renaming a field, an operation, a parameter or an enum valueBreakingAdd the new one alongside, deprecate the old one
Making an optional request field required, adding a required fieldBreakingNew field or new operation
Changing a type, unit or format (centimes to decimals, date to date-time)BreakingNew field
Changing the pagination style, the default sort, or lowering a page limitBreakingNew operation
Tightening validationBreakingOnly with the deprecation window
Changing the meaning of a status or error code, reusing a codeBreaking — never doneCreate a new code
Requiring a new header (for example Idempotency-Key) or a new permission that rejects existing callersBreakingDeprecation window; check the app versions in use

A breaking need is shipped as an addition plus a deprecation, never in place. /v2 is reserved for a wholesale redesign and is not planned: the V2 product scope is additive and stays in /v1.

Deprecation

  • The route or field is marked deprecated in the contract. It appears as deprecated in the OpenAPI document, and the generated Dart code flags it.
  • The server answers deprecated operations with Deprecation (RFC 9745) and Sunset (RFC 8594) headers and a link to the page:
Deprecation: @1798761600
Sunset: Fri, 31 Dec 2027 23:59:59 GMT
Link: <https://{docs-site}/docs/srs/api/orders#orderslist>; rel="deprecation"
  • Removal needs all three conditions:
    1. at least two releases of each affected app have reached the track the users are on (closed track during the pilot, production after) without using the deprecated item;
    2. the minimum supported version of each affected app (BO-66) has been raised above the last version that uses it;
    3. the server logs (which carry X-App-Version) show no call to it for 14 days.
  • The back-office and the reseller web are deployed with the server, so they migrate in the same release; the window exists for the mobile apps.

Versioning

VersionWhereRule
Path version/v1For the whole life of the V1 and V2 product scope
Contract versioninfo.version of the document, tag contracts-vX.Y.Z1.0.0 at API contract v1 published; minor for additive changes; patch for descriptions and examples; 0.x before that milestone
App versionX-App-Version header from the appsBelow the minimum for the app and platform: 426 UPGRADE_REQUIRED
Platform versionvX.Y.Z tags in hocoIndependent of the contract version (versions)

Assumption — the export currently takes info.version from apps/server/package.json. SRV-03 switches it to the version of @hoco/contracts, bumped in each contract PR, so the contract version moves only when the contract does.

Error codes

  • Every error an operation can return is declared in the contract with its HTTP status and details shape, and listed in the error registry.
  • Codes are UPPER_SNAKE_CASE and stable forever: never renamed, never reused for another meaning. A retired code stays documented as retired.
  • A new code arrives in one PR with: the contract declaration, the registry row (status, meaning, details, operations), the server message in French with ar and en keys, and an example. Imad adds the app mapping when he regenerates.
  • Clients branch on code — never on message, never on the status alone.
  • NOT_IMPLEMENTED (501) exists on staging only. The release checklist verifies that no contracted operation of the release still answers it.

Keeping the reference pages in sync

  • Once SRV-03 lands, the OpenAPI document is authoritative. endpoints.json and the API reference pages describe it for people.
  • The contract PR updates, in the same PR: the operation in endpoints.json (op id, method, path, auth, permission, apps, release, requirements); its section on the API page (request, response example, errors, rules), under the anchor of its op id (cart.checkout → #cartcheckout); the error registry.
  • CI (added with SRV-03) compares the op ids of the exported document with endpoints.json and fails when an operation is missing on either side.
  • When a page and the document disagree, the document wins. The page is fixed in a docs(contracts): … commit.
  • Scalar (/reference, local and staging) renders the same document, so it is always in sync.

Contract changelog

Newest first. One entry per merged contract PR, in the same PR. Imad reads this table before each regeneration.

VersionTask · PRChangeClassApp action
1.2.0MOB-03 · Imad's requestsGeneration only: every tagged union declares discriminator with its mapping (BannerTarget and ReportHistoryItem by type, LiveEvent by event, OrderHistoryEntry by kind, ResellerMap by mode); the enums of query and path parameters reference their named models (CatalogSort, ResellerListSort, ObjectivePeriod, PerformancePeriod, PromotionType…), and parameters that share a name with different values get one model per operation (AdminOrdersListSort, AdminOrdersListDateField…). Earlier, OpenAPI pass 2 (#45) named every nested object. Changed: ProductPresented of delegate.visits.report carries `kind: "CATEGORY""PRODUCT"(required, the discriminator); the report view's presented products carry the samekind`.Breaking for ProductPresented only — accepted in place because no released app sends it (asked by Imad); the rest is additive
1.1.0SRV-41New operations: admin.users.get, admin.staff.assignable, admin.credit.list, admin.variants.prices, admin.variants.bulkPrices, admin.resellers.paymentMethods, admin.banners.reorder, admin.imports.list, admin.imports.template, creditNotes.list, announcements.get. New fields and filters: admin.resellers.list/map/export/bulk ids; map pins delegateName; DeliveryCard version, warehouse and deliveries filters warehouseId, cod, validatedFrom/validatedTo; admin.orders.create response issues; Promotion fixedPrices (per customer type, value still valid) and preview for a customerType or a guest (resellerId optional, response viewer, priceLocked; reseller absent without a reseller); AttributeDefinition minValue/maxValue/showOnProductSheet, VariantAttribute onProductSheet; Announcement bodyAr, image (imageFileId), audience.customerTypes, targetNames, file; FilePurpose ANNOUNCEMENT_IMAGE; orders.confirmAssisted paymentMethod/addressId/note; orders.rejectAssisted reasonCode + comment (reason now optional); returns.create reasonCode (reason optional with a code), ReturnView reasonCode; reference lists ASSISTED_REJECT_REASON, RETURN_REASON; FinanceSummary paymentInstructions; payments.declare reference optional; promotions.list brandId; OrderSummary invoiceStatus; ProductDetail complements; ProductCard/VariantPromotion isFlashSale (+ card endsAt); notifications.markAllRead group. New stream event: return.status_changed (admin stream; return.requested and it also reach support.view holders). Fix: catalog.products.similar lists the configured complements first.Additive (new enum values announced here: AdminEventType.return.status_changed, FilePurpose.ANNOUNCEMENT_IMAGE, ReferenceListKey × 2)Regenerate; RA-23 confirm/reject with the new fields, RA-31 optional reference and payment details, RA-40 return reasons from the list, flash badge from isFlashSale, inbox « tout lire » per tab
1.0.0SRV-03 — done 18 Oct 2026Contract v1: all 269 V1 operations of the catalogue as 341 routes (multi-method operations split per verb, e.g. admin.segments.crud.list), with request and response schemas and pagination; enum alignment (BannerPlacement, AttributeFilterWidget, InventorySessionStatus); Prism mock in dynamic mode. Response examples follow.—Generate hoco_api (MOB-03)
0.1.0PRJ-01 — doneScaffold: system.health, system.version, the error envelope, money and pagination schemas—None — not consumed

New entries follow this template:

| `1.5.0` | SRV-21 · #48 | delegate.visits.arrive returns `distanceMeters`; delegate.visits.start accepts an optional `note` | Additive | Regenerate; show the distance on DA-10 |

On this page