{
  "openapi": "3.1.0",
  "info": {
    "title": "DeCA2U ingestion API",
    "version": "1.0.0",
    "summary": "Post a dispatch event and get a DeCA back",
    "description": "The endpoint a dispatch system calls to have an electronic administrative control document (DeCA) issued. It is the same endpoint the capture app itself uses, so there is no second code path.\n\nField names follow the eFTI common data set (Delegated Regulation (EU) 2024/2024). A system that maps its dispatch record to these names has done its eFTI mapping too.\n\nWhat may change inside v1 and what may not is stated in ADR-0025: optional request fields and response fields may be added, validation may be relaxed; nothing may be removed, renamed, made required or tightened.\n\nThis is the ingestion endpoint alone. Correcting, reissuing, the customer's own catalogues and identifier blocks are documented route by route inside the platform, under Administration, for whoever already has an account.",
    "contact": {
      "name": "DRIVEN2U",
      "url": "https://deca2u.com"
    }
  },
  "servers": [
    {
      "url": "https://api.deca2u.com/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Documents",
      "description": "Issuing a DeCA."
    }
  ],
  "paths": {
    "/documents": {
      "post": {
        "tags": ["Documents"],
        "operationId": "issueDocument",
        "summary": "Issue a DeCA for a dispatch",
        "description": "Creates the document, renders its PDF and publishes it at a credential-free URL.\n\nIdempotent on the delivery, not on the caller: the same delivery posted twice, or posted by two systems under two different keys, produces one document. The second call is answered 200 with the document that already exists, not 201.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DispatchEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "This delivery already had a document. The one that exists is returned, and nothing was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssuedDocument"
                }
              }
            }
          },
          "201": {
            "description": "Issued. The document is published and its URL resolves.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssuedDocument"
                }
              }
            }
          },
          "400": {
            "description": "The payload is not a dispatch event: a field has the wrong shape.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidPayload"
                }
              }
            }
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one this platform does not know."
          },
          "403": {
            "description": "The credential may not issue for this tenant or this site."
          },
          "409": {
            "description": "The identifier sent has already been used by another document. Take the next one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "422": {
            "description": "A mandatory article 6 field is missing. The response names which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Incomplete"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "qr",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["1", "true"]
            },
            "description": "Ask for the document code in the response. Off by default: it is some six kilobytes, and a caller that renders its own code from the url does not need it."
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "One key per dispatch system, not one per company. A key is shown once when it is created and stored hashed."
      }
    },
    "schemas": {
      "DispatchEvent": {
        "type": "object",
        "required": ["siteId", "data"],
        "additionalProperties": false,
        "properties": {
          "siteId": {
            "type": "string",
            "minLength": 1,
            "description": "The dispatch point this document is issued at. The tenant is never sent: it comes from the credential."
          },
          "data": {
            "$ref": "#/components/schemas/DecaDataSet",
            "description": "The eight fields of article 6."
          },
          "id": {
            "type": "string",
            "minLength": 22,
            "description": "An identifier from a block this site holds. Sent by a device that issued with no connectivity and already printed it; generated by the platform when absent."
          },
          "externalRef": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "What the calling system calls this transport. Deduplication runs on it: one document per delivery, whichever system published it. Send it. Case and separators are normalised before matching, so M477 52900 and M477-52900 are one delivery. A customer whose own systems write one delivery several ways can be given a resolution rule, after which every one of those ways resolves to the same delivery; ask us before relying on it."
          },
          "deliveryNoteRef": {
            "type": "string",
            "minLength": 1,
            "description": "The reference printed on the delivery note, as printed. Used as the external reference when that is absent, and shown on the document."
          },
          "referenceSource": {
            "$ref": "#/components/schemas/ValueSource",
            "description": "Where the reference came from. A dispatch system sends 'api'; only a channel that decoded a code may claim 'note-qr'."
          },
          "sources": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/ValueSource"
            },
            "description": "Where each field came from, keyed by field name. A dispatch system may send an empty object: everything it sends came from a system."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the document was created at the dispatch point. Kept as sent; publication is stamped by the server, and a divergence is reported rather than corrected."
          },
          "issuedOffline": {
            "type": "boolean",
            "description": "The document was already printed and handed over before this call. Its identifier has to keep resolving even if the delivery turns out to be issued already."
          },
          "clientPdfHash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "SHA-256 of the PDF the caller rendered, hex. Reported back as a match or a mismatch; never a reason to refuse the document."
          }
        }
      },
      "GoodsLine": {
        "type": "object",
        "description": "One article on the transport: what it is, how much of it, and in what it is measured.",
        "required": ["description", "quantity", "unit"],
        "properties": {
          "description": {
            "type": "string",
            "minLength": 1
          },
          "quantity": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "unit": {
            "type": "string",
            "enum": ["kg", "t", "m3", "l", "pallet", "unit", "other"]
          },
          "unitLabel": {
            "type": "string",
            "minLength": 1,
            "maxLength": 16,
            "description": "Required when unit is other, and printed on the document exactly as written. Road haulage measures sacks, drums and big bags, and a system with no fit would otherwise send the nearest wrong unit."
          }
        }
      },
      "DecaDataSet": {
        "type": "object",
        "description": "The eight fields Orden FOM/2861/2012 article 6 fixes. Six are always required; e) and h) are conditional and must never be presented as mandatory.",
        "required": [
          "contractualShipper",
          "effectiveCarrier",
          "location",
          "goods",
          "transportDate",
          "vehicle"
        ],
        "additionalProperties": false,
        "properties": {
          "contractualShipper": {
            "type": "object",
            "description": "a) The contractual shipper.",
            "required": ["name", "taxId", "address"],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1
              },
              "taxId": {
                "$ref": "#/components/schemas/TaxId"
              },
              "address": {
                "type": "string",
                "minLength": 1
              }
            }
          },
          "effectiveCarrier": {
            "type": "object",
            "description": "b) The effective carrier. No delivery-note QR carries this: it comes from the dispatch system or from master data.",
            "required": ["name", "taxId"],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1
              },
              "taxId": {
                "$ref": "#/components/schemas/TaxId"
              }
            }
          },
          "location": {
            "type": "object",
            "description": "c) Origin and destination of the consignment.",
            "required": ["origin", "destination"],
            "properties": {
              "origin": {
                "type": "string",
                "minLength": 1
              },
              "destination": {
                "type": "string",
                "minLength": 1
              }
            }
          },
          "goods": {
            "description": "d) Nature and weight of the goods, one entry per article. Article 6 allows another magnitude where the exact weight cannot be determined at loading, which is what lets a load be documented in cubic metres or in pallets. A lorry carrying three materials is three entries, and the document prints every one of them.\n\nA single article may also be sent as an object; it is read as a list of one.",
            "oneOf": [
              {
                "type": "array",
                "minItems": 1,
                "items": {
                  "$ref": "#/components/schemas/GoodsLine"
                }
              },
              {
                "$ref": "#/components/schemas/GoodsLine"
              }
            ]
          },
          "specialCirculationAuthorisation": {
            "type": "string",
            "minLength": 1,
            "description": "e) Conditional: only when the vehicle travels under one. Send it when it applies and omit it otherwise; never send an empty string."
          },
          "transportDate": {
            "type": "string",
            "format": "date",
            "description": "f) The date of the transport, as yyyy-mm-dd."
          },
          "vehicle": {
            "type": "object",
            "description": "g) The plates. The trailer is required only for an articulated unit.",
            "required": ["tractorPlate", "articulated"],
            "properties": {
              "tractorPlate": {
                "$ref": "#/components/schemas/Plate"
              },
              "articulated": {
                "type": "boolean"
              },
              "trailerPlate": {
                "$ref": "#/components/schemas/Plate"
              }
            }
          },
          "observations": {
            "type": "string",
            "maxLength": 500,
            "description": "h) Conditional: only when a party asks for it. Free text on a document anybody can download with no credentials, so send nothing here that the article does not require."
          }
        }
      },
      "TaxId": {
        "type": "string",
        "pattern": "^[A-Z]?\\d{7,8}[A-Z0-9]$",
        "description": "Spanish tax identifier. The format is checked; the control character is not."
      },
      "Plate": {
        "type": "string",
        "pattern": "^[A-Z]{0,2}[- ]?\\d{4}[- ]?[A-Z]{1,3}$",
        "description": "Registration plate, current or pre-2000 Spanish format."
      },
      "ValueSource": {
        "type": "string",
        "enum": ["note-qr", "printed-block", "master-data", "operator", "api"],
        "description": "How a value was obtained. A decoded code can be trusted; extracted text cannot and is confirmed by an operator before it reaches the document."
      },
      "IssuedDocument": {
        "type": "object",
        "required": ["id", "url", "publishedAt", "publicUntil", "pdfHash"],
        "properties": {
          "id": {
            "type": "string",
            "description": "The document identifier. Opaque."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Where the PDF downloads from, with no credentials. This is what the QR resolves to."
          },
          "qr": {
            "type": "string",
            "description": "The same code the document carries, as an SVG, and only when the request asked for it with ?qr=1. Print it on your own delivery note and the driver shows one piece of paper instead of two. It has no colour and no size of its own: it takes the ink and the box you place it in."
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "publicUntil": {
            "type": "string",
            "format": "date-time",
            "description": "When the credential-free download closes: seven calendar days after the service ends."
          },
          "pdfHash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "clientHashMatches": {
            "type": "boolean",
            "description": "Present when a client hash was sent. False means the caller rendered different bytes from the published file, which is reported and never refused."
          },
          "clockSkewSeconds": {
            "type": "integer",
            "description": "Absolute difference between the creation time sent and server time on publication."
          },
          "referenceUnexpected": {
            "type": "boolean",
            "description": "Present and true when the reference does not have the shape this customer declared. The document was issued anyway. Worth investigating at integration time: a system sending its own order number instead of the delivery reference produces a second document per transport."
          },
          "alreadyIssuedAs": {
            "type": "string",
            "description": "Present on a 200: this delivery was already issued, and this is the document it was issued as. Nothing was created."
          },
          "externalRef": {
            "type": "string",
            "description": "Present on a 200: the reference the delivery was matched on."
          }
        }
      },
      "Incomplete": {
        "type": "object",
        "required": ["error", "missingMandatoryFields", "missing"],
        "properties": {
          "error": {
            "type": "string",
            "const": "incomplete document"
          },
          "missingMandatoryFields": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["a", "b", "c", "d", "f", "g"]
            },
            "description": "Which letters of article 6 are missing, e.g. [\"g\"] for the plates. Letters are what the regulation names and what an operator is asked for. A document with a gap is not a DeCA and is never published."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The same gaps as field paths, e.g. [\"vehicle.tractorPlate\"]. This is the one to build against: a letter does not say which value inside it is missing."
          }
        }
      },
      "InvalidPayload": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "const": "invalid payload"
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Which fields failed and why."
          }
        }
      },
      "Refusal": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          },
          "id": {
            "type": "string"
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "Every refusal has this shape. `issues` carries the field-level detail.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string"
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        }
      }
    }
  }
}
