Poštár

API Documentation

SAPI-SK: Standardised Access Point Interface

SAPI-SK is a unified interface specification for Peppol Access Point providers and software applications. Solve market fragmentation with a single, standardized API for all Peppol communications.

Official: sapi-sk.sk

What is SAPI-SK?

SAPI-SK is an OpenPeppol Community Initiative that standardizes the interface between Peppol Access Points and business software. Instead of each vendor implementing custom integrations with every Access Point, SAPI-SK defines a common specification that all parties can implement once and use everywhere.

  • For Software Vendors: One API integration works with all SAPI-SK-compliant Access Points
  • For Access Providers: Standardized onboarding and consistent security model
  • Decentralized: Each AP/SP remains responsible for their own implementation and operation
  • Peppol-native: Full support for Peppol Document Type Identifiers and Process Identifiers

Core API Endpoints

POST /sapi/auth/token

Obtain access and refresh tokens using OAuth 2.0 client_credentials grant

POST /sapi/auth/renew

Renew tokens using a refresh token (includes token rotation for security)

GET /sapi/auth/token/status

Check token validity and expiration (proactive refresh recommendation)

POST /sapi/auth/revoke

Revoke a refresh token (logout or security compromised)


POST /sapi/document/send

Submit an electronic business document for Peppol delivery

GET /sapi/document/receive

List received documents (cursor-paginated, oldest first)

GET /sapi/document/receive/{documentId}

Retrieve a specific received document with full content

POST /sapi/document/receive/{documentId}/acknowledge

Acknowledge document receipt (idempotent, marks as CONFIRMED with isRead=true)

Getting Started with SAPI-SK

  1. Register your client application with your SAPI-SK Access Point provider and obtainclient_id andclient_secret.
  2. Authenticate: Exchange credentials for JWT tokens using POST /sapi/auth/token.Access token is valid for 15 minutes; refresh token for 30 days.
  3. Specify organization context: Include X-Peppol-Participant-Id header in each API call (e.g., 0245:1234567890).
  4. Send documents: Use POST /sapi/document/send with Peppol-conformant UBL XML and include an idempotency key for safety.
  5. Receive documents: Poll GET /sapi/document/receive with cursor pagination, fetch full documents, and acknowledge receipt.
  6. Monitor token expiration: Use GET /sapi/auth/token/status to check remaining time and refresh proactively when recommended.

Key Features

OAuth 2.0 Security

JWT tokens with automatic refresh and proactive expiration warnings

Idempotency

Unique Idempotency-Key header prevents duplicate submissions (24h expiry)

Cursor Pagination

Efficient pagination with opaque tokens; documents sorted oldest first

Structured Error Handling

Consistent SAPIError objects with categories, codes, and retryability

Integrity Checking

Optional SHA-256 checksums for payload integrity verification

Multi-Org Context

Single client connection serves multiple organization contexts

TypeScript Integration Example

import { v4 as uuid } from 'uuid';

const SAPI_BASE_URL = 'https://app.peppos.cz';
const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const PARTICIPANT_ID = '0245:1234567890'; // Your Peppol ID

let accessToken: string;
let accessTokenExpiresAt: number;

// Step 1: Authenticate
async function authenticate() {
  const response = await fetch(`${SAPI_BASE_URL}/sapi/auth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      grant_type: 'client_credentials',
    }),
  });

  if (!response.ok) {
    throw new Error(`Auth failed: ${response.statusText}`);
  }

  const data = await response.json();
  accessToken = data.access_token;
  accessTokenExpiresAt = Date.now() + data.expires_in * 1000;
  console.log('✓ Authenticated. Token expires at:', new Date(accessTokenExpiresAt));
}

// Step 2: Check token status
async function checkTokenStatus() {
  const response = await fetch(`${SAPI_BASE_URL}/sapi/auth/token/status`, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
    },
  });

  if (!response.ok) {
    throw new Error('Token status check failed');
  }

  const data = await response.json();
  console.log('Token valid:', data.valid);
  console.log('Expires in:', data.expires_in_seconds, 'seconds');
  if (data.should_refresh) {
    console.log('⚠ Recommend proactive refresh');
  }
}

// Step 3: Send Document
async function sendDocument(invoiceXml: string) {
  const idempotencyKey = uuid();
  const response = await fetch(`${SAPI_BASE_URL}/sapi/document/send`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey,
      'X-Peppol-Participant-Id': PARTICIPANT_ID,
    },
    body: JSON.stringify({
      metadata: {
        documentId: 'INV-2026-0001',
        documentTypeId: '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: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0',
        senderParticipantId: PARTICIPANT_ID,
        receiverParticipantId: '0245:9876543210',
        creationDateTime: new Date().toISOString(),
      },
      payload: invoiceXml,
      payloadFormat: 'XML',
      payloadEncoding: 'UTF-8',
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Send failed: ${error.error.message}`);
  }

  const result = await response.json();
  console.log('✓ Document sent. Provider ID:', result.providerDocumentId);
  console.log('Status:', result.status);
  return result;
}

// Step 4: List Received Documents
async function listReceivedDocuments(pageToken?: string) {
  const queryParams = new URLSearchParams();
  if (pageToken) queryParams.append('pageToken', pageToken);
  queryParams.append('limit', '20');

  const response = await fetch(
    `${SAPI_BASE_URL}/sapi/document/receive?${queryParams}`,
    {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'X-Peppol-Participant-Id': PARTICIPANT_ID,
      },
    }
  );

  if (!response.ok) {
    throw new Error('List failed');
  }

  const result = await response.json();
  console.log('✓ Received', result.documents.length, 'documents');
  return result;
}

// Step 5: Retrieve Document Details
async function getReceivedDocument(documentId: string) {
  const response = await fetch(
    `${SAPI_BASE_URL}/sapi/document/receive/${documentId}`,
    {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'X-Peppol-Participant-Id': PARTICIPANT_ID,
      },
    }
  );

  if (!response.ok) {
    throw new Error(`Get document failed: ${response.statusText}`);
  }

  const result = await response.json();
  console.log('✓ Document retrieved');
  console.log('XML Payload length:', result.payload.length, 'bytes');
  return result;
}

// Step 6: Acknowledge Document
async function acknowledgeDocument(documentId: string) {
  const response = await fetch(
    `${SAPI_BASE_URL}/sapi/document/receive/${documentId}/acknowledge`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'X-Peppol-Participant-Id': PARTICIPANT_ID,
      },
    }
  );

  if (!response.ok) {
    throw new Error('Acknowledge failed');
  }

  const result = await response.json();
  console.log('✓ Document acknowledged at:', result.acknowledgedDateTime);
  return result;
}

// Usage
(async () => {
  try {
    // Authenticate
    await authenticate();
    await checkTokenStatus();

    // Send invoice (example)
    const invoiceXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
  <cbc:ID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">INV-001</cbc:ID>
</Invoice>`;

    const sendResult = await sendDocument(invoiceXml);

    // List received documents
    const listResult = await listReceivedDocuments();
    if (listResult.documents.length > 0) {
      const firstDoc = listResult.documents[0];
      
      // Get full document
      const fullDoc = await getReceivedDocument(firstDoc.documentId);
      console.log('Document content:', fullDoc.payload.substring(0, 200), '...');

      // Acknowledge
      await acknowledgeDocument(firstDoc.documentId);
    }
  } catch (error) {
    console.error('Error:', (error as Error).message);
  }
})();

Error Handling

All SAPI errors use a consistent structure with categories to guide retry logic:

AUTH

Authentication/authorization failures (401, 403, 423). Client must fix credentials or IP whitelist.

VALIDATION

Request validation errors (400, 404, 422). Malformed request; do NOT retry without changes.

TEMPORARY

Transient failures (429, 502, 503, 504). MUST retry with exponential backoff.

PROCESSING

Server-side processing errors (409, 500). May be retryable depending on context.

PERMANENT

Non-retryable failures. Corrective action required; retrying will not help.

Each error includes retryable flag, correlation_id for support, and optional details array for field-level diagnostics.

Important Notes

  • Document Submission: The POST /sapi/document/send endpoint returns 202 Accepted, confirming technical receipt. It does not guarantee Peppol delivery or legal effect.
  • No Automatic Acknowledgement: Document retrieval with GET /sapi/document/receive/{documentId} does not automatically acknowledge. You must explicitly call the acknowledge endpoint.
  • Token Rotation: When renewing tokens, the old refresh token is invalidated. Always store the new refresh token.
  • Multi-Organization: A single authenticated client can serve multiple organizations by changing the X-Peppol-Participant-Id header per request.
  • Idempotency Windows: Idempotency keys are valid for 24 hours. Use unique UUIDs to prevent accidental duplicates.

Note: SAPI-SK is an interface specification (not a platform or hub). Each Access Point provider remains fully responsible for their own implementation, operation, security, and compliance. This guide demonstrates the unified SAPI contract that all compliant providers must support.