HOCO B2B
SRSAPI reference

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.

MethodPathOperationAccessAppsRelease
POST/files/uploadsfiles.createUpload
Request a pre-signed upload URL for a photo or document
session
Idempotency-Key
RA · RW · DA · BOV1
POST/files/{fileId}/completefiles.completeUpload
Confirm an upload; the server checks size/type and creates thumbnails
session
Idempotency-Key
RA · RW · DA · BOV1
GET/files/{fileId}/urlfiles.downloadUrl
Short-lived signed URL for a private file the caller may access
sessionRA · RW · DA · BOV1

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

  1. Prepare on the device. Pick or capture images as JPEG with image_picker imageQuality: 85 and maxWidth/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.
  2. Create. POST /files/uploads with an Idempotency-Key stored with the pending action.
  3. PUT the bytes to uploadUrl with exactly the returned headers (they are part of the signature). The URL expires after 10 minutes; a 403 from the storage means expired or tampered — call createUpload again with the same Idempotency-Key, which returns the same fileId with a freshly signed URL as long as the pending file has not been purged (hourly job); after that, start again with a new key.
  4. Complete. POST /files/{fileId}/complete returns the FileRef.
  5. Attach the fileId in 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.

PurposeBucketMax sizeAccepted typesWho may upload
PRODUCT_IMAGEpublic5 MBJPEG, PNG, WebPStaff with catalog.update
BRAND_LOGOpublic2 MBPNG, WebP, JPEGStaff with catalog.update
BANNER_IMAGEpublic5 MBJPEG, PNG, WebPStaff with promotions.create or promotions.update
STORE_PHOTOprivate5 MBJPEG, PNG, WebPThe reseller (own store), a delegate (resellers in scope), staff with crm.update
VISIT_PHOTOprivate5 MBJPEG, PNG, WebPThe delegate (own visits)
STORE_DOCUMENTprivate10 MBPDF, JPEG, PNGThe reseller (own store), a delegate (resellers in scope), staff with crm.update
PAYMENT_PROOFprivate10 MBPDF, JPEG, PNGThe reseller (own declarations), staff with finance.create
DELIVERY_PROOFprivate10 MBJPEG, PNG, PDFStaff with delivery.update
TICKET_ATTACHMENTprivate10 MBJPEG, PNG, WebP, PDFThe reseller (own tickets, after-sales claims and return requests), staff with support.update
AVATARprivate2 MBJPEG, PNG, WebPAny signed-in user (own avatar)
GENERATED_DOCUMENTprivate—PDF, XLSX, CSVNever uploaded: produced by the server (invoices, delivery notes, credit notes, exports)
IMPORT_FILEprivate20 MBXLSX, CSVStaff 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

PurposeReadable by
PRODUCT_IMAGE, BRAND_LOGO, BANNER_IMAGEEveryone, guests included (public CDN URL).
STORE_PHOTOThe reseller of the store; delegates who have the reseller in scope; staff with crm.view.
VISIT_PHOTOThe delegate who took it; staff with field.view. Never the reseller.
STORE_DOCUMENTThe 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_PROOFThe reseller who declared it; staff with finance.view.
DELIVERY_PROOFThe reseller of the order; staff with delivery.view or orders.view.
TICKET_ATTACHMENTThe reseller of the ticket or return; staff with support.view (tickets, after-sales claims) or orders.view (returns).
AVATARAny signed-in user who is shown the person (it appears in UserSummary.avatarUrl).
GENERATED_DOCUMENTInvoices, delivery notes and credit notes: the reseller concerned and staff with finance.view / orders.view. Exports: the staff user who requested them.
IMPORT_FILEThe 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

  1. 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).
  2. 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.
  3. Capture metadata kept server-side. Before stripping, the EXIF capture time and GPS position of VISIT_PHOTO and STORE_PHOTO files 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.
  4. 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.
  5. Thumbnails. Every image gets a 400 px WebP thumbnail (File.thumbnailKey, served as thumbnailUrl), generated during completeUpload (typically under 1 s). PDFs have no thumbnail in V1; clients show a document icon with the page count unknown.
  6. No antivirus in V1 (Assumption): PDFs are served with Content-Disposition: attachment and X-Content-Type-Options: nosniff, never rendered inline by the API's domains.

URL rules

  • Public files: url is a permanent CDN URL (https://cdn.hoco.dz/products/2026/11/01a1….webp), cacheable forever (file names are immutable ids).
  • Private files: every FileRef.url and thumbnailUrl returned 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 call files.downloadUrl.
  • Caching on mobile: cache images by fileId + variant (for example CachedNetworkImage(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-public or hoco-private, key <purpose>/<yyyy>/<mm>/<fileId>.<ext>. The original name is kept on File.originalName and used for downloads.
  • Hosts storage.hoco.dz and cdn.hoco.dz are 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
}
FieldNotes
headersSend 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.
expiresAt10 minutes after creation.
maxSizeThe purpose's limit in bytes, so the client can refuse early.

Errors

StatusCodeWhen
400VALIDATION_FAILEDMissing fields, size not positive, malformed checksum, file name too long.
403FILE_PURPOSE_NOT_ALLOWEDThe caller may not upload this purpose (a reseller sending PRODUCT_IMAGE, anyone sending GENERATED_DOCUMENT).
409IDEMPOTENCY_KEY_REUSEDSame key with a different body.
422FILE_TYPE_NOT_ALLOWEDmimeType not accepted for the purpose (details.allowed).
422FILE_TOO_LARGEsize above the purpose's limit (details.maxSize).
429RATE_LIMITEDMore than 60 uploads per minute, or more than 50 pending (not completed) uploads for the user.

Rules & side effects

  1. A File row is created (status: PENDING, visibility from the purpose, originalName, mimeType, sizeBytes, checksumSha256, uploadedById, uploadExpiresAt = now + 10 min). Pending rows past uploadExpiresAt are purged by an hourly job. A client id already used by another file answers 409 IDEMPOTENCY_KEY_REUSED unless it is the replay of the same request.
  2. Replay with the same Idempotency-Key and body: the same fileId; if the original URL has expired and the file is still pending, the replay carries a newly signed uploadUrl and expiresAt (the only fields that change).
  3. Nothing is linked to a business record here: the fileId means 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 parameterTypeNotes
fileIdUuidFrom 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

StatusCodeWhen
404NOT_FOUNDUnknown id, uploaded by another user, or purged after its upload URL expired.
409IDEMPOTENCY_KEY_REUSEDSame key used for another request.
422FILE_NOT_UPLOADEDNo object in the storage yet (the PUT failed or has not finished). Retry the PUT, then complete again.
422FILE_SIZE_MISMATCHThe stored object's size differs from the declared size. The object is deleted; start a new upload.
422FILE_TOO_LARGEThe stored object exceeds the purpose's limit. Deleted.
422FILE_TYPE_NOT_ALLOWEDThe detected content is not an accepted type, or does not match the declared mimeType. Deleted.
422FILE_UNREADABLECorrupt image or PDF, or an image above 40 megapixels. Deleted.

Rules & side effects

  1. Completing an already completed file returns its FileRef again (200) — completion is idempotent even without the key.
  2. 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 thumbnailUrl is filled by a background job.
  3. Success sets status: READY, completedAt, width/height for images and thumbnailKey. 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 parameterRequiredValuesNotes
fileId (path)yesUuid
variantnooriginal (default), thumbnail
downloadnotrue, 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

StatusCodeWhen
404NOT_FOUNDUnknown file, or the caller may not read it.
422FILE_NOT_READYThe uploader asks for a file not completed yet (others get 404).
422VALIDATION_FAILEDvariant=thumbnail for a file without thumbnail (PDF, spreadsheet).

Rules & side effects

  1. The URL is signed for 5 minutes; opening it after that answers 403 from the storage — ask for a new one.
  2. Each URL issued for STORE_DOCUMENT, PAYMENT_PROOF and exports is written to the request log with user, file and time (kept 90 days), so access to sensitive documents can be traced.
  3. 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.

On this page