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
POST /v1/freightdocuments/{id}/attachmentsavectype,file_name,mime_type,size_bytes(déclaratifs) etsealed— crée l'entrée (statutPENDING_UPLOAD) et retourne une URL de formulaire (upload_url) et les champs à soumettre avec (upload_fields).- Votre client fait un
POSTHTTP direct versupload_url, en formulaire multipart, avec lesupload_fieldsreçus + le fichier lui-même. Ce n'est pas un simplePUT: 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. - La pièce jointe passe au statut
AVAILABLEune fois confirmée — soit explicitement viaPOST .../attachments/{attachmentId}/confirm, soit automatiquement au premierGET .../download. Tant qu'elle estPENDING_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 leSUBMITTERdu 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 typeSUPPLEMENTouCOMMENT(jamaisGENERAL/GOODS, qui restent définitivement non supprimables une foisDRAFTpassé — cohérent avec le fait qu'elles ne sont ajoutables qu'enDRAFT). LeSUBMITTERne 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_UPLOADne 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 documentDRAFTl'a toujours — il n'existe aucun moyen d'obtenir un PDFDRAFTsans elle.
Le paramètre
langd'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) :
POST /v1/freightdocuments/{id}/pdf/share-link(authentifié, nécessiteVIEW) — retourne untokenet sa date d'expiration.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.