KSeF Sessions
Opening KSeF sessions, token/XAdES authentication, session lifecycle, and auto-recovery.
Overview
Before submitting invoices, you need an active KSeF session. The session authenticates your company with the Ministry of Finance and provides encryption keys for invoice submission.
┌───────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────┐
│ Credential│ │ Gateway │ │ KSeF API │ │ Session │
│ (token or │────▶│ 1. Decrypt │────▶│ 2. Challenge│────▶│ active │
│ cert) │ │ 3. Encrypt │ │ 4. Auth │ │ (~1h) │
└───────────┘ │ 5. Store │ └──────────────┘ └─────────┘
└─────────────┘Key facts:
- Sessions last approximately 1 hour (set by KSeF)
- The Gateway automatically reuses active sessions — creating a session when one exists returns the existing one
- When a session expires during invoice submission, the Gateway automatically opens a new one
- Sessions are per company — each company has its own KSeF session
Create Session (Token-Based)
The simplest way. Requires a stored KSeF token credential (Tokens & Certificates).
POST /api/sessions/
Authorization: Bearer {jwt_token}
Content-Type: application/jsonRequest:
{
"company_id": "22919080-f3fe-4aff-8d71-f6493afc956a",
"session_type": "interactive",
"is_test_mode": false
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
company_id | UUID | ✅ | — | Company to authenticate |
session_type | string | ❌ | interactive | interactive (online) or batch |
is_test_mode | boolean | ❌ | false | Use KSeF test environment |
Response (201):
{
"session_id": "faeebfa8-78e0-41af-a1f8-40c76575bcc6",
"company_id": "22919080-f3fe-4aff-8d71-f6493afc956a",
"session_token": "eyJhbGciOiJIUzI1NiIs...(KSeF JWT)",
"session_type": "interactive",
"status": "active",
"ksef_session_reference": "20260314-SO-2AD5D7C000-D5D99D891F-B6",
"context_identifier": null,
"created_at": "2026-03-14T12:28:35.993816Z",
"expires_at": "2026-03-14T13:28:36.152333Z",
"terminated_at": null,
"is_test_mode": false,
"is_active": true
}Session reuse: If a company already has an active session, this endpoint returns the existing session instead of creating a new one. This is by design — KSeF limits concurrent sessions per NIP.
What happens internally:
- Gateway retrieves active KSeF credential from DB (decrypts)
- Calls KSeF
POST /auth/challenge→ gets challenge + timestamp - Fetches MF public key (
KsefTokenEncryption) - Encrypts token with RSA using MF public key
- Calls KSeF
POST /auth/ksef-token→ receives session reference + encryption keys - Stores session in DB and Redis (for fast access)
Authenticate with KSeF (Alternative)
Direct authentication endpoint — opens a session and stores tokens internally.
POST /api/companies/{company_id}/ksef/authenticate
Authorization: Bearer {jwt_token}Response (200):
{
"success": true,
"message": "Successfully authenticated with KSeF",
"access_token_preview": "eyJhbGciOiJIUzI1NiIs...JrIiDEFDjK8O1h0HYdls",
"refresh_token_preview": "eyJhbGciOiJIUzI1NiIs...uCSi-t5cdz5K1rEGuJ98"
}This endpoint is functionally equivalent to
POST /api/sessions/but returns less detail. UsePOST /api/sessions/for full session information.
XAdES Authentication (Certificate-Based)
Two-step flow for organizations using X.509 certificates instead of tokens.
Step 1: Get Challenge
POST /api/companies/{company_id}/ksef/auth/challenge
Authorization: Bearer {jwt_token}
Content-Type: application/jsonRequest (optional):
{
"identifier_type": "nip",
"identifier": "8133793363"
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
identifier_type | string | ❌ | nip | nip or pesel |
identifier | string | ❌ | company NIP | NIP or PESEL value |
Response (200):
{
"challenge": "20260314-CR-2AD6E39000-D05B5F7A...",
"timestamp": "2026-03-14T12:32:13.1483963+00:00",
"expires_at": "2026-03-14T12:42:13.172422+00:00",
"xml_to_sign": "<?xml version='1.0'...AuthTokenRequest...335 chars",
"xml_base64": "PD94bWwgdm..."
}The challenge expires in 10 minutes. You must sign and submit before it expires.
Step 2: Submit Signed XAdES
Sign xml_to_sign with your qualified certificate using XAdES-BES, then submit:
POST /api/companies/{company_id}/ksef/auth/xades
Authorization: Bearer {jwt_token}
Content-Type: application/jsonRequest:
{
"signed_xml": "<?xml version='1.0'...<ds:Signature>...</Signature>...</xml>",
"signed_xml_base64": null
}Provide either signed_xml (string) or signed_xml_base64 (Base64-encoded).
Response (200):
{
"session_id": "abc...",
"ksef_reference_number": "20260314-AU-...",
"expires_at": "2026-03-14T13:32:13Z",
"auth_type": "xades",
"message": "XAdES authentication successful. Use session_id in X-KSeF-Session header."
}Session Management
Get Active Session
GET /api/sessions/company/{company_id}/active
Authorization: Bearer {jwt_token}Response (200): Full SessionResponse of the active session.
Response (404): No active session for this company.
Check Session Status
GET /api/sessions/{session_id}/status
Authorization: Bearer {jwt_token}Response (200):
{
"session_id": "faeebfa8-78e0-41af-a1f8-40c76575bcc6",
"status": "active",
"is_active": true,
"expires_at": "2026-03-14T13:28:36.152333Z",
"time_remaining_seconds": 3398
}
time_remaining_secondstells you exactly how much time is left before automatic expiry. Plan invoice submissions accordingly.
Get Session by ID
GET /api/sessions/{session_id}
Authorization: Bearer {jwt_token}Returns full SessionResponse including session_token, ksef_session_reference, timestamps.
List Sessions
GET /api/sessions/?company_id={id}&status={status}&limit={n}&offset={n}
Authorization: Bearer {jwt_token}| Parameter | Type | Required | Description |
|---|---|---|---|
company_id | UUID | ❌ | Filter by company |
status | string | ❌ | active, expired, terminated |
limit | integer | ❌ | Results per page |
offset | integer | ❌ | Pagination offset |
Response (200):
{
"sessions": [
{
"session_id": "faeebfa8-...",
"status": "active",
"ksef_session_reference": "20260314-SO-2AD5D7C000-...",
"is_active": true,
"created_at": "2026-03-14T12:28:35Z",
"expires_at": "2026-03-14T13:28:36Z"
},
{
"session_id": "0d861405-...",
"status": "expired",
"ksef_session_reference": "20260314-SO-26E90F4000-...",
"is_active": false,
"created_at": "2026-03-14T11:19:59Z",
"expires_at": "2026-03-14T12:20:00Z"
}
],
"total": 5
}KSeF Token Management (Within Session)
Get Token Info
GET /api/companies/{company_id}/ksef/token-info
Authorization: Bearer {jwt_token}Response (200):
{
"has_access_token": true,
"has_refresh_token": true,
"access_token_expires_at": "2026-03-14T12:39:56.358356+00:00",
"access_token_expires_soon": false
}Get KSeF Access Token
GET /api/companies/{company_id}/ksef/access-token
Authorization: Bearer {jwt_token}Returns the raw KSeF access token (for direct KSeF API calls, advanced use).
Refresh KSeF Access Token
POST /api/companies/{company_id}/ksef/refresh-token
Authorization: Bearer {jwt_token}Response (200):
{
"accessToken": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"validUntil": "2026-03-14T12:47:13.541304Z"
}
}The Gateway handles token refresh automatically during invoice submission. Use this only if you need to extend a session manually.
Invalidate All Tokens
DELETE /api/companies/{company_id}/ksef/invalidate-tokens
Authorization: Bearer {jwt_token}Revokes all cached KSeF tokens for the company (access + refresh). The next invoice submission will trigger a fresh authentication.
Closing Sessions
Close Current Session
POST /api/companies/{company_id}/ksef/terminate-session
Authorization: Bearer {jwt_token}Response (200):
{
"success": true,
"message": "Session terminated successfully",
"referenceNumber": "session-terminated"
}Close Online (Interactive) Session by Reference
POST /api/sessions/online/{reference_number}/close?company_id={company_id}
Authorization: Bearer {jwt_token}Close Batch Session by Reference
POST /api/sessions/batch/{reference_number}/close?company_id={company_id}
Authorization: Bearer {jwt_token}Terminate All Sessions
POST /api/companies/{company_id}/ksef/terminate-all-sessions
Authorization: Bearer {jwt_token}Response (200):
{
"success": true,
"message": "All sessions terminated successfully",
"referenceNumber": "batch-terminate",
"terminatedSessionsCount": 3
}Terminate by Session ID
POST /api/sessions/terminate
Authorization: Bearer {jwt_token}
Content-Type: application/json{
"session_id": "faeebfa8-78e0-41af-a1f8-40c76575bcc6"
}Invoices Within Session
List Invoices in Session
GET /api/sessions/{reference_number}/invoices?company_id={id}&page_size=50&page_offset=0
Authorization: Bearer {jwt_token}Response (200):
{
"continuationToken": null,
"invoices": [
{
"referenceNumber": "20260110-SE-1234567890-ABCDEF1234-12",
"ordinalNumber": 1,
"invoicingDate": "2026-01-10T12:00:00Z",
"invoiceHash": "abc123...",
"status": { "code": 200, "description": "Sukces" }
}
]
}List Failed Invoices in Session
GET /api/sessions/{reference_number}/invoices/failed?company_id={id}
Authorization: Bearer {jwt_token}Response (200):
{
"continuationToken": null,
"invoices": [
{
"referenceNumber": "20260110-SE-...",
"ordinalNumber": 1,
"invoiceHash": "abc123...",
"status": { "code": 430, "description": "Invoice file verification error" }
}
]
}Get Invoice UPO in Session
GET /api/sessions/{session_reference}/invoices/ksef/{ksef_reference_number}/upo?company_id={id}
Authorization: Bearer {jwt_token}Get Session Collective UPO
GET /api/sessions/{session_reference}/upo/{upo_reference_number}?company_id={id}
Authorization: Bearer {jwt_token}Auth Sessions (Keycloak/KSeF)
Manage authentication sessions at the Keycloak/KSeF level.
GET /api/auth/sessions?company_id={id} List active auth sessions
DELETE /api/auth/sessions/current?company_id={id} Revoke current auth session
DELETE /api/auth/sessions/{ref}?company_id={id} Revoke by reference number
GET /api/auth/{ref}?auth_token={token} Check authentication statusSession Lifecycle
┌─────────┐
POST /sessions/ ───▶│ ACTIVE │──── ~1 hour TTL
└────┬────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ EXPIRED │ │TERMINATED│ │ (auto) │
│ (timeout)│ │ (manual) │ │ new auth │
└──────────┘ └──────────┘ └──────────┘| Status | Meaning | Can submit invoices? |
|---|---|---|
active | Session is open and valid | ✅ Yes |
expired | TTL exceeded (~1 hour) | ❌ No — Gateway auto-creates new session |
terminated | Manually closed | ❌ No — create new session |
Auto-recovery: When invoice submission encounters an expired session, the Gateway automatically:
- Detects 401 from KSeF
- Opens a new session
- Retries the invoice submission
- This is transparent to the API client
Error Responses
| HTTP | Error | Cause |
|---|---|---|
| 200/201 | — | Success |
| 400 | No active credential found | Company has no KSeF token/certificate |
| 401 | Not authenticated | Missing JWT |
| 403 | Access denied to this company | Wrong tenant |
| 404 | Company not found | Invalid company_id |
| 404 | No active session | No active session for company |
| 409 | Session already exists | Rarely — concurrent session creation |
| 500 | KSeF authentication failed | KSeF API error (check KSeF status) |
Endpoint Reference
Session Management
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/sessions/ | Create new session (or return existing) |
| GET | /api/sessions/ | List sessions (filter: company_id, status) |
| GET | /api/sessions/{session_id} | Get session by ID |
| GET | /api/sessions/{session_id}/status | Check status + time_remaining |
| GET | /api/sessions/company/{company_id}/active | Get active session |
| POST | /api/sessions/online/{ref}/close | Close interactive session |
| POST | /api/sessions/batch/{ref}/close | Close batch session |
| POST | /api/sessions/terminate | Terminate by session_id |
KSeF Authentication
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/companies/{id}/ksef/authenticate | Authenticate (token-based) |
| POST | /api/companies/{id}/ksef/auth/challenge | Get XAdES challenge |
| POST | /api/companies/{id}/ksef/auth/xades | Submit signed XAdES |
| GET | /api/companies/{id}/ksef/access-token | Get raw KSeF access token |
| GET | /api/companies/{id}/ksef/token-info | Token status info |
| POST | /api/companies/{id}/ksef/refresh-token | Refresh access token |
| DELETE | /api/companies/{id}/ksef/invalidate-tokens | Revoke all cached tokens |
| POST | /api/companies/{id}/ksef/terminate-session | Close current session |
| POST | /api/companies/{id}/ksef/terminate-all-sessions | Close all sessions |
Invoices in Session
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/sessions/{ref}/invoices | List invoices in session |
| GET | /api/sessions/{ref}/invoices/failed | List failed invoices |
| GET | /api/sessions/{ref}/invoices/ksef/{ksef_ref}/upo | Invoice UPO in session |
| GET | /api/sessions/{ref}/upo/{upo_ref} | Session collective UPO |
Auth Sessions
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/auth/sessions | List active auth sessions |
| DELETE | /api/auth/sessions/current | Revoke current |
| DELETE | /api/auth/sessions/{ref} | Revoke by reference |
| GET | /api/auth/{ref} | Check auth status |
Complete Example
#!/bin/bash
TOKEN="your_jwt_token"
BASE="https://demo.ksefapi.com"
COMPANY_ID="22919080-f3fe-4aff-8d71-f6493afc956a"
# 1. Create session (or reuse active)
SESSION=$(curl -s -X POST "$BASE/api/sessions/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"company_id\": \"$COMPANY_ID\"}")
echo "$SESSION" | jq '{session_id, status, ksef_session_reference, expires_at}'
# 2. Check status
SESSION_ID=$(echo "$SESSION" | jq -r '.session_id')
curl -s "$BASE/api/sessions/$SESSION_ID/status" \
-H "Authorization: Bearer $TOKEN" | jq '{status, time_remaining_seconds}'
# 3. Token info
curl -s "$BASE/api/companies/$COMPANY_ID/ksef/token-info" \
-H "Authorization: Bearer $TOKEN" | jq .
# 4. Submit invoices → see Invoice Submission docs
# (Session is now active, invoice submission will use it automatically)
# 5. When done, close session (optional — expires automatically)
curl -s -X POST "$BASE/api/companies/$COMPANY_ID/ksef/terminate-session" \
-H "Authorization: Bearer $TOKEN" | jq .Important Notes
Session reuse: POST /api/sessions/ is idempotent for active sessions. Calling it multiple times returns the same session. No need to check for existing sessions before creating.
Auto-session management: For most use cases, you don't need to manage sessions manually. The Gateway creates sessions automatically when you submit an invoice. Sessions are needed explicitly only if you want to pre-authenticate or inspect session state.
Session TTL: KSeF sessions expire after approximately 1 hour. The exact time is in expires_at. After expiry, the Gateway auto-creates a new session on the next invoice submission.
Interactive vs batch sessions: Interactive sessions are for real-time invoice submission (online). Batch sessions are for large file uploads (offline). Most API integrations use interactive.
KSeF session token: The session_token in the response is a KSeF-issued JWT (not your API JWT). It's used internally by the Gateway for KSeF API calls. You don't need to use it directly unless making raw KSeF API calls.
Next steps:
- Submit invoices → Invoice Submission
- Batch submission → Batch Submission