{
  "openapi": "3.1.0",
  "info": {
    "title": "eDocument Tools Validation API",
    "version": "1.0.0",
    "summary": "Validate electronic invoices against pinned, versioned rule sets.",
    "description": "Server-to-server validation of electronic business documents.\n\nA document that runs and fails validation is **HTTP 200** with a report saying so.\nHTTP status describes whether the API executed the request, never what it concluded\nabout an invoice.\n\nDocuments sent to this API are uploaded on purpose, processed in memory and never\nretained. This is a different privacy model from the browser tools, where documents\nnever leave the device. See docs/API.md.",
    "contact": {
      "url": "https://edocumenttools.com/en/contact"
    }
  },
  "servers": [
    {
      "url": "https://edocumenttools.com/api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "validation"
    },
    {
      "name": "discovery"
    }
  ],
  "paths": {
    "/validate": {
      "post": {
        "tags": [
          "validation"
        ],
        "operationId": "validateDocument",
        "summary": "Validate one document against one target.",
        "description": "Exactly one file per request. The target is required: there is no automatic country or profile selection, because two official schemas can share a root element and guessing between them would report a verdict the caller never asked for.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "target",
                  "file"
                ],
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "A public target id from `GET /targets`.",
                    "enum": [
                      "ubl-2.1-syntax",
                      "en16931-1.3.16-ubl-2.1",
                      "france-rfe-1.4.0.04-en16931-ubl-2.1",
                      "france-rfe-1.4.0.04-extended-ctc-fr-ubl-2.1",
                      "france-rfe-1.4.0.04-en16931-cii-d22b",
                      "france-rfe-1.4.0.04-extended-ctc-fr-cii-d22b",
                      "factur-x-1.09.2-basic-wl",
                      "factur-x-1.09.2-en16931",
                      "factur-x-1.09.2-extended",
                      "greece-mydata-2.0.2-invoicesdoc",
                      "greece-mydata-2.0.2-invoicesdoc-detailed",
                      "greece-mydata-2.0.2-requesteddoc",
                      "greece-mydata-2.0.2-responsedoc",
                      "greece-mydata-2.0.2-requestedproviderdoc",
                      "greece-mydata-2.0.2-requestedstatementdoc",
                      "greece-mydata-2.0.2-statementdoc",
                      "greece-mydata-2.0.2-incomeclassificationsdoc",
                      "greece-mydata-2.0.2-expensesclassificationsdoc",
                      "greece-mydata-2.0.2-paymentmethodsdoc",
                      "greece-mydata-2.0.2-requestede3info",
                      "greece-mydata-2.0.2-requestedvatinfo",
                      "greece-mydata-2.0.2-transport",
                      "greece-mydata-2.0.2-confirmdeliveryoutcomerequest",
                      "greece-mydata-2.0.2-confirmdeliveryreturnrequest",
                      "greece-mydata-2.0.2-rejectdeliverynoterequest",
                      "greece-mydata-2.0.2-getdeliverynotestatusresponse",
                      "greece-mydata-2.0.2-generategroupqrcoderequest",
                      "greece-mydata-2.0.2-generategroupqrcoderesponse",
                      "greece-mydata-2.0.2-requestgroupqrdetailsresponse"
                    ]
                  },
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The document. XML or PDF, decided from its bytes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation executed. The document may be valid or invalid; read the report.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request could not be understood — a missing field, an unknown field, or a body that is not multipart.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No API key, or one that is not valid.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key is valid but is not authorised for the requested target.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such API route, or the API is not enabled on this deployment.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The upload is larger than the global cap or than this target accepts.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "The uploaded bytes are not the kind this target validates. Decided from the bytes, not the declared type.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The target is unknown, the document does not suit it, or the document could not be read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Two different limits answer with this status, and the body’s `code` says which.\n\n`rate_limited` is the per-key request rate. `Retry-After` gives the seconds until the\nwindow rolls, which is under a minute.\n\n`quota_exceeded` is the account’s daily validation allowance. `Retry-After` gives the\nseconds until 00:00 UTC, when it resets. `X-Quota-Limit` and `X-Quota-Remaining` report\nwhere the account stands; they are advisory and are also sent on a successful response.\n\nRetrying sooner will not help in either case, and the two need different handling:\none is a reason to slow down, the other is a reason to wait for the reset or ask for\nmore allowance.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "The request could not be completed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "No validation capacity, or the service is not ready.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "504": {
            "description": "Validation did not finish within the time limit.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/targets": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "listTargets",
        "summary": "Every validation target this deployment can run.",
        "security": [],
        "responses": {
          "200": {
            "description": "The public target catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TargetsResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such API route, or the API is not enabled on this deployment.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "discovery"
        ],
        "operationId": "health",
        "summary": "Whether the API is accepting work.",
        "security": [],
        "responses": {
          "200": {
            "description": "Accepting work.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Not ready — persistence or the validator is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An operator-issued key, sent as `Authorization: Bearer <key>`. Server-to-server only: this API sends no CORS headers, and a key in browser JavaScript is a published secret."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. `code` is the stable contract; the prose may be reworded.",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "requestId"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "api_disabled",
              "unauthorized",
              "forbidden_target",
              "not_found",
              "invalid_request",
              "duplicate_field",
              "unknown_target",
              "target_input_mismatch",
              "unsupported_media_type",
              "document_unreadable",
              "payload_too_large",
              "rate_limited",
              "quota_exceeded",
              "capacity_unavailable",
              "validation_timeout",
              "internal_error"
            ]
          },
          "detail": {
            "type": "string"
          },
          "requestId": {
            "type": "string",
            "pattern": "^req_[0-9a-f]{32}$"
          }
        }
      },
      "Check": {
        "type": "object",
        "required": [
          "layer",
          "label",
          "status",
          "errorCount",
          "warningCount"
        ],
        "properties": {
          "layer": {
            "type": "string",
            "enum": [
              "XSD",
              "EN16931",
              "COUNTRY_PROFILE"
            ]
          },
          "label": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "PASSED",
              "FAILED",
              "WARNING",
              "NOT_RUN",
              "SKIPPED",
              "UNSUPPORTED"
            ],
            "description": "A layer that did not run says so. It never inherits the previous layer’s success."
          },
          "detail": {
            "type": [
              "string",
              "null"
            ]
          },
          "ruleSetId": {
            "type": [
              "string",
              "null"
            ]
          },
          "ruleSetVersion": {
            "type": [
              "string",
              "null"
            ]
          },
          "errorCount": {
            "type": "integer"
          },
          "warningCount": {
            "type": "integer"
          },
          "durationMs": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "Finding": {
        "type": "object",
        "required": [
          "severity",
          "message",
          "source"
        ],
        "properties": {
          "ruleId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The official rule identifier, untranslated. The stable machine contract."
          },
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warning",
              "info"
            ]
          },
          "message": {
            "type": "string",
            "description": "English. Not a stable contract; use ruleId."
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string"
          },
          "explanation": {
            "type": [
              "string",
              "null"
            ]
          },
          "remediation": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Target": {
        "type": "object",
        "required": [
          "id",
          "name",
          "family",
          "version",
          "input",
          "layers",
          "maxBytes",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "family": {
            "type": "string",
            "enum": [
              "generic",
              "france",
              "factur-x",
              "greece"
            ]
          },
          "jurisdiction": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": "string"
          },
          "input": {
            "type": "string",
            "enum": [
              "xml",
              "pdf"
            ]
          },
          "documentRoots": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "layers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "maxBytes": {
            "type": "integer"
          },
          "limitations": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What this target does NOT establish. Read them before quoting a result."
          },
          "status": {
            "type": "string",
            "enum": [
              "available"
            ]
          }
        }
      },
      "TargetsResponse": {
        "type": "object",
        "required": [
          "apiVersion",
          "requestId",
          "targets"
        ],
        "properties": {
          "apiVersion": {
            "type": "string",
            "const": "v1"
          },
          "requestId": {
            "type": "string"
          },
          "targets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Target"
            }
          }
        }
      },
      "ValidationResponse": {
        "type": "object",
        "required": [
          "apiVersion",
          "requestId",
          "target",
          "input",
          "result"
        ],
        "properties": {
          "apiVersion": {
            "type": "string",
            "const": "v1"
          },
          "requestId": {
            "type": "string"
          },
          "target": {
            "type": "object",
            "required": [
              "id",
              "family",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "family": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "input": {
            "type": "object",
            "required": [
              "kind",
              "sizeBytes"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "xml",
                  "pdf"
                ]
              },
              "sizeBytes": {
                "type": "integer"
              }
            }
          },
          "result": {
            "type": "object",
            "required": [
              "valid",
              "partial",
              "checks",
              "findings",
              "findingCount"
            ],
            "properties": {
              "valid": {
                "type": "boolean",
                "description": "True only when every requested layer ran and passed. A report with an unsupported layer is never valid — read the layers, not this flag."
              },
              "partial": {
                "type": "boolean",
                "description": "At least one layer did not produce a verdict."
              },
              "detectedProfile": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ruleSetVersions": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "checks": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Check"
                }
              },
              "findings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Finding"
                }
              },
              "findingCount": {
                "type": "integer",
                "description": "Total the validation produced."
              },
              "returnedFindingCount": {
                "type": "integer"
              },
              "findingsTruncated": {
                "type": "boolean"
              },
              "durationMs": {
                "type": "number"
              }
            }
          }
        }
      }
    }
  }
}
