{
  "openapi": "3.0.0",
  "paths": {
    "/public/documents": {
      "get": {
        "description": "Issued documents of one kind, oldest change first. Store `nextCursor` and pass it on the next run to receive only what changed since; deduplicate by `type` + `id`, because a document is listed again whenever it changes. Changes become visible after about 30 seconds.\n\nFirst run: pass `updatedSince` (or nothing, to start from the beginning). Every run after that: pass the last `nextCursor` as `cursor`. The two are not combinable, and a cursor belongs to the `type` it was issued for. While `hasMore` is true, call again right away.",
        "operationId": "listPublicDocuments",
        "parameters": [
          {
            "name": "type",
            "required": true,
            "in": "query",
            "description": "Which kind of document to list. Each kind is its own feed with its own cursor.",
            "schema": {
              "type": "string",
              "enum": [
                "INVOICE",
                "CREDIT_NOTE",
                "CANCELLATION"
              ]
            }
          },
          {
            "name": "updatedSince",
            "required": false,
            "in": "query",
            "description": "Only documents changed at or after this instant (ISO 8601). Starting point of a first sync; omit to start from the beginning. Not combinable with `cursor`.",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "required": false,
            "in": "query",
            "description": "Opaque `nextCursor` of the previous page. Store the last one you received and pass it on the next run to receive only what changed since.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Page size",
            "schema": {
              "minimum": 1,
              "maximum": 100,
              "default": 50,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicDocumentListResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query, e.g. `cursor` and `updatedSince` together, or a cursor of another document type. Fix the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, invalid, expired or revoked. Retrying does not help — create a new token.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (per token: 60 list calls and 120 PDF downloads per minute, 25,000 calls per day; per IP: 120 requests per minute and route). Wait as many seconds as `Retry-After` says.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before the next attempt",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          },
          "503": {
            "description": "The rate limiter is temporarily unavailable. Try again after `Retry-After`.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before the next attempt",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          }
        ],
        "summary": "List issued documents (change feed)",
        "tags": [
          "Public Documents API"
        ]
      }
    },
    "/public/documents/{id}/pdf": {
      "get": {
        "description": "The `ETag` is the SHA-256 of the PDF bytes: identical across downloads while the stored PDF is unchanged, so it can serve as a content fingerprint (a re-rendered PDF gets a new one, and the document reappears in the feed). 404 for any id that is not an issued document of this shop.",
        "operationId": "downloadPublicDocumentPdf",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Document id, as listed",
            "schema": {
              "example": "cm2x8k1q40001",
              "type": "string"
            }
          },
          {
            "name": "type",
            "required": true,
            "in": "query",
            "description": "Kind of the document, as listed",
            "schema": {
              "type": "string",
              "enum": [
                "INVOICE",
                "CREDIT_NOTE",
                "CANCELLATION"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The PDF, as an attachment",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "Quoted SHA-256 of the PDF bytes",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "description": "attachment; filename=\"invoice-RE-2909.pdf\" (prefix `invoice-`, `credit-note-` or `cancellation-invoice-`)",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid `type`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, invalid, expired or revoked. Retrying does not help — create a new token.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          },
          "404": {
            "description": "No issued document with this id and type in the token’s shop",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (per token: 60 list calls and 120 PDF downloads per minute, 25,000 calls per day; per IP: 120 requests per minute and route). Wait as many seconds as `Retry-After` says.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before the next attempt",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          },
          "503": {
            "description": "The rate limiter is temporarily unavailable. Try again after `Retry-After`.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before the next attempt",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorDto"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          }
        ],
        "summary": "Download the PDF of an issued document",
        "tags": [
          "Public Documents API"
        ]
      }
    }
  },
  "info": {
    "title": "Easy Invoices Documents API",
    "description": "Read-only API through which a shop’s own systems fetch the documents Easy Invoices issued for it — invoices, credit notes and cancellation invoices — with their PDFs.\n\n**Authentication.** Every request carries one of the shop’s API tokens as `Authorization: Bearer ei_live_…`. The shop owner creates it in the app under Settings → Advanced → Manage API access. Tokens are valid for 30 to 365 days and can be revoked at any time.\n\n**Syncing.** Each document type is its own change feed. First run: `updatedSince` (or nothing). Every run after that: the last `nextCursor` as `cursor`. A document is listed again whenever it changes, so deduplicate by `type` + `id`.\n\n**Limits.** Per token: 60 list calls and 120 PDF downloads per minute, 25,000 calls per day. Per IP address: 120 requests per minute and route.",
    "version": "1",
    "contact": {}
  },
  "tags": [],
  "servers": [
    {
      "url": "https://api.easy-invoices.de/api/v1",
      "description": "Production"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "scheme": "bearer",
        "bearerFormat": "ei_live_",
        "type": "http",
        "description": "A token of the shop, starting with `ei_live_`. Created by the shop owner in the app; shown once."
      }
    },
    "schemas": {
      "PublicOrderAttributeDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Attribute name, e.g. \"Order_id\"",
            "example": "Order_id"
          },
          "value": {
            "type": "string",
            "description": "Attribute value, e.g. \"COM-277476493-2-A\"",
            "example": "COM-277476493-2-A"
          }
        },
        "required": [
          "name",
          "value"
        ]
      },
      "PublicDocumentDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable id of the document. Use it (with `type`) as your deduplication key — it never changes, and a document listed twice is the same document.",
            "example": "cm2x8k1q40001"
          },
          "type": {
            "type": "string",
            "enum": [
              "INVOICE",
              "CREDIT_NOTE",
              "CANCELLATION"
            ]
          },
          "number": {
            "type": "string",
            "description": "Document number as printed, e.g. \"RE-26392\"",
            "example": "RE-2909"
          },
          "issueDate": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-28T09:14:00.000Z"
          },
          "status": {
            "type": "string",
            "description": "Processing status. Only documents whose PDF has been produced are listed, so every listed document is downloadable.",
            "example": "EMAIL_SENT"
          },
          "isActive": {
            "type": "boolean",
            "description": "False once an invoice has been replaced by a corrected one (it then stays listed, with this flag, so a consumer can withdraw it). Always true for credit notes and cancellations.",
            "example": true
          },
          "orderId": {
            "type": "string",
            "description": "Shopify order id (numeric) the document belongs to. For a credit note or cancellation, the order of the invoice it corrects; null for a credit note without one.",
            "nullable": true,
            "example": "8594129453322"
          },
          "orderNumber": {
            "type": "string",
            "description": "Shopify order number (`order_number`, e.g. \"2901\"), WITHOUT the prefix the shop may print in front of it (\"TN2901\"). Match on `orderId` where you can.",
            "nullable": true,
            "example": "2909"
          },
          "orderAttributes": {
            "description": "Additional details of the Shopify order as it stood when the invoice was issued — where sales-channel connectors store their own order id (Mirakl Connect: `Order_id`, e.g. \"COM-277476493-2-A\"; eBay: `eBay Order Id`). For a credit note or cancellation, those of the invoice it corrects. Attributes whose value contains an email address are left out. Empty when the order carried none.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicOrderAttributeDto"
            }
          },
          "orderTags": {
            "description": "Tags of the Shopify order as it stood when the invoice was issued (same source and rules as `orderAttributes`).",
            "example": [
              "COM-277476493-2-A",
              "Mirakl",
              "Shop Apotheke"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "relatedInvoiceId": {
            "type": "string",
            "description": "For a credit note or cancellation: id of the invoice it corrects (type INVOICE). Null for an invoice.",
            "nullable": true,
            "example": null
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 currency",
            "example": "EUR"
          },
          "totalGross": {
            "type": "string",
            "description": "Gross total as a decimal string, e.g. \"20.90\"",
            "example": "41.80"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-28T09:14:04.000Z"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last change; the feed is ordered by this",
            "format": "date-time",
            "example": "2026-10-28T09:15:12.000Z"
          },
          "pdfUrl": {
            "type": "string",
            "description": "Absolute URL of the PDF download (same token)",
            "format": "uri",
            "example": "https://api.easy-invoices.de/api/v1/public/documents/cm2x8k1q40001/pdf?type=INVOICE"
          }
        },
        "required": [
          "id",
          "type",
          "number",
          "issueDate",
          "status",
          "isActive",
          "orderId",
          "orderNumber",
          "orderAttributes",
          "orderTags",
          "relatedInvoiceId",
          "currency",
          "totalGross",
          "createdAt",
          "updatedAt",
          "pdfUrl"
        ]
      },
      "PublicDocumentListResponseDto": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicDocumentDto"
            }
          },
          "nextCursor": {
            "type": "string",
            "description": "Pass as `cursor` to continue. Present on EVERY page, including the last one: store it, and the next run receives only documents changed since. `hasMore` says whether to fetch again right away.",
            "example": "eyJ2IjoxLCJ0IjoiSU5WT0lDRSIsInUiOiIyMDI2LTEwLTI4VDA5OjE1OjEyLjAwMFoiLCJpIjoiY20yeDhrMXE0MDAwMSJ9"
          },
          "hasMore": {
            "type": "boolean",
            "description": "Whether another page is already waiting",
            "example": false
          }
        },
        "required": [
          "data",
          "nextCursor",
          "hasMore"
        ]
      },
      "PublicApiErrorDto": {
        "type": "object",
        "properties": {
          "statusCode": {
            "type": "integer",
            "description": "HTTP status",
            "example": 401
          },
          "timestamp": {
            "type": "string",
            "description": "When the error occurred",
            "format": "date-time",
            "example": "2026-10-28T09:15:12.000Z"
          },
          "path": {
            "type": "string",
            "description": "Path and query of the request",
            "example": "/api/v1/public/documents?type=INVOICE"
          },
          "message": {
            "type": "string",
            "description": "Human-readable reason. Do not parse it; branch on the HTTP status.",
            "example": "Invalid or missing API token."
          }
        },
        "required": [
          "statusCode",
          "timestamp",
          "path",
          "message"
        ]
      }
    }
  },
  "externalDocs": {
    "description": "API reference",
    "url": "https://easy-invoices.de/en/documentation/api"
  }
}
