Skip to content

Pièces jointes & PDF

Pièces jointes

L'API ne reçoit jamais le contenu binaire d'une pièce jointe directement : l'upload passe par une URL pré-signée, et votre client envoie le fichier en direct vers le stockage objet.

Cycle de vie en trois étapes

  1. POST /v1/freightdocuments/{id}/attachments avec type, file_name, mime_type, size_bytes (déclaratifs) et sealed — crée l'entrée (statut PENDING_UPLOAD) et retourne une URL de formulaire (upload_url) et les champs à soumettre avec (upload_fields).
  2. Votre client fait un POST HTTP direct vers upload_url, en formulaire multipart, avec les upload_fields reçus + le fichier lui-même. Ce n'est pas un simple PUT : c'est un POST avec formulaire, et le stockage objet vérifie lui-même que la taille et le type correspondent à ce qui a été autorisé — un fichier trop volumineux ou d'un type différent de celui déclaré à l'étape 1 est rejeté par le stockage avant même d'atteindre l'API.
  3. La pièce jointe passe au statut AVAILABLE une fois confirmée — soit explicitement via POST .../attachments/{attachmentId}/confirm, soit automatiquement au premier GET .../download. Tant qu'elle est PENDING_UPLOAD, elle n'apparaît dans aucune liste et n'est pas téléchargeable.

La taille et le type effectivement enregistrés en base après confirmation sont ceux réellement constatés sur le fichier stocké — jamais ceux que vous aviez déclarés à l'étape 1. Une déclaration incorrecte à la création n'est donc jamais silencieusement acceptée : elle est rattrapée à la confirmation.

file_name reste entièrement à votre charge — l'API n'en invente jamais un à la création. GET /v1/freightdocuments/{id}/attachments/suggested-name (et son équivalent public scopé au nonce de signature, GET /v1/signatures/attachments/suggested-name?nonce=...) retourne un nom par défaut (<référence du document>_<rôle>_<n>) purement informatif, pensé pour être affiché comme suggestion modifiable avant l'envoi — jamais une réservation, et sans effet sur ce que vous soumettez ensuite à l'étape 1.

Types et règles de mutabilité

Type Ajoutable quand
GENERAL Uniquement si le document est DRAFT
GOODS Uniquement si le document est DRAFT
SUPPLEMENT À tout moment — mais n'a aucune valeur légale, jamais couverte par une signature
COMMENT À tout moment. Notamment utilisée par l'écran de signature pour une ou plusieurs photos/documents pris au même moment qu'un commentaire texte optionnel (comment_text) — chaque pièce est uploadée et confirmée indépendamment (circuit attachments classique), puis toutes liées à la signature via attachment_ids dans la requête de complétion, exactement comme le commentaire l'est via comment_text (voir Signature électronique)
POD À tout moment — aucun effet sur le statut, quel que soit le moment où elle est confirmée (voir Cycle de vie).

La suppression (DELETE .../attachments/{attachmentId}) suit une règle différente et plus stricte, et dépend du statut du document :

  • Tant que le document est DRAFT : celui qui a uploadé la pièce, ou le SUBMITTER du document, peut supprimer n'importe quel type de pièce jointe.
  • Une fois le document sorti de DRAFT : seul celui qui a uploadé la pièce peut encore la supprimer, et uniquement si elle est de type SUPPLEMENT ou COMMENT (jamais GENERAL/GOODS, qui restent définitivement non supprimables une fois DRAFT passé — cohérent avec le fait qu'elles ne sont ajoutables qu'en DRAFT). Le SUBMITTER ne bénéficie plus de son droit de suppression général ici — cette fenêtre plus étroite couvre uniquement « annuler ma propre erreur », pas une gestion générale du document.

Chaque pièce jointe retournée par GET .../attachments porte aussi is_own (vrai si l'appelant est celui qui l'a uploadée — la même vérification d'identité que DELETE applique, pour décider s'il est pertinent de proposer une suppression) et uploaded_by_role (le role_type de l'uploadeur sur ce document, jamais un nom personnel — null pour un upload anonyme scopé au nonce de signature, qui n'a aucun compte associé).

Limites de taille

  • 2 Mo maximum par pièce jointe.
  • 10 Mo maximum cumulés par document (pièces confirmées uniquement — une pièce encore PENDING_UPLOAD ne compte pas dans ce total).

Scellement (sealed)

Une pièce jointe peut être marquée sealed à la création. Certains rôles ne la voient tout simplement pas : elle est absente de GET .../attachments, et une tentative de téléchargement renvoie la même erreur attachment.not_found que pour une pièce inexistante — voir Rôles & Permissions pour la liste des rôles concernés.

Types de fichiers acceptés

application/pdf, image/jpeg, image/png, text/csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (xlsx), application/vnd.openxmlformats-officedocument.wordprocessingml.document (docx) — tout autre type est rejeté (attachment.invalid_mime_type), que ce soit à la déclaration ou à la confirmation. Volontairement limité aux formats Open XML modernes (jamais .doc/.xls/.docm/.xlsm) : ces derniers ne peuvent pas transporter de macros, contrairement aux anciens formats binaires — un choix de sécurité, pas seulement de modernité.

Génération du PDF

GET /v1/freightdocuments/{id}/pdf génère le PDF du document, dans le template correspondant à son type (WAYBILL, WAYBILL_ESP, ...). Régénéré à chaque appel — pas de version mise en cache.

  • Repli de template : si aucun template dédié n'existe encore pour le type du document, le template générique est utilisé automatiquement plutôt que de faire échouer la génération.
  • Mention brouillon : tant que le document n'est pas au moins ISSUED, le PDF porte une mention visible explicite. Un document final n'a jamais cette mention, un document DRAFT l'a toujours — il n'existe aucun moyen d'obtenir un PDF DRAFT sans elle.

Le paramètre lang d'avant cette mission (?lang=fr) est toujours accepté pour ne casser aucun appelant existant, mais n'a plus aucun effet — voir la section langue ci-dessous pour ce qui a changé et pourquoi.

PDF versionné (?milestone=)

GET .../pdf accepte un paramètre optionnel milestone, une valeur de SnapshotMilestone (ex. DELIVERY_SIGNED) — voir Jalons d'intégrité documentaire. Fourni, il reconstitue le PDF exactement comme il existait à ce jalon (statut, rôles, marchandises, références, dates telles qu'elles étaient à ce moment-là, signatures et pièces jointes filtrées par leur propre horodatage) plutôt que le contenu courant. Omis, le comportement reste celui d'avant cette mission : toujours le contenu live, régénéré à chaque appel.

Demander un jalon que ce document n'a pas encore atteint renvoie freight_document.snapshot_not_found (404) — le même code que celui déjà utilisé par GET .../snapshots/compare pour la situation identique ("ce jalon n'existe pas encore pour ce document"), pas un code distinct. Un milestone qui ne correspond à aucune valeur réelle de SnapshotMilestone est rejeté en 422 par la validation de requête, avant tout traitement métier.

Mise en page — le formulaire CMR standard à 24 cases

Le PDF respecte la structure à 24 cases numérotées du formulaire CMR papier, plutôt qu'une simple mise en page libre. Une case sans donnée disponible reste affichée, numérotée, vide — jamais omise : la structure visuelle complète est là dès maintenant pour qu'une future mission n'ait qu'à remplir une case déjà positionnée.

