File uploads & downloads
Two-step uploads to S3-compatible storage with pre-signed URLs, per-purpose limits, image processing (EXIF stripping, thumbnails) and the rules for reading private files.
Every photo and document in HOCO — store façades, visit photos, payment proofs, after-sales photos, store documents, product images — goes through this group. Bytes never transit through the API: the client asks for a pre-signed PUT URL, sends the bytes straight to the object storage (MinIO locally, an S3-compatible service in production — INT-05), then completes the upload so the server can check and process the file. The resulting fileId is then attached by the business operation that needs it (delegate.visits.addPhoto, payments.declare, tickets.create, returns.create, profile.documents.add, me.update…).
Private files are never publicly reachable (SEC-04): they are read through signed GET URLs valid 5 minutes, issued only to users allowed to see them. Product images, brand logos and banners are public and served by the CDN.
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| POST | /files/uploads | files.createUploadRequest a pre-signed upload URL for a photo or document | sessionIdempotency-Key | RA · RW · DA · BO | V1 |
| POST | /files/{fileId}/complete | files.completeUploadConfirm an upload; the server checks size/type and creates thumbnails | sessionIdempotency-Key | RA · RW · DA · BO | V1 |
| GET | /files/{fileId}/url | files.downloadUrlShort-lived signed URL for a private file the caller may access | session | RA · RW · DA · BO | V1 |
Besides the errors listed per operation, every operation can answer 401 UNAUTHENTICATED, 403 ACCOUNT_SUSPENDED, 426 UPGRADE_REQUIRED, 429 RATE_LIMITED, 500 INTERNAL_ERROR and 503 MAINTENANCE. Guests cannot upload.
Upload flow
- Prepare on the device. Pick or capture images as JPEG with
image_pickerimageQuality: 85andmaxWidth/maxHeight: 2560— the plugin re-encodes (HEIC from iOS galleries becomes JPEG) and a 12-megapixel photo drops from about 4 MB to under 1 MB on 3G/4G. - Create.
POST /files/uploadswith anIdempotency-Keystored with the pending action. - PUT the bytes to
uploadUrlwith exactly the returnedheaders(they are part of the signature). The URL expires after 10 minutes; a403from the storage means expired or tampered — callcreateUploadagain with the sameIdempotency-Key, which returns the samefileIdwith a freshly signed URL as long as the pending file has not been purged (hourly job); after that, start again with a new key. - Complete.
POST /files/{fileId}/completereturns theFileRef. - Attach the
fileIdin the business operation. Assumption — a completed file attached to nothing after 24 hours is soft-deleted (deletedAt) by the cleanup job.
Offline (delegate app). Photos taken offline are kept on the device with their capture time and position, under a client-generated UUIDv7 that the app also uses as id in createUpload — so the queued business action can reference the file before the upload happens. When the network returns, the outbox runs steps 2–4 for each photo, then replays the business action (through sync.batch or directly) with the fileId. The capture metadata travels in the business call (capturedAt, location of a visit photo), not in the file — see rule 3 of image processing.
Purposes and limits
The purpose (enum FilePurpose) decides the bucket, the limits, who may upload and who may read.
| Purpose | Bucket | Max size | Accepted types | Who may upload |
|---|---|---|---|---|
PRODUCT_IMAGE | public | 5 MB | JPEG, PNG, WebP | Staff with catalog.update |
BRAND_LOGO | public | 2 MB | PNG, WebP, JPEG | Staff with catalog.update |
BANNER_IMAGE | public | 5 MB | JPEG, PNG, WebP | Staff with promotions.create or promotions.update |
STORE_PHOTO | private | 5 MB | JPEG, PNG, WebP | The reseller (own store), a delegate (resellers in scope), staff with crm.update |
VISIT_PHOTO | private | 5 MB | JPEG, PNG, WebP | The delegate (own visits) |
STORE_DOCUMENT | private | 10 MB | PDF, JPEG, PNG | The reseller (own store), a delegate (resellers in scope), staff with crm.update |
PAYMENT_PROOF | private | 10 MB | PDF, JPEG, PNG | The reseller (own declarations), staff with finance.create |
DELIVERY_PROOF | private | 10 MB | JPEG, PNG, PDF | Staff with delivery.update |
TICKET_ATTACHMENT | private | 10 MB | JPEG, PNG, WebP, PDF | The reseller (own tickets, after-sales claims and return requests), staff with support.update |
AVATAR | private | 2 MB | JPEG, PNG, WebP | Any signed-in user (own avatar) |
GENERATED_DOCUMENT | private | — | PDF, XLSX, CSV | Never uploaded: produced by the server (invoices, delivery notes, credit notes, exports) |
IMPORT_FILE | private | 20 MB | XLSX, CSV | Staff with the import's permission (catalog.administer, settings.administer) |
Accepted MIME types: image/jpeg, image/png, image/webp, application/pdf, text/csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet. HEIC, GIF, SVG and archives are refused. Assumption — the limits not given by enums.json (brand logo, banner, delivery proof, avatar, import file) and the use of TICKET_ATTACHMENT for return-request photos are proposals to confirm.
Who may read a file
| Purpose | Readable by |
|---|---|
PRODUCT_IMAGE, BRAND_LOGO, BANNER_IMAGE | Everyone, guests included (public CDN URL). |
STORE_PHOTO | The reseller of the store; delegates who have the reseller in scope; staff with crm.view. |
VISIT_PHOTO | The delegate who took it; staff with field.view. Never the reseller. |
STORE_DOCUMENT | The reseller of the store; staff with crm.view; a delegate only for the documents they uploaded themselves (RC, NIF and ID cards are sensitive — CRM-10, ACL-04). |
PAYMENT_PROOF | The reseller who declared it; staff with finance.view. |
DELIVERY_PROOF | The reseller of the order; staff with delivery.view or orders.view. |
TICKET_ATTACHMENT | The reseller of the ticket or return; staff with support.view (tickets, after-sales claims) or orders.view (returns). |
AVATAR | Any signed-in user who is shown the person (it appears in UserSummary.avatarUrl). |
GENERATED_DOCUMENT | Invoices, delivery notes and credit notes: the reseller concerned and staff with finance.view / orders.view. Exports: the staff user who requested them. |
IMPORT_FILE | The uploader and staff with settings.administer. |
Anyone else gets 404 NOT_FOUND — not 403 — so the existence of a file is not revealed.
Image processing
- Checks. The real type is detected from the file's first bytes (magic number), not from the declared MIME type or the extension; images are decoded fully (corrupt files refused) and limited to 40 megapixels (decompression bombs).
- Orientation, then metadata stripping. The EXIF orientation is applied to the pixels, then all metadata is removed (EXIF, XMP, IPTC, embedded thumbnails; colours converted to sRGB). A photo downloaded from HOCO never carries the device, the owner or a GPS position.
- Capture metadata kept server-side. Before stripping, the EXIF capture time and GPS position of
VISIT_PHOTOandSTORE_PHOTOfiles are copied into the file's internal metadata (File.metadata.exifCapturedAt,exifLocation). They are shown to staff on the visit detail (BO-25) to compare with the capture time and position declared by the app — a photo picked from the gallery days earlier is visible. They are never returned to resellers. - Re-encoding. Private photos are stored as JPEG (quality 85, longest edge at most 2560 px). Public images are converted to WebP: a 1600 px rendition (served as
url) and the original kept for the back-office. - Thumbnails. Every image gets a 400 px WebP thumbnail (
File.thumbnailKey, served asthumbnailUrl), generated duringcompleteUpload(typically under 1 s). PDFs have no thumbnail in V1; clients show a document icon with the page count unknown. - No antivirus in V1 (Assumption): PDFs are served with
Content-Disposition: attachmentandX-Content-Type-Options: nosniff, never rendered inline by the API's domains.
URL rules
- Public files:
urlis a permanent CDN URL (https://cdn.hoco.dz/products/2026/11/01a1….webp), cacheable forever (file names are immutable ids). - Private files: every
FileRef.urlandthumbnailUrlreturned by any operation is signed at response time and valid 5 minutes. Clients must not store signed URLs; to display a file later they re-read the resource or callfiles.downloadUrl. - Caching on mobile: cache images by
fileId+ variant (for exampleCachedNetworkImage(cacheKey: "${file.id}:thumb")), not by URL — the URL changes on every response while the content never does. - Storage keys never contain the original file name: bucket
hoco-publicorhoco-private, key<purpose>/<yyyy>/<mm>/<fileId>.<ext>. The original name is kept onFile.originalNameand used for downloads. - Hosts
storage.hoco.dzandcdn.hoco.dzare placeholders, like the API domain.
Operations
POST /files/uploads
files.createUpload · Access session (any user type; purpose rules above) · Apps RA, RW, DA, BO · Idempotency-Key required · Release V1 · Requirements INT-05, SEC-04
Purpose. Reserves a file id and returns a pre-signed URL to which the client uploads the bytes directly.
Request
interface CreateUploadRequest {
id?: Uuid; // client-generated UUIDv7 (offline photos); the server generates one otherwise
purpose: string; // FilePurpose
fileName: string; // original name, 1–200 characters ("facade_el_wafa.jpg")
mimeType: string; // one of the accepted types for the purpose
size: number; // bytes, 1 … max size of the purpose
checksumSha256?: string; // hex SHA-256 of the bytes (64 characters); when given, the storage verifies it on PUT
}{
"purpose": "VISIT_PHOTO",
"fileName": "IMG_20261105_101522.jpg",
"mimeType": "image/jpeg",
"size": 845312,
"checksumSha256": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d"
}Response — 201 Created.
{
"fileId": "01a1a0b9-4c2d-7e6f-8a1b-2c3d4e5f6a7b",
"uploadUrl": "https://storage.hoco.dz/hoco-private/visit_photo/2026/11/01a1a0b9-4c2d-7e6f-8a1b-2c3d4e5f6a7b.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=…&X-Amz-Date=20261105T101530Z&X-Amz-Expires=600&X-Amz-SignedHeaders=content-type%3Bhost%3Bx-amz-checksum-sha256&X-Amz-Signature=5d1e…",
"method": "PUT",
"headers": {
"Content-Type": "image/jpeg",
"x-amz-checksum-sha256": "q1MKE+RZFJgrefm34/uplM/R8/si9xzqGvvwK0YMbR0="
},
"expiresAt": "2026-11-05T10:25:30Z",
"maxSize": 5242880
}| Field | Notes |
|---|---|
headers | Send all of them, unchanged, on the PUT; add nothing that is signed differently. Content-Length is set by the HTTP client. The checksum header is the base64 form of the hex value sent in the body. |
expiresAt | 10 minutes after creation. |
maxSize | The purpose's limit in bytes, so the client can refuse early. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Missing fields, size not positive, malformed checksum, file name too long. |
| 403 | FILE_PURPOSE_NOT_ALLOWED | The caller may not upload this purpose (a reseller sending PRODUCT_IMAGE, anyone sending GENERATED_DOCUMENT). |
| 409 | IDEMPOTENCY_KEY_REUSED | Same key with a different body. |
| 422 | FILE_TYPE_NOT_ALLOWED | mimeType not accepted for the purpose (details.allowed). |
| 422 | FILE_TOO_LARGE | size above the purpose's limit (details.maxSize). |
| 429 | RATE_LIMITED | More than 60 uploads per minute, or more than 50 pending (not completed) uploads for the user. |
Rules & side effects
- A
Filerow is created (status: PENDING,visibilityfrom the purpose,originalName,mimeType,sizeBytes,checksumSha256,uploadedById,uploadExpiresAt= now + 10 min). Pending rows pastuploadExpiresAtare purged by an hourly job. A clientidalready used by another file answers409 IDEMPOTENCY_KEY_REUSEDunless it is the replay of the same request. - Replay with the same
Idempotency-Keyand body: the samefileId; if the original URL has expired and the file is still pending, the replay carries a newly signeduploadUrlandexpiresAt(the only fields that change). - Nothing is linked to a business record here: the
fileIdmeans nothing until an operation attaches it.
Used by — camera and picker steps of DA-10 (visit photos), DA-14 (façade photo), RA-28 (store photos and documents), RA-31 (payment proof), RA-33 (ticket and after-sales photos), RA-40 (return photos), avatars on RA-27 and DA-24; product and banner editors BO-28, BO-51; imports BO-67.
POST /files/{fileId}/complete
files.completeUpload · Access session (the uploader only) · Apps RA, RW, DA, BO · Idempotency-Key required · Release V1 · Requirements INT-05, SEC-04
Purpose. Confirms that the bytes are in the storage; the server verifies them, strips metadata, creates the thumbnail and makes the file usable.
Request
| Path parameter | Type | Notes |
|---|---|---|
fileId | Uuid | From files.createUpload. |
No body (send {} or nothing).
Response — 200 OK: a FileRef.
{
"id": "01a1a0b9-4c2d-7e6f-8a1b-2c3d4e5f6a7b",
"url": "https://storage.hoco.dz/hoco-private/visit_photo/2026/11/01a1a0b9-4c2d-7e6f-8a1b-2c3d4e5f6a7b.jpg?X-Amz-Expires=300&X-Amz-Signature=9b0c…",
"thumbnailUrl": "https://storage.hoco.dz/hoco-private/visit_photo/2026/11/01a1a0b9-4c2d-7e6f-8a1b-2c3d4e5f6a7b_t.webp?X-Amz-Expires=300&X-Amz-Signature=e41a…",
"mimeType": "image/jpeg",
"size": 798204,
"purpose": "VISIT_PHOTO"
}size is the stored size after re-encoding, which may differ from the declared one.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id, uploaded by another user, or purged after its upload URL expired. |
| 409 | IDEMPOTENCY_KEY_REUSED | Same key used for another request. |
| 422 | FILE_NOT_UPLOADED | No object in the storage yet (the PUT failed or has not finished). Retry the PUT, then complete again. |
| 422 | FILE_SIZE_MISMATCH | The stored object's size differs from the declared size. The object is deleted; start a new upload. |
| 422 | FILE_TOO_LARGE | The stored object exceeds the purpose's limit. Deleted. |
| 422 | FILE_TYPE_NOT_ALLOWED | The detected content is not an accepted type, or does not match the declared mimeType. Deleted. |
| 422 | FILE_UNREADABLE | Corrupt image or PDF, or an image above 40 megapixels. Deleted. |
Rules & side effects
- Completing an already completed file returns its
FileRefagain (200) — completion is idempotent even without the key. - Processing (checks, orientation, metadata stripping, re-encoding, thumbnail) runs before the answer; the file becomes usable when the call returns. If thumbnail generation exceeds 5 s the file is still completed and
thumbnailUrlis filled by a background job. - Success sets
status: READY,completedAt,width/heightfor images andthumbnailKey. Rejected objects are deleted from the storage immediately and the row is soft-deleted.
Used by — the same screens as files.createUpload, right after the PUT; the delegate outbox before replaying an action that references photos.
GET /files/{fileId}/url
files.downloadUrl · Access session (read rules above) · Apps RA, RW, DA, BO · Idempotency-Key — · Release V1 · Requirements SEC-04
Purpose. Issues a fresh short-lived URL for a file the caller may read — to display it again after the 5-minute URL of a previous response expired, or to download it with its original name.
Request
| Path / query parameter | Required | Values | Notes |
|---|---|---|---|
fileId (path) | yes | Uuid | |
variant | no | original (default), thumbnail | |
download | no | true, false (default) | true → Content-Disposition: attachment with the original file name. |
Response — 200 OK, Cache-Control: private, no-store.
{
"url": "https://storage.hoco.dz/hoco-private/store_document/2026/11/01a1a1c3-8d2e-7f40-9a1b-3c4d5e6f7a8b.pdf?X-Amz-Expires=300&response-content-disposition=attachment%3B%20filename%3D%22RC_Magasin_El_Wafa.pdf%22&X-Amz-Signature=77fd…",
"expiresAt": "2026-11-05T10:20:12Z",
"mimeType": "application/pdf",
"size": 1532044,
"fileName": "RC_Magasin_El_Wafa.pdf"
}For a public file the response carries the CDN URL and expiresAt: null.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown file, or the caller may not read it. |
| 422 | FILE_NOT_READY | The uploader asks for a file not completed yet (others get 404). |
| 422 | VALIDATION_FAILED | variant=thumbnail for a file without thumbnail (PDF, spreadsheet). |
Rules & side effects
- The URL is signed for 5 minutes; opening it after that answers
403from the storage — ask for a new one. - Each URL issued for
STORE_DOCUMENT,PAYMENT_PROOFand exports is written to the request log with user, file and time (kept 90 days), so access to sensitive documents can be traced. - Invoices, delivery notes and credit notes of an order are usually reached through
orders.document, which applies the same rules.
Used by — document lists of RA-28 and RW-12; photo viewers of DA-09 and the back-office reseller 360° BO-11 and visit detail BO-25; proof viewers of BO-45 and BO-56.
Me, devices, app config & reference data
The signed-in user's profile and devices, the start-up configuration of each app (version policy, maintenance, public settings) and the cacheable reference data: wilayas, communes and reference lists.
Notifications & announcements
The in-app inbox, push notifications and their deep links, the live event stream of the reseller and delegate apps, channel preferences and back-office announcements.