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
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.
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)
client_id andclient_secret.POST /sapi/auth/token.Access token is valid for 15 minutes; refresh token for 30 days.X-Peppol-Participant-Id header in each API call (e.g., 0245:1234567890).POST /sapi/document/send with Peppol-conformant UBL XML and include an idempotency key for safety.GET /sapi/document/receive with cursor pagination, fetch full documents, and acknowledge receipt.GET /sapi/auth/token/status to check remaining time and refresh proactively when recommended.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
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);
}
})();
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.
POST /sapi/document/send endpoint returns 202 Accepted, confirming technical receipt. It does not guarantee Peppol delivery or legal effect.GET /sapi/document/receive/{documentId} does not automatically acknowledge. You must explicitly call the acknowledge endpoint.X-Peppol-Participant-Id header per request.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.