{
  "openapi": "3.0.3",
  "info": {
    "title": "SAPI-SK - Standardised Access Point Interface (Slovakia)",
    "version": "1.0.0",
    "contact": {
      "name": "Slovak OpenPeppol Community"
    },
    "description": "SAPI-SK v1.0 defines the standardised interface between Peppol Access Points and business software in Slovakia.\n\nEach interaction (Authentication, Send, Receive, Receipt) defines its own normative request/response schema. There is no mandatory universal envelope wrapper across all endpoints.\n\nAll error responses use a consistent structure with a structured SAPIError object (category, code, message, retryable, correlation_id) for consistent error handling across all interactions.\n\nError categories: AUTH (authentication/authorization), VALIDATION (request validation), PROCESSING (server-side), TEMPORARY (transient, retryable), PERMANENT (non-retryable).\n\nHTTP status mapping: 400=VALIDATION, 401/403=AUTH, 404/422=VALIDATION, 409=PROCESSING, 423=AUTH, 429/502/503/504=TEMPORARY, 500=PROCESSING."
  },
  "servers": [
    {
      "url": "/sapi",
      "description": "SAPI Base URL"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Client authentication and token management"
    },
    {
      "name": "Documents",
      "description": "Document exchange operations (requires authentication)"
    }
  ],
  "paths": {
    "/auth/token": {
      "post": {
        "tags": ["Authentication"],
        "summary": "Obtain access and refresh tokens",
        "description": "Exchange your client credentials for JWT tokens using OAuth 2.0 client_credentials grant. The access token is used for API calls (valid for 15 minutes). The refresh token is used to obtain new tokens (valid for 30 days).",
        "operationId": "getToken",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["client_id", "client_secret", "grant_type"],
                "properties": {
                  "client_id": {
                    "type": "string",
                    "description": "Your client identifier. Use demo_swagger_client for testing.",
                    "example": "demo_swagger_client"
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "Your client secret. Use demo_secret_swagger_12345 for testing.",
                    "example": "demo_secret_swagger_12345"
                  },
                  "grant_type": {
                    "type": "string",
                    "description": "OAuth 2.0 grant type. Must be 'client_credentials'.",
                    "enum": ["client_credentials"],
                    "example": "client_credentials"
                  },
                  "scope": {
                    "type": "string",
                    "description": "Optional scope for the access token",
                    "example": "document:send document:receive"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens issued successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials or client not approved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponseEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "IP address not allowed for this client",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponseEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "Account temporarily locked due to too many failed attempts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponseEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/auth/token/status": {
      "get": {
        "tags": ["Authentication"],
        "summary": "Check token validity and expiration",
        "description": "Check the status of your current access token. Use this to determine when to refresh your token proactively.",
        "operationId": "getTokenStatus",
        "security": [{"bearerAuth": []}],
        "responses": {
          "200": {
            "description": "Token status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenStatus"
                }
              }
            }
          },
          "401": {
            "description": "Token is invalid or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponseEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/auth/renew": {
      "post": {
        "tags": ["Authentication"],
        "summary": "Renew tokens using refresh token",
        "description": "Exchange your refresh token for new access and refresh tokens. Token Rotation: The old refresh token is invalidated and a new one is issued for security.",
        "operationId": "renewToken",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["refresh_token"],
                "properties": {
                  "refresh_token": {
                    "type": "string",
                    "description": "Your current refresh token",
                    "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New tokens issued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "401": {
            "description": "Refresh token is invalid, expired, or revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponseEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/auth/revoke": {
      "post": {
        "tags": ["Authentication"],
        "summary": "Revoke refresh tokens",
        "description": "Revoke a refresh token to prevent it from being used. Use this when logging out or if you suspect a token has been compromised.",
        "operationId": "revokeToken",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The refresh token to revoke"
                  },
                  "token_type_hint": {
                    "type": "string",
                    "enum": ["refresh_token"],
                    "default": "refresh_token"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token revoked successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {"type": "boolean", "example": true},
                    "message": {"type": "string", "example": "Token revoked successfully"},
                    "timestamp": {"type": "string", "format": "date-time"}
                  }
                }
              }
            }
          }
        }
      }
    },
    "/document/send": {
      "post": {
        "tags": ["Documents"],
        "summary": "Submit a document for Peppol delivery",
        "description": "Submit a single electronic business document for processing and delivery via Peppol network.",
        "operationId": "sendDocument",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique key for idempotent document submission. Same key + same payload returns the original response. Same key + different payload returns 409 Conflict. Keys expire after 24 hours.",
            "schema": { "type": "string", "format": "uuid", "example": "550e8400-e29b-41d4-a716-446655440000" }
          },
          {
            "name": "X-Peppol-Participant-Id",
            "in": "header",
            "required": true,
            "description": "Sender's Peppol Participant ID (organization context). Must match metadata.senderParticipantId in the request body. Use 0245:1234567890 for demo testing.",
            "schema": { "type": "string", "example": "0245:1234567890" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendDocumentRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Document accepted for processing",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SendDocumentResponse" }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          },
          "401": {
            "description": "Authentication error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          },
          "403": {
            "description": "Authorization error (no org access)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          },
          "409": {
            "description": "Conflict - Idempotency-Key has already been used with a different request payload",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          }
        }
      }
    },
    "/document/receive": {
      "get": {
        "tags": ["Documents"],
        "summary": "List received documents",
        "description": "Retrieve a cursor-paginated list of documents received via Peppol network for your organization. Documents are returned in ascending order by received date (oldest first) per SAPI specification. Use pageToken for cursor-based pagination. This operation does not imply acknowledgement or state transition.",
        "operationId": "listReceivedDocuments",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "X-Peppol-Participant-Id",
            "in": "header",
            "required": true,
            "description": "Receiver's Peppol Participant ID (organization context). Use 0245:1234567890 for demo testing.",
            "schema": { "type": "string", "example": "0245:1234567890" }
          },
          {
            "name": "pageToken",
            "in": "query",
            "required": false,
            "description": "Opaque cursor token for the next page. Obtained from the nextPageToken field of a previous response.",
            "schema": { "type": "string" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of records to return per page",
            "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 100 }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter by document status",
            "schema": { "type": "string", "enum": ["RECEIVED", "ACKNOWLEDGED"] }
          }
        ],
        "responses": {
          "200": {
            "description": "List of received documents",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReceivedDocumentListResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          },
          "403": {
            "description": "Authorization error (no org access)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          }
        }
      }
    },
    "/document/receive/{documentId}": {
      "get": {
        "tags": ["Documents"],
        "summary": "Retrieve received document",
        "description": "Retrieve the full details and content of a specific received document. The document content is returned as UTF-8 XML. This operation does not imply acknowledgement or state transition.",
        "operationId": "getReceivedDocument",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "documentId",
            "in": "path",
            "required": true,
            "description": "The document identifier. Copy from the 'Seed Demo Credentials' response above.",
            "schema": { "type": "string" }
          },
          {
            "name": "X-Peppol-Participant-Id",
            "in": "header",
            "required": true,
            "description": "Receiver's Peppol Participant ID (must match document receiver)",
            "schema": { "type": "string", "example": "0245:1234567890" }
          }
        ],
        "responses": {
          "200": {
            "description": "Document retrieved successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReceivedDocumentDetailResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          },
          "403": {
            "description": "Authorization error (document belongs to different organization)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          },
          "404": {
            "description": "Document not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          }
        }
      }
    },
    "/document/receive/{documentId}/acknowledge": {
      "post": {
        "tags": ["Documents"],
        "summary": "Acknowledge document receipt",
        "description": "This marks the document as ACKNOWLEDGED. The operation is idempotent - acknowledging an already-acknowledged document returns success. This operation confirms successful handover to the software endpoint.",
        "operationId": "acknowledgeReceivedDocument",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "documentId",
            "in": "path",
            "required": true,
            "description": "The provider document ID to acknowledge. Copy from the 'Seed Demo Credentials' response above.",
            "schema": { "type": "string" }
          },
          {
            "name": "X-Peppol-Participant-Id",
            "in": "header",
            "required": true,
            "description": "Receiver's Peppol Participant ID (must match document receiver)",
            "schema": { "type": "string", "example": "0245:1234567890" }
          }
        ],
        "responses": {
          "200": {
            "description": "Document acknowledged successfully",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AcknowledgeResponse" }
              }
            }
          },
          "401": {
            "description": "Authentication error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          },
          "403": {
            "description": "Authorization error (document belongs to different organization)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          },
          "404": {
            "description": "Document not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponseEnvelope" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "JWT access token from /auth/token or /auth/renew"
      }
    },
    "schemas": {
      "SAPIError": {
        "type": "object",
        "required": ["category", "code", "message", "retryable", "correlation_id"],
        "description": "SAPI v1.0 structured error object per Error & Status Model specification. All error responses use this consistent structure for machine-readable error handling, explicit retry behaviour, and Peppol-aligned diagnostics.",
        "properties": {
          "category": {
            "type": "string",
            "enum": ["AUTH", "VALIDATION", "PROCESSING", "TEMPORARY", "PERMANENT"],
            "description": "Error category. AUTH = authentication/authorization failures (client-resolvable). VALIDATION = request validation errors (detected before processing). PROCESSING = server-side processing errors (may be retryable). TEMPORARY = transient failures (retryable with backoff). PERMANENT = non-retryable failures (corrective action required).",
            "example": "VALIDATION"
          },
          "code": {
            "type": "string",
            "description": "Stable, implementation-independent error code (e.g. SAPI-VAL-001, SAPI-AUTH-001)",
            "example": "SAPI-VAL-001"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of the error",
            "example": "Document payload is invalid"
          },
          "details": {
            "type": "array",
            "description": "Optional field-level or contextual information to aid diagnostics",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string",
                  "description": "The field that caused the error",
                  "example": "document.type"
                },
                "issue": {
                  "type": "string",
                  "description": "Description of the issue",
                  "example": "Unsupported value"
                },
                "value": {
                  "type": "string",
                  "description": "The invalid value that was provided"
                }
              }
            }
          },
          "retryable": {
            "type": "boolean",
            "description": "Explicit indicator whether the request may be retried without modification. Clients MUST only retry when true and MUST implement exponential backoff.",
            "example": false
          },
          "correlation_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifier used for tracing, logging, and support"
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "required": ["access_token", "token_type", "expires_in"],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "JWT access token for API calls (valid for 15 minutes)"
          },
          "token_type": {
            "type": "string",
            "enum": ["Bearer"],
            "example": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "description": "Access token lifetime in seconds",
            "example": 900
          },
          "scope": {
            "type": "string",
            "description": "Granted scope (if requested)",
            "example": "document:send document:receive"
          }
        }
      },
      "TokenStatus": {
        "type": "object",
        "properties": {
          "valid": {
            "type": "boolean",
            "description": "Whether the token is currently valid",
            "example": true
          },
          "token_type": {
            "type": "string",
            "enum": ["access"],
            "example": "access"
          },
          "client_id": {
            "type": "string",
            "description": "The client ID this token belongs to"
          },
          "issued_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the token was issued"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the token will expire"
          },
          "expires_in_seconds": {
            "type": "integer",
            "description": "Seconds remaining until expiration",
            "example": 542
          },
          "should_refresh": {
            "type": "boolean",
            "description": "True if token should be refreshed soon (less than 3 minutes remaining)",
            "example": false
          },
          "refresh_recommended_at": {
            "type": "string",
            "format": "date-time",
            "description": "Recommended time to refresh the token"
          }
        }
      },
      "SendDocumentRequest": {
        "type": "object",
        "required": ["metadata", "payload", "payloadFormat"],
        "description": "SAPI v1.0 document submission payload (per section 7.1).",
        "properties": {
          "metadata": {
            "$ref": "#/components/schemas/DocumentMetadata"
          },
          "payload": {
            "type": "string",
            "description": "Peppol-conformant business document content (UTF-8 encoded UBL XML). Maximum size: 10 MB.",
            "example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\"><cbc:ID>INV-2026-0001</cbc:ID></Invoice>"
          },
              "payloadFormat": {
                "type": "string",
                "enum": ["XML"],
                "description": "Format of the payload. Fixed to XML in SAPI v1.0.",
                "example": "XML"
              },
          "payloadEncoding": {
            "type": "string",
            "description": "Optional character encoding of the payload. Defaults to UTF-8.",
            "example": "UTF-8"
          },
              "checksum": {
                "type": "string",
                "description": "Optional integrity checksum. If provided, MUST be SHA-256 calculated over the exact byte representation of the payload before transmission, encoded as lowercase hexadecimal (64 characters).",
                "pattern": "^[a-f0-9]{64}$",
                "example": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
              }
        }
      },
      "SendDocumentResponse": {
        "type": "object",
        "required": ["providerDocumentId", "status", "timestamp"],
        "description": "Send Document response (per section 7.2). Synchronous acceptance confirms technical receipt only. It does not imply semantic validation, successful Peppol delivery, or legal effect.",
        "properties": {
          "providerDocumentId": {
            "type": "string",
            "description": "Unique document identifier assigned by the Access Point provider",
            "example": "doc_a1b2c3d4e5f6g7h8"
          },
          "status": {
            "type": "string",
            "enum": ["ACCEPTED", "REJECTED"],
            "description": "ACCEPTED = technical receipt confirmed. REJECTED = document was not accepted.",
            "example": "ACCEPTED"
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp when the document was received. Required when status = ACCEPTED."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "Response generation timestamp"
          }
        }
      },
      "ReceivedDocumentListResponse": {
        "type": "object",
        "required": ["documents"],
        "description": "Document listing response (per section 8.1). Listing does not imply retrieval, processing, or business acceptance.",
        "properties": {
          "documents": {
            "type": "array",
            "description": "List of document metadata (sorted oldest first)",
            "items": {
              "$ref": "#/components/schemas/DocumentMetadata"
            }
          },
          "nextPageToken": {
            "type": "string",
            "description": "Opaque cursor token for the next page. Pass as ?pageToken= to fetch the next page. Absent when no more pages.",
            "example": "MjAyNi0wMi0wM1QxMDowMDowMC4wMDBafDEyMw=="
          }
        }
      },
      "DocumentMetadata": {
        "type": "object",
        "required": ["documentId", "documentTypeId", "processId", "senderParticipantId", "receiverParticipantId", "creationDateTime"],
        "properties": {
          "documentId": {
            "type": "string",
            "description": "Client-assigned unique document identifier",
            "example": "INV-2026-0001"
          },
          "documentTypeId": {
            "type": "string",
            "description": "Peppol Document Type Identifier",
            "example": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"
          },
          "processId": {
            "type": "string",
            "description": "Peppol Process Identifier",
            "example": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"
          },
          "senderParticipantId": {
            "type": "string",
            "description": "Sender's Peppol Participant ID",
            "example": "0245:1234567890"
          },
          "receiverParticipantId": {
            "type": "string",
            "description": "Receiver's Peppol Participant ID",
            "example": "0245:9876543210"
          },
          "creationDateTime": {
            "type": "string",
            "format": "date-time",
            "description": "Document creation timestamp in ISO 8601 format",
            "example": "2026-02-12T10:30:00Z"
          }
        }
      },
      "ReceivedDocumentDetailResponse": {
        "type": "object",
        "required": ["metadata", "payload", "payloadFormat"],
        "description": "Document retrieval response (per section 8.2). The payload is delivered as received via the Peppol network and is not altered by the API.",
        "properties": {
          "metadata": {
            "$ref": "#/components/schemas/DocumentMetadata"
          },
          "payload": {
            "type": "string",
            "description": "The document content as received via the Peppol network, delivered unaltered by the API"
          },
          "payloadFormat": {
            "type": "string",
            "description": "Format of the payload content",
            "example": "XML"
          }
        }
      },
      "AcknowledgeResponse": {
        "type": "object",
        "required": ["documentId", "status", "acknowledgedDateTime"],
        "description": "Acknowledge response. Confirms successful handover to the software endpoint. The operation is idempotent.",
        "properties": {
          "documentId": {
            "type": "string",
            "description": "The acknowledged document ID"
          },
          "status": {
            "type": "string",
            "enum": ["ACKNOWLEDGED"],
            "description": "Acknowledgement status",
            "example": "ACKNOWLEDGED"
          },
          "acknowledgedDateTime": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp when the document was acknowledged"
          }
        }
      },
      "ErrorResponseEnvelope": {
        "type": "object",
        "required": ["error"],
        "description": "SAPI v1.0 error response. All error responses across all interactions (Authentication, Send, Receive, Receipt) use this consistent structure per the Error & Status Model specification.",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/SAPIError"
          }
        },
        "example": {
          "error": {
            "category": "VALIDATION",
            "code": "SAPI-VAL-001",
            "message": "Document payload is invalid",
            "details": [
              {
                "field": "document.type",
                "issue": "Unsupported value"
              }
            ],
            "retryable": false,
            "correlation_id": "550e8400-e29b-41d4-a716-446655440000"
          }
        }
      }
    },

  }
}