Case Donnée Source
1 Expéditeur Rôle CONSIGNOR
2 Destinataire Rôle CONSIGNEE
3 Lieu de livraison Rôle PLACEOFDELIVERY, sinon adresse du CONSIGNEE
4 Lieu et date de prise en charge Rôle PLACEOFTAKINGOVER (sinon CONSIGNOR) + date planifiée/effective
5 Documents annexés Liste des pièces jointes (voir ci-dessus) — une pièce scellée porte une mention "(sealed / scellé / sellado)"
6-12 Marchandises Table goods_lines, une ligne par entrée, dans l'ordre de position
13, 13bis Instructions / réserves de l'expéditeur Vide (aucune donnée backend pour l'instant)
14 Prescriptions d'affranchissement Vide
15 Remboursement Vide
16 Transporteur Rôle CARRIER + vehicle_plate/trailer_plate
17 Transporteurs successifs Rôles SUBSEQUENTCARRIER (liste)
18 Réserves du transporteur Vide
19 Conventions particulières references, affiché en liste clé/valeur brute
20 Modalités de paiement Vide
21 Lieu et date d'établissement Adresse du rôle SUBMITTER + date de création, ou vide si absent
22 Signature de l'expéditeur Signature COLLECTION (prompt 9), si apposée
23 Signature du transporteur Signature COLLECTION_ACCEPTANCE (le chauffeur/transporteur, au moment de la collecte), si apposée
24 Signature du destinataire Signature DELIVERY (prompt 9), si apposée

Le rôle SUBCONTRACTOR n'apparaît dans aucune case — il n'a pas d'équivalent sur le formulaire papier standard et ne doit pas être confondu avec SUBSEQUENTCARRIER (case 17), qui est un concept différent.

Les signatures HANDOVER/ACCEPTANCE (relais transporteur-à-transporteur, prompt 9) n'apparaissent jamais dans les cases 22-24 — c'est volontaire, pas un oubli : le formulaire papier ne modélise aucun relais dans sa zone de signature, c'est précisément le rôle de la case 17. COLLECTION_ACCEPTANCE (case 23) est un type de signature distinct de ce relais, même si les deux sont signés par le rôle CARRIER : elle enregistre l'acceptation par le transporteur de la marchandise collectée sur ce même document, pas un changement de garde entre deux sociétés de transport.

Pour chaque signature affichée, le PDF montre : un badge visuel distinguant clairement une signature ADES (qualifiée) d'une signature SIGN_ON_GLASS — libellée explicitement comme un niveau de preuve réduit, pas comme équivalente — l'horodatage, et le chemin de l'endpoint public de vérification (GET .../signatures/{id}/verify, prompt 9). Depuis cette mission, le nom complet du signataire (Subaccount.display_name) est également affiché à côté de ce badge, sur les trois cases 22/23/24. Ceci n'est pas une exigence confirmée de la Convention CMR (la case 16 papier ne nomme jamais un individu, seulement la société transporteuse) — c'est une pratique rapportée par l'opérateur pour le marché espagnol, en attente de revue juridique.

Langue : libellés trilingues fixes, données jamais traduites

Chaque libellé de case est affiché simultanément en anglais, français et espagnol ("Sender / Expéditeur / Remitente"), en dur — indépendant de tout paramètre de requête. C'est la même logique que le formulaire papier réel : seul le libellé de la case est traduit, jamais la donnée qui y est saisie (le nom d'un expéditeur reste écrit tel quel, dans sa langue d'origine — traduire automatiquement une donnée à valeur contractuelle serait à la fois inutile et risqué).

Le paramètre lang existait avant cette mission pour choisir la langue d'affichage ; il continue d'être accepté pour la compatibilité, mais n'a plus aucun effet sur ce PDF. Le mécanisme de langue par requête reste utilisé ailleurs (interface ecmr-app/ecmr-platform, contenu des notifications) — uniquement plus pour ce document légal.

Lien de partage sans authentification

Pour partager un PDF avec quelqu'un qui n'a pas de compte (un chauffeur au moment de la livraison, par exemple) :

  1. POST /v1/freightdocuments/{id}/pdf/share-link (authentifié, nécessite VIEW) — retourne un token et sa date d'expiration.
  2. GET /v1/freightdocuments/{id}/pdf/share?token={token}endpoint public, aucune authentification requise. Sert le PDF si le token est valide et non expiré.

Un token invalide, expiré, ou destiné à un autre document renvoie systématiquement la même erreur générique (pdf.invalid_share_token) — impossible de distinguer laquelle de ces trois causes s'applique.