Nieprawidłowe żądanie w KSeF

0

Skrypt w PHP:

<?php

if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES['invoice']) && $_FILES['invoice']['error'] == UPLOAD_ERR_OK) {
    // Adres API KSeF (testowy)
    $ksefApiUrl = 'https://ksef-test.mf.gov.pl/api/v1/invoices';

    // Token KSeF (przykładowy, w rzeczywistości użyj swojego tokenu)
    $token = 'xxx';

    // Ścieżka do pliku tymczasowego
    $invoiceXmlPath = $_FILES['invoice']['tmp_name'];

    // Wczytanie zawartości pliku XML
    $invoiceData = file_get_contents($invoiceXmlPath);

    // Sprawdzenie, czy XML jest poprawny według schematu XSD
    libxml_use_internal_errors(true);
    $xml = new DOMDocument();
    $xml->loadXML($invoiceData);
    $xsdPath = 'https://magazyn.xxx.pl/schemas/Faktura_v2-0.xsd';
    if (!$xml->schemaValidate($xsdPath)) {
        echo "Plik XML jest niepoprawny według schematu XSD.<br>";
        foreach (libxml_get_errors() as $error) {
            echo htmlspecialchars($error->message) . "<br>";
        }
        exit;
    } else {
        echo "Plik XML jest poprawny według schematu XSD.<br>";
    }

    // Logowanie zawartości XML
    file_put_contents('invoice_log.xml', $invoiceData);

    // Inicjalizacja cURL
    $ch = curl_init();

    // Ustawienie opcji cURL
    curl_setopt($ch, CURLOPT_URL, $ksefApiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $invoiceData);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/xml',
        'Accept: application/json',
        'Authorization: Bearer ' . $token,
        'Cache-Control: no-cache',
        'Pragma: no-cache'
    ]);

    // Wykonanie żądania
    $response = curl_exec($ch);

    // Sprawdzenie błędów cURL
    if (curl_errno($ch)) {
        echo 'cURL error: ' . curl_error($ch);
    } else {
        // Odczytanie kodu odpowiedzi HTTP
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($httpCode == 200) {
            echo "Faktura została pomyślnie wysłana do KSeF.<br>";
            echo "Odpowiedź z serwera: " . htmlspecialchars($response) . "<br>";
        } else {
            echo "Wystąpił błąd podczas wysyłania faktury do KSeF.<br>";
            echo "Status code: " . $httpCode . "<br>";
            echo "Odpowiedź z serwera: " . htmlspecialchars($response) . "<br>";

            // Logowanie odpowiedzi serwera dla celów diagnostycznych
            file_put_contents('error_log.txt', "Status code: $httpCode\nResponse: $response\n", FILE_APPEND);
        }
    }

    // Zamknięcie cURL
    curl_close($ch);
}
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Upload Invoice for Validation</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
    <div class="container">
        <h2 class="mt-4">Upload Invoice for Validation</h2>
        <form action="" method="post" enctype="multipart/form-data">
            <div class="form-group">
                <label for="invoice">Choose Invoice XML file:</label>
                <input type="file" class="form-control-file" id="invoice" name="invoice" required>
            </div>
            <button type="submit" class="btn btn-primary">Upload</button>
        </form>
    </div>
</body>
</html>

Dostaje:

Plik XML jest poprawny według schematu XSD.
Wystąpił błąd podczas wysyłania faktury do KSeF.
Status code: 400
Odpowiedź z serwera: {"exception":{"serviceCtx":"srvTEMFF","serviceCode":"0711A016-9DEB-41B0-9538-FB4A7E5DC52B","serviceName":"exception.handler","timestamp":"2024-06-07T12:23:10.759Z","exceptionDetailList":[{"exceptionCode":21180,"exceptionDescription":"Nieprawidłowe żądanie."}]}}
1

Jak robisz $invoiceData = file_get_contents($invoiceXmlPath);, to zakładam że tam jest XML?

A potem go wkładasz tutaj: curl_setopt($ch, CURLOPT_POSTFIELDS, $invoiceData);.

Opcja CURLOPT_POSTFIELDS przyjmuje słownik parametrów key-value:

  • albo jako tablica asocjacyjna, której klucze i wartości to pola POST
  • albo jako string (który chyba przekazujesz), ale ten string to musi być url-encoded string wartości.

Więc ja bym Ci proponował żebyś przekazał array, a nie string.

Pokaż najlepiej ten plik który próbujesz wysłać. Nie znam dokładnie API ksef, więc nie wiem dokładnie jakie pola trzeba dodać akurat pod to żądanie.

0
Riddle napisał(a):

Pokaż najlepiej ten plik który próbujesz wysłać. Nie znam dokładnie API ksef, więc nie wiem dokładnie jakie pola trzeba dodać akurat pod to żądanie.

<?xml version="1.0" encoding="UTF-8"?>
<Faktura xmlns="http://crd.gov.pl/wzor/2023/06/29/12648/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://crd.gov.pl/wzor/2023/06/29/12648/ Faktura_v2-0.xsd">
  <Naglowek>
    <KodFormularza kodSystemowy="FA (2)" wersjaSchemy="1-0E">FA</KodFormularza>
    <WariantFormularza>2</WariantFormularza>
    <DataWytworzeniaFa>2024-06-07T13:04:37</DataWytworzeniaFa>
    <SystemInfo>Magnet</SystemInfo>
  </Naglowek>
  <Podmiot1>
    <DaneIdentyfikacyjne>
      <NIP>5771876945</NIP>
      <Nazwa>xxx</Nazwa>
    </DaneIdentyfikacyjne>
    <Adres>
      <KodKraju>PL</KodKraju>
      <AdresL1>ul. xxx 17</AdresL1>
      <AdresL2>42-000 xxxx<AdresL2>
    </Adres>
  </Podmiot1>
  <Podmiot2>
    <DaneIdentyfikacyjne>
      <NIP>7010796111</NIP>
      <Nazwa>xxxx</Nazwa>
    </DaneIdentyfikacyjne>
    <Adres>
      <KodKraju>PL</KodKraju>
      <AdresL1>Ul. ALEJE JEROZOLIMSKIE 43</AdresL1>
      <AdresL2>02-001 WARSZAWA</AdresL2>
    </Adres>
  </Podmiot2>
  <Fa>
    <KodWaluty>PLN</KodWaluty>
    <P_1>2024-06-06</P_1>
    <P_2>119/06/2024</P_2>
    <P_13_1>14.5</P_13_1>
    <P_14_1>2.71</P_14_1>
    <P_15>17.21</P_15> 
    <WierszeFaktur>
      <WierszFaktury>
        <NrWierszaFa>1</NrWierszaFa>
        <P_7>CD-R HP 700 MB x52</P_7>
        <P_8A>10</P_8A>
        <P_8B>szt.</P_8B>
        <P_9A>11.79</P_9A>
        <P_11>11.79</P_11>
        <P_12>23</P_12>
        <P_14A>14.5</P_14A>
      </WierszFaktury>
    </WierszeFaktur>
  </Fa>
</Faktura>
0

Znajdź dokumentację tego endpointa pod który strzelasz, i wrzuć tu link.

0
Riddle napisał(a):

Znajdź dokumentację tego endpointa pod który strzelasz, i wrzuć tu link.

No właśnie nic nie ma poza :https://ksef-test.mf.gov.pl/

0
xoree napisał(a):
Riddle napisał(a):

Znajdź dokumentację tego endpointa pod który strzelasz, i wrzuć tu link.

No właśnie nic nie ma po za :https://ksef-test.mf.gov.pl/

To skąd wziąłeś to?

$ksefApiUrl = 'https://ksef-test.mf.gov.pl/api/v1/invoices';
0
Riddle napisał(a):
xoree napisał(a):
Riddle napisał(a):

Znajdź dokumentację tego endpointa pod który strzelasz, i wrzuć tu link.

No właśnie nic nie ma po za :https://ksef-test.mf.gov.pl/

To skąd wziąłeś to?

$ksefApiUrl = 'https://ksef-test.mf.gov.pl/api/v1/invoices';

Podany był przykładowy adres API dla środowiska testowego (gdzieś w necie) ogólnie błędy zwraca poprawnie np. exceptionCode 21180 bo były też inne ale rozwiązałem z nimi problem.
Pytanie jest jeszcze inne czy token jest jedna dla wszystkich api czy do testów trzeba mieć inne.

0

A czy w tym przykładzie był przykład body zadania post? Jeśli tak, to wklej tutaj.

0
Riddle napisał(a):

A czy w tym przykładzie był przykład body zadania post? Jeśli tak, to wklej tutaj.

Odpowiedź z serwera: [Odpowiedź serwera w formacie JSON]

A może ktoś zna inny testowy Endpoint ?

1

Jedna moja uwaga. Skoro mogą zaorać tą wersję KSEF lub w ogóle z tego systemu zrezygnować, czemu ma służyć integracja? Bo z tego co mówią media ta wersja była na tyle wadliwa, że na pewno nie będą jej wprowadzać. Tym samym nie ma jasnej dokumentacji integracji. Jak masz zgadywać endpointy to cała ta zabawa nie ma sensu.
Podstawowa dokumentacja musi być.
Może to średnio pomocny technicznie post, ale moim zdaniem musisz to brać pod uwagę.

0
jurek1980 napisał(a):

Jedna moja uwaga. Skoro mogą zaorać tą wersję KSEF lub w ogóle z tego systemu zrezygnować, czemu ma służyć integracja? Bo z tego co mówią media ta wersja była na tyle wadliwa, że na pewno nie będą jej wprowadzać. Tym samym nie ma jasnej dokumentacji integracji. Jak masz zgadywać endpointy to cała ta zabawa nie ma sensu.
Podstawowa dokumentacja musi być.
Może to średnio pomocny technicznie post, ale moim zdaniem musisz to brać pod uwagę.

To informacja ważna, bo jeśli będzie nowa wersja to prace wstrzymuje.

1

Pierwszy artykuł z brzegu https://grantthornton.pl/publikacja/mf-ksef-niewejdzie-w-zycie-1-lipca-2024-r/
W innych jest większa krytyka i jeszcze późniejsze daty lub w ogóle info o rezygnacji z tego systemu. Poczytaj.

0
jurek1980 napisał(a):

Pierwszy artykuł z brzegu https://grantthornton.pl/publikacja/mf-ksef-niewejdzie-w-zycie-1-lipca-2024-r/
W innych jest większa krytyka i jeszcze późniejsze daty lub w ogóle info o rezygnacji z tego systemu. Poczytaj.

Wiesz to ze nie wejdzie w lipcu to ja wiem, bo ma wejść (duże firmy) 01.02.2026 (a male firmy) kwiecień 2026 ale nie wiedziałem ze API będzie nowe ....

0

niech ktoś podpowie co robię źle ....

<?php

// Funkcja do uzyskania tokenu sesji
function getKSeFSessionToken($apiUrl, $apiKey, $internalIdentifier, $challengeTime) {
    $curl = curl_init();

    // Przygotowanie zakodowanego Base64 tokenu
    $tokenData = array(
        'token' => base64_encode($apiKey . '|' . $challengeTime),  // Zakodowanie tokenu w Base64
        'contextIdentifier' => array(
            'type' => 'onip',  // Typ identyfikatora
            'identifier' => $internalIdentifier  // Identyfikator wewnętrzny
        )
    );

    // Konwersja na JSON, aby był zgodny z `Content-Type: application/json`
    $postData = json_encode($tokenData);

    curl_setopt_array($curl, [
        CURLOPT_URL => $apiUrl . "/online/Session/InitToken",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/json",  // Ustawione na JSON
            "Accept: application/json",  // Oczekiwanie odpowiedzi w formacie JSON
            "Authorization: Bearer " . $apiKey  // Klucz API w nagłówku
        ],
        CURLOPT_POSTFIELDS => $postData  // Dane do przesłania w formacie JSON
    ]);

    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    $curlError = curl_error($curl);

    curl_close($curl);

    // Wyświetlanie odpowiedzi w celach debugowania
    if ($curlError) {
        echo "Błąd cURL: " . $curlError . "\n";
    }

    echo "Kod odpowiedzi HTTP: " . $httpCode . "\n";
    echo "Odpowiedź serwera: " . $response . "\n";

    $data = json_decode($response, true);

    // Sprawdzenie, czy token sesji został zwrócony
    if (isset($data['token'])) {
        return $data['token'];
    }

    return false;
}

// Użycie funkcji

$apiUrl = "https://ksef-demo.mf.gov.pl/api";  // Zmieniony adres na demo
$apiKey = "xxx";  // Klucz API
$internalIdentifier = "xx";  // Identyfikator wewnętrzny
$challengeTime = "2024-10-24T04:33:01.837Z";  // Wartość challengeTime (przykładowa)

$sessionToken = getKSeFSessionToken($apiUrl, $apiKey, $internalIdentifier, $challengeTime);

if ($sessionToken) {
    echo "Uzyskano token sesji: " . $sessionToken . "\n";
} else {
    echo "Nie udało się uzyskać tokenu sesji.";
}

?>

Mam :Kod odpowiedzi HTTP: 415 Odpowiedź serwera: Nie udało się uzyskać tokenu sesji.

0

Ten $challengeTime nie możesz brać żywcem z odpowiedzi na wyzwanie autoryzacyjne, musisz zamienić na liczbę milisekund licząc od 1 stycznia 1970. Dodatkowo, zanim token zakodujesz w Base64, musisz go najpierw zaszyfrować kluczem publicznym.

0

Takie coś :

<?php

date_default_timezone_set("UTC");
// Funkcja do uzyskania tokenu sesji
function getKSeFSessionToken($apiUrl, $apiKey, $challengeTime) {
    $curl = curl_init();

    // Obliczanie milisekund od 1 stycznia 1970
    $timestamp = strtotime($challengeTime) * 1000;

    // Przygotowanie danych tokenu
    $tokenData = $apiKey . '|' . $timestamp;

    // Załaduj klucz publiczny z pliku PEM
    $publicKey = file_get_contents('publicKey.pem');
    if (!$publicKey) {
        die("Nie można załadować pliku publicKey.pem.\n");
    }

    // Zaszyfruj token przy użyciu klucza publicznego z paddingiem PKCS1
    $encryptedToken = null;
    if (!openssl_public_encrypt($tokenData, $encryptedToken, $publicKey, OPENSSL_PKCS1_PADDING)) {
        die("Błąd szyfrowania przy użyciu klucza publicznego.\n");
    }

    // Debugowanie: wyświetlenie zaszyfrowanych danych (binarnych)
    echo "Zaszyfrowane dane binarne (token): " . bin2hex($encryptedToken) . "\n";

    curl_setopt_array($curl, [
        CURLOPT_URL => $apiUrl . "/online/Session/InitToken",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/octet-stream",  // Wysłanie jako dane binarne
            "Accept: application/json",  // Oczekiwanie odpowiedzi w formacie JSON
            "Authorization: Bearer " . $apiKey,  // Klucz API w nagłówku
        ],
        CURLOPT_POSTFIELDS => $encryptedToken  // Wysłanie zaszyfrowanego tokenu jako dane binarne
    ]);

    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    $curlError = curl_error($curl);

    curl_close($curl);

    // Wyświetlanie odpowiedzi w celach debugowania
    if ($curlError) {
        echo "Błąd cURL: " . $curlError . "\n";
    }

    echo "Kod odpowiedzi HTTP: " . $httpCode . "\n";
    echo "Odpowiedź serwera: " . $response . "\n";

    $data = json_decode($response, true);

    // Sprawdzenie, czy token sesji został zwrócony
    if (isset($data['token'])) {
        return $data['token'];
    }

    return false;
}

// Użycie funkcji

$apiUrl = "https://ksef-demo.mf.gov.pl/api";  // Adres demo
$apiKey = "xxx";  // Klucz API

// Ustawienie strefy czasowej na UTC dla lepszej synchronizacji z serwerem
date_default_timezone_set("UTC");

// Aktualny czas UTC w formacie ISO 8601 z milisekundami
$challengeTime = gmdate("Y-m-d\TH:i:s.v\Z", strtotime('+2 hours'));

echo "Aktualny challengeTime: " . $challengeTime . "\n";

$sessionToken = getKSeFSessionToken($apiUrl, $apiKey, $challengeTime);

if ($sessionToken) {
    echo "Uzyskano token sesji: " . $sessionToken . "\n";
} else {
    echo "Nie udało się uzyskać tokenu sesji.";
}

?>

(Dziękuję za odpowiedz) Dostaje błąd :

Aktualny challengeTime: 2024-10-25T06:58:10.000Z Zaszyfrowane dane binarne (token): 89a3c9192b8889dccb45484e5322cd66709650b36986ef06e2a431adbe47a3c74fa5b4ddad72ddf4a56e1cd136f8f12911e91296604435dee0702dae7a441c53ecdaf428af226d14da9c2b4129d315d19eaa886ea9d6e0f22e8365d3a1d884d796032fecf2f9cbfed2e99d0fb99e612024d6a81ae33e6181486c67766e0dffdf56f26ef4c58a7a723edfa1b398049488f27d9367f0f00b934fbe0817e249451baf393c98bbd33cfa59f41c231519f413ad82fafc78dc2b298b64542995e3824a34791d7cbe042669a1b5a7ce351b6d4602bc4a46e1774bed4712875a845380e7c5cf408e349b6ecd37473d51edace70df4d247558e64fc5a73f0b0f26abeca73 Kod odpowiedzi HTTP: 400 Odpowiedź serwera: {"exception":{"serviceCtx":"srvTRMFB","serviceCode":"20241025-EX-78131B06E2-065F9A0871-14","serviceName":"online.session.session.token.init","timestamp":"2024-10-25T04:58:10.680Z","referenceNumber":"20241025-SE-C23E89EC47-9822C28F8E-58","exceptionDetailList":[{"exceptionCode":21401,"exceptionDescription":"Dokument nie jest zgodny ze schemą (xsd)."}]}} Nie udało się uzyskać tokenu sesji.

Robiłem binarnie i jsonem - to samo

0

Powinno być: base64_encode($encryptedToken). Natomiast challengeTime to nie ma być aktualny czas, tylko znacznik czasu (timestamp), który otrzymujesz w zwrotce po wysłaniu żądania na "/online/Session/AuthorisationChallenge". Poza tym w żądaniu na "/online/Session/InitToken" w CURLOPT_POSTFIELDS powinien być nie sam token, tylko specjalny xml InitSessionTokenRequest. Tu jest przykład: AuthorisationChallenge zwraca kod 200 przy błędnym SessionToken

0

Dziękuje za informacje. Poprawki:

<?php

// Funkcja do uzyskania challenge i timestamp z endpointu /online/Session/AuthorisationChallenge
function getChallengeAndTimestamp($apiUrl, $nip) {
    $curl = curl_init();

    $data = array(
        'contextIdentifier' => array(
            'type' => 'onip',
            'identifier' => $nip
        )
    );
    $postData = json_encode($data);

    curl_setopt_array($curl, [
        CURLOPT_URL => $apiUrl . "/online/Session/AuthorisationChallenge",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS => $postData
    ]);

    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    $curlError = curl_error($curl);
    curl_close($curl);

    if ($curlError) {
        echo "Błąd cURL: " . $curlError . "\n";
        return false;
    }

    if ($httpCode === 201) {
        return json_decode($response, true);
    } else {
        echo "Kod odpowiedzi HTTP: " . $httpCode . "\n";
        echo "Odpowiedź serwera: " . $response . "\n";
        return false;
    }
}

// Funkcja do szyfrowania tokenu kluczem publicznym
function encryptToken($token, $publicKey) {
    $encryptedToken = '';
    if (openssl_public_encrypt($token, $encryptedToken, $publicKey)) {
        return base64_encode($encryptedToken);  // Zwróć zaszyfrowany token zakodowany w Base64
    } else {
        echo "Nie udało się zaszyfrować tokenu.\n";
        return false;
    }
}

// Funkcja do uzyskania tokenu sesji KSeF
function getKSeFSessionToken($apiUrl, $encryptedToken, $challenge, $nip) {
    $xmlRequest = <<<XML
<?xml version="1.0" encoding="utf-8"?>
<ns3:InitSessionTokenRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:ns2="http://ksef.mf.gov.pl/schema/gtw/svc/types/2021/10/01/0001" 
    xmlns:ns3="http://ksef.mf.gov.pl/schema/gtw/svc/online/auth/request/2021/10/01/0001">
  <ns3:Context>
    <Challenge xmlns="http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001">{$challenge}</Challenge>
    <Identifier xsi:type="ns2:SubjectIdentifierByCompanyType" xmlns="http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001">
      <ns2:Identifier>{$nip}</ns2:Identifier>
    </Identifier>
    <DocumentType xmlns="http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001">
      <ns2:Service>KSeF</ns2:Service>
      <ns2:FormCode>
        <ns2:SystemCode>FA (2)</ns2:SystemCode>
        <ns2:SchemaVersion>1-0E</ns2:SchemaVersion>
        <ns2:TargetNamespace>http://crd.gov.pl/wzor/2023/06/29/12648/</ns2:TargetNamespace>
        <ns2:Value>FA</ns2:Value>
      </ns2:FormCode>
    </DocumentType>
    <Token xmlns="http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001">{$encryptedToken}</Token>
  </ns3:Context>
</ns3:InitSessionTokenRequest>
XML;

    echo "Wygenerowany XML przed wysyłką:\n" . $xmlRequest . "\n";

    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_URL => $apiUrl . "/online/Session/InitToken",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/xml; charset=utf-8",  // Nagłówek dla XML
            "Accept: application/xml",  // Oczekujemy odpowiedzi w formacie XML
        ],
        CURLOPT_POSTFIELDS => $xmlRequest
    ]);

    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    $curlError = curl_error($curl);
    curl_close($curl);

    if ($curlError) {
        echo "Błąd cURL: " . $curlError . "\n";
    }

    echo "Kod odpowiedzi HTTP: " . $httpCode . "\n";
    echo "Odpowiedź serwera (jako tekst):\n" . $response . "\n";  // Wyświetlenie pełnej odpowiedzi serwera

    // Jeśli odpowiedź jest w XML, spróbuj ją sparsować jako XML
    if ($httpCode === 200) {
        $data = simplexml_load_string($response);

        if ($data === false) {
            echo "Błąd podczas parsowania odpowiedzi XML:\n";
            foreach (libxml_get_errors() as $error) {
                echo $error->message . "\n";
            }
            return false;
        }

        return $data;
    }

    return false;
}

// Funkcja do wysyłania faktury z użyciem SessionToken
function sendInvoice($apiUrl, $sessionToken, $invoiceFile) {
    $curl = curl_init();

    $headers = [
        'accept: application/json',
        'SessionToken: ' . $sessionToken,
        'Content-Type: application/xml'
    ];

    // Wczytaj zawartość pliku XML
    $invoiceData = file_get_contents($invoiceFile);

    curl_setopt_array($curl, [
        CURLOPT_URL => $apiUrl . "/online/Invoice/Send",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => $invoiceData  // Wysyłamy zawartość pliku XML
    ]);

    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    $curlError = curl_error($curl);
    curl_close($curl);

    if ($curlError) {
        echo "Błąd cURL: " . $curlError . "\n";
    }

    echo "Kod odpowiedzi HTTP: " . $httpCode . "\n";
    echo "Odpowiedź serwera:\n" . $response . "\n";

    return $httpCode === 200 ? json_decode($response, true) : false;
}

// Cały proces

$apiUrl = "https://ksef-demo.mf.gov.pl/api";
$nip = "xxx";  // Twój NIP
$apiKey = "xxx";  // Twój klucz API
$invoiceFile = __DIR__ . '/faktura_213-10-2024_2024-10-24.xml';  // Ścieżka do pliku XML z fakturą

// Krok 1: Pobierz challenge i timestamp
$challengeData = getChallengeAndTimestamp($apiUrl, $nip);
if ($challengeData) {
    $challenge = $challengeData['challenge'];
    $timestamp = $challengeData['timestamp'];

    // Krok 2: Przygotowanie tokenu do zaszyfrowania
    $tokenData = $timestamp . '|' . $apiKey;  // Łączenie timestamp i tokenu

    // Krok 3: Szyfrowanie tokenu kluczem publicznym
    $publicKeyPath = __DIR__ . '/publicKey.pem';  // Ścieżka do pliku klucza publicznego

    if (file_exists($publicKeyPath) && is_readable($publicKeyPath)) {
        $publicKey = file_get_contents($publicKeyPath);
        $encryptedToken = encryptToken($tokenData, $publicKey);

        if ($encryptedToken) {
            // Krok 4: Uzyskaj token sesji
            $sessionToken = getKSeFSessionToken($apiUrl, $encryptedToken, $challenge, $nip);

            if ($sessionToken) {
                echo "Uzyskano token sesji.\n";

                // Krok 5: Wczytaj i wyślij fakturę z pliku XML
                $sendResult = sendInvoice($apiUrl, $sessionToken, $invoiceFile);

                if ($sendResult) {
                    echo "Faktura została wysłana.\n";
                } else {
                    echo "Nie udało się wysłać faktury.\n";
                }
            } else {
                echo "Nie udało się uzyskać tokenu sesji.\n";
            }
        }
    } else {
        echo "Nie udało się odczytać pliku z kluczem publicznym.\n";
    }
}
?>

Dostaje :Kod odpowiedzi HTTP: 201 Odpowiedź serwera: {"timestamp":"2024-10-26T04:36:46.490Z","challenge":"20241026-CR-ABE36025B7-CA3D584718-8F"} (i to jest ok)
A następnie : Wygenerowany XML przed wysyłką: 20241026-CR-1C86ECE11B-790E65E4AE-DD 5771876968 KSeF FA (2) 1-0E http://crd.gov.pl/wzor/2023/06/29/12648/ FA mqYthJM2GqsNhGvXWcTxuEkZr0fz8BNo6SuhthgHB9YkN8UyyPtS/sNVIyYRYGEgqu62mQMBIOYXTelSkyILGupK1Up5GZQc3utPLKCVxz4QaJpVPR3gTKtO91ChnlbXxLzfKWsjQF/UrJbEXe9EmrqJ7nt6CsGT48KHcf/iltRgkDMiXy8HozCdmPSdpiy2sbShtNTuvYfyXHdpSROoscgcJzX5VkV3amTwCZ9XR53nVXtt3DR6T46GbMBSTA15eyYVxySKy+XbbM/+wA+K7YBqTJtieSMVyJ61SGk0ZWuiWU6hMjQ8gDYI6rvlb4TjNkl0SYCkDMCdptnQ+tD+WA== Kod odpowiedzi HTTP: 415 Odpowiedź serwera (jako tekst): Nie udało się uzyskać tokenu sesji.

0
<?php

// Funkcja do walidacji XML względem XSD
function validateXML($xmlContent, $xsdPath) {
    libxml_use_internal_errors(true);
    $xml = new DOMDocument();
    $xml->loadXML($xmlContent);

    if (!$xml->schemaValidate($xsdPath)) {
        echo "Błędy walidacji XML względem XSD:\n";
        foreach (libxml_get_errors() as $error) {
            echo "\t", $error->message, "\n";
        }
        libxml_clear_errors();
        return false;
    }
    echo "XML jest zgodny ze schematem XSD.\n";
    return true;
}

// Funkcja do wysyłania zapytań HTTP
function sendRequest($url, $data, $headers) {
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    if ($data) {
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    }

    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    return ['response' => $response, 'httpCode' => $httpCode];
}

// Funkcja do uzyskania challenge i timestamp z endpointu /online/Session/AuthorisationChallenge
function getChallengeAndTimestamp($apiUrl, $nip) {
    $challengeUrl = "$apiUrl/online/Session/AuthorisationChallenge";
    $challengeData = json_encode([
        "contextIdentifier" => [
            "type" => "onip",
            "identifier" => $nip
        ]
    ]);
    $challengeHeaders = [
        "Content-Type: application/json",
        "Accept: application/json"
    ];

    $challengeResponse = sendRequest($challengeUrl, $challengeData, $challengeHeaders);
    if ($challengeResponse['httpCode'] === 201) {
        return json_decode($challengeResponse['response'], true);
    } else {
        die("Błąd w uzyskiwaniu challenge: " . $challengeResponse['response']);
    }
}

// Funkcja do szyfrowania tokenu kluczem publicznym
function encryptToken($token, $publicKey) {
    $encryptedToken = '';
    if (openssl_public_encrypt($token, $encryptedToken, $publicKey)) {
        echo "Token zaszyfrowany pomyślnie.\n";
        return base64_encode($encryptedToken);
    } else {
        echo "Nie udało się zaszyfrować tokenu.\n";
        return false;
    }
}

// Funkcja do uzyskania tokenu sesji KSeF
function getKSeFSessionToken($apiUrl, $encryptedToken, $challenge, $nip, $xsdPath) {
    $xml = new SimpleXMLElement('<ns3:InitSessionTokenRequest/>');
    $xml->addAttribute('xmlns:ns2', 'http://ksef.mf.gov.pl/schema/gtw/svc/types/2021/10/01/0001');
    $xml->addAttribute('xmlns:ns3', 'http://ksef.mf.gov.pl/schema/gtw/svc/online/auth/request/2021/10/01/0001');
    $xml->addAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');

    // Dodanie elementów zgodnie z wymogami schematu
    $context = $xml->addChild('ns3:Context');
    $context->addChild('ns3:Challenge', $challenge, 'http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001');
    
    $identifier = $context->addChild('ns3:Identifier', null, 'http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001');
    $identifier->addAttribute('xsi:type', 'ns2:SubjectIdentifierByCompanyType');
    $identifier->addChild('ns2:Identifier', $nip);

    $documentType = $context->addChild('ns3:DocumentType', null, 'http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001');
    $documentType->addChild('ns2:Service', 'KSeF');
    $formCode = $documentType->addChild('ns2:FormCode');
    $formCode->addChild('ns2:SystemCode', 'FA (2)');
    $formCode->addChild('ns2:SchemaVersion', '1-0E');
    $formCode->addChild('ns2:TargetNamespace', 'http://crd.gov.pl/wzor/2023/06/29/12648/');
    $formCode->addChild('ns2:Value', 'FA');

    $context->addChild('ns3:Token', trim($encryptedToken), 'http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001');
    
    $xmlRequest = $xml->asXML();

    // Walidacja XML względem XSD
    if (!validateXML($xmlRequest, $xsdPath)) {
        echo "XML nie przeszedł walidacji i nie zostanie wysłany.\n";
        return false;
    }

    echo "Wygenerowany XML przed wysyłką:\n" . $xmlRequest . "\n";

    $initTokenUrl = "$apiUrl/online/Session/InitToken";
    $initTokenHeaders = [
        "Content-Type: application/xml",
        "Accept: application/json"
    ];

    $initTokenResponse = sendRequest($initTokenUrl, $xmlRequest, $initTokenHeaders);
    if ($initTokenResponse['httpCode'] === 200) {
        $sessionData = json_decode($initTokenResponse['response'], true);
        return $sessionData;
    } else {
        echo "Błąd w uzyskiwaniu tokenu sesji: " . $initTokenResponse['response'] . "\n";
        return false;
    }
}

// Funkcja do wysyłania faktury
function sendInvoice($apiUrl, $sessionToken, $invoiceFile) {
    $invoiceData = file_get_contents($invoiceFile);
    if ($invoiceData === false) {
        die("Nie udało się wczytać pliku faktury.\n");
    }

    $sendInvoiceUrl = "$apiUrl/online/Invoice/Send";
    $invoiceHeaders = [
        "Content-Type: application/xml",
        "Accept: application/json",
        "SessionToken: $sessionToken"
    ];

    $invoiceResponse = sendRequest($sendInvoiceUrl, $invoiceData, $invoiceHeaders);
    if ($invoiceResponse['httpCode'] === 200) {
        echo "Faktura została wysłana.\n";
        return true;
    } else {
        echo "Błąd w wysyłaniu faktury: " . $invoiceResponse['response'] . "\n";
        return false;
    }
}

// Pełny proces

$apiUrl = "https://ksef-demo.mf.gov.pl/api";
$nip = "xxx";  // Twój NIP
$apiKey = "xxx"// Twój klucz API
$publicKeyPath = __DIR__ . '/publicKey.pem';  // Ścieżka do klucza publicznego
$xsdPath = __DIR__ . '/authRequest.xsd';  // Ścieżka do pliku XSD
$invoiceFile = __DIR__ . '/faktura_213-10-2024_2024-10-24.xml';  // Ścieżka do pliku XML z fakturą

// Krok 1: Uzyskaj challenge i timestamp
$challengeData = getChallengeAndTimestamp($apiUrl, $nip);
if ($challengeData) {
    $challenge = $challengeData['challenge'];
    $timestamp = $challengeData['timestamp'];
    echo "Uzyskano challenge: $challenge i timestamp: $timestamp\n";
}

// Krok 2: Zaszyfruj token
if (file_exists($publicKeyPath) && is_readable($publicKeyPath)) {
    $publicKey = file_get_contents($publicKeyPath);
    $tokenToEncrypt = "$timestamp|$apiKey";
    $encryptedToken = encryptToken($tokenToEncrypt, $publicKey);

    if ($encryptedToken) {
        echo "Zaszyfrowany token: $encryptedToken\n"; // Wyświetlenie zaszyfrowanego tokenu

        // Krok 3: Uzyskaj token sesji
        $sessionTokenData = getKSeFSessionToken($apiUrl, $encryptedToken, $challenge, $nip, $xsdPath);
        if ($sessionTokenData) {
            echo "Uzyskano token sesji: " . $sessionTokenData['sessionToken']['token'] . "\n";

            // Krok 4: Wyślij fakturę
            $sessionToken = $sessionTokenData['sessionToken']['token'];
            $sendResult = sendInvoice($apiUrl, $sessionToken, $invoiceFile);
            if ($sendResult) {
                echo "Faktura została pomyślnie wysłana.\n";
            }
        }
    } else {
        echo "Błąd podczas szyfrowania tokenu.\n";
    }
} else {
    echo "Nie udało się odczytać pliku z kluczem publicznym.\n";
    exit;
}
?>

dodałem walidacje xml okazuje się ze jest w nim problem ...: Uzyskano challenge: 20241101-CR-3E70930CD3-F2C9A1EF01-A5 i timestamp: 2024-11-01T07:31:40.652Z Token zaszyfrowany pomyślnie. Zaszyfrowany token: F7WH6Ps1I4QLL3uHY/eo4mZsX9sdxHKDGVRBDWXrdvIxhwPFiGVNXRpVLBNvA2Q0U9HHr3RufPmIjnhYkSFG3zmgKNyMdVcVTEiw377vdhF/IexaqN/NUI34XriHkvqDMDXEGyHfmVnONZv8cTQsqiFvrXuL5dXN0Ob7JIaN36iHCBblL5VL6unxusGr3+xQEyJ11v36kx6XgNoCzkAP12ANkT9TGTs9sZxAGSPNyIJSypjaARk4wnpgJSrxGMljUir2NlDeYBkS46Y1YU4G8m0aHn70hU4w0msWE3K+HWv4/C9i8UkrZFXH2MKjxt6OGtRfwy5gc+Z+ookZ9xlngw== Błędy walidacji XML względem XSD: Namespace prefix ns3 on InitSessionTokenRequest is not defined failed to load external entity "/var/www/html/ksef/authRequest(4).xsd" Failed to locate the main schema resource at '/var/www/html/ksef/authRequest(4).xsd'. XML nie przeszedł walidacji i nie zostanie wysłany.

0

Finalnie 😀

<?php

class KSeFClient {
    private $apiUrl;
    private $nip;
    private $apiKey;
    private $publicKeyPath;
    private $sessionToken;

    public function __construct($apiUrl, $nip, $apiKey, $publicKeyPath) {
        $this->apiUrl = $apiUrl;
        $this->nip = $nip;
        $this->apiKey = $apiKey;
        $this->publicKeyPath = $publicKeyPath;
    }

    private function sendRequest($url, $data, $headers, $method = 'POST') {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); 
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
        if ($data) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        }

        $response = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        $curlError = curl_error($curl);
        curl_close($curl);

        if ($curlError) {
            echo "Błąd cURL: " . $curlError . "\n";
        }

        return ['response' => $response, 'httpCode' => $httpCode];
    }

    public function getChallengeAndTimestamp() {
        $url = "{$this->apiUrl}/online/Session/AuthorisationChallenge";
        $data = json_encode([
            "contextIdentifier" => [
                "type" => "onip",
                "identifier" => $this->nip
            ]
        ]);
        $headers = ["Content-Type: application/json", "Accept: application/json"];

        $response = $this->sendRequest($url, $data, $headers);
        if ($response['httpCode'] === 201) {
            return json_decode($response['response'], true);
        } else {
            die("Błąd w uzyskiwaniu challenge: " . $response['response']);
        }
    }

    private function encryptToken($token, $challengeTimeMillis) {
        $dataToEncrypt = "$token|$challengeTimeMillis";
        $encryptedToken = '';
        $publicKey = file_get_contents($this->publicKeyPath);
        if (openssl_public_encrypt($dataToEncrypt, $encryptedToken, $publicKey, OPENSSL_PKCS1_PADDING)) {
            return base64_encode($encryptedToken);
        } else {
            echo "Nie udało się zaszyfrować tokenu.\n";
            return false;
        }
    }

    public function getKSeFSessionToken($encryptedToken, $challenge) {
        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->formatOutput = true;

        $root = $dom->createElement('ns3:InitSessionTokenRequest');
        $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns2', 'http://ksef.mf.gov.pl/schema/gtw/svc/types/2021/10/01/0001');
        $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns3', 'http://ksef.mf.gov.pl/schema/gtw/svc/online/auth/request/2021/10/01/0001');
        $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns4', 'http://ksef.mf.gov.pl/schema/gtw/svc/online/types/2021/10/01/0001');
        $dom->appendChild($root);

        $context = $dom->createElement('ns3:Context');
        $root->appendChild($context);
        $context->appendChild($dom->createElement('ns4:Challenge', $challenge));
        $identifier = $dom->createElement('ns4:Identifier');
        $identifier->setAttribute('xsi:type', 'ns2:SubjectIdentifierByCompanyType');
        $identifier->appendChild($dom->createElement('ns2:Identifier', $this->nip));
        $context->appendChild($identifier);

        $tokenElement = $dom->createElement('ns4:Token', trim($encryptedToken));
        $context->appendChild($tokenElement);

        $url = "{$this->apiUrl}/online/Session/InitToken";
        $headers = ["Content-Type: application/octet-stream", "Accept: application/json"];

        $response = $this->sendRequest($url, $dom->saveXML(), $headers);
        if ($response['httpCode'] === 200 || $response['httpCode'] === 201) {
            $this->sessionToken = json_decode($response['response'], true)['sessionToken']['token'];
            return $this->sessionToken;
        } else {
            echo "Błąd w uzyskiwaniu tokenu sesji.\n";
            return false;
        }
    }

    public function sendInvoice($invoiceFile) {
        if (!file_exists($invoiceFile) || !is_readable($invoiceFile)) {
            die("Plik faktury nie istnieje lub nie można go odczytać: $invoiceFile\n");
        }

        $invoiceData = file_get_contents($invoiceFile);
        $hashSHA = base64_encode(hash('sha256', $invoiceData, true));
        $invoiceBody = base64_encode($invoiceData);
        $fSize = filesize($invoiceFile);

        $body = json_encode([
            "invoiceHash" => [
                "fileSize" => $fSize,
                "hashSHA" => ["algorithm" => "SHA-256", "encoding" => "Base64", "value" => $hashSHA]
            ],
            "invoicePayload" => ["type" => "plain", "invoiceBody" => $invoiceBody]
        ]);

        $headers = [
            'Accept: application/json',
            'SessionToken: ' . $this->sessionToken,
            'Content-Type: application/json'
        ];

        $response = $this->sendRequest("{$this->apiUrl}/online/Invoice/Send", $body, $headers, 'PUT');
        $httpCode = $response['httpCode'];

        if ($httpCode === 200 || $httpCode === 201) {
            return json_decode($response['response'], true);
        } elseif ($httpCode === 202) {
            echo "Żądanie zaakceptowane do przetwarzania.\n";
            return json_decode($response['response'], true);
        } else {
            echo "Błąd w wysyłaniu faktury.\n";
            return false;
        }
    }

    public function terminateSession() {
        $url = "{$this->apiUrl}/online/Session/Terminate";
        $headers = ['Accept: application/json', 'SessionToken: ' . $this->sessionToken];
        $response = $this->sendRequest($url, null, $headers, 'GET');

        if ($response['httpCode'] === 200) {
            echo "Sesja została zakończona pomyślnie.\n";
            return json_decode($response['response'], true);
        } else {
            echo "Błąd zamknięcia sesji.\n";
            return false;
        }
    }
}

// Użycie klasy
$ksefClient = new KSeFClient("https://ksef-demo.mf.gov.pl/api", "nip", "ApiKey", __DIR__ . '/publicKey.pem');
$challengeData = $ksefClient->getChallengeAndTimestamp();

if ($challengeData) {
    $challenge = $challengeData['challenge'];
    $challengeTimeMillis = strtotime($challengeData['timestamp']) * 1000;
    $encryptedToken = $ksefClient->encryptToken("xxx", $challengeTimeMillis);

    if ($encryptedToken && $ksefClient->getKSeFSessionToken($encryptedToken, $challenge)) {
        $ksefClient->sendInvoice(__DIR__ . '/faktura.xml');
        $ksefClient->terminateSession();
    }
}
?>

Jeszcze dodam wersje pod C# REST Api i Graphql (hot chocolate)

1

Dodałem walidator do FV w xml ... https://github.com/bsdnetpl/ValidatorFVXml

A duże podziękowania dla: asoio https://4programmers.net/Profile/117335

0

Dodałem funkcje sprawdzania statusu sesji

// Funkcja do sprawdzania statusu sesji
function getSessionStatus($apiUrl, $sessionToken, $referenceNumber, $pageSize = 10, $pageOffset = 0, $includeDetails = true) {
    // Budowanie URL z parametrami zapytania
    $statusUrl = "$apiUrl/online/Session/Status/$referenceNumber";
    $queryParams = http_build_query([
        'PageSize' => $pageSize,
        'PageOffset' => $pageOffset,
        'IncludeDetails' => $includeDetails ? 'true' : 'false'
    ]);
    $statusUrl .= '?' . $queryParams;

    // Nagłówki żądania
    $headers = [
        'Accept: application/json',
        'SessionToken: ' . $sessionToken
    ];

    // Wysłanie żądania
    $statusResponse = sendRequest($statusUrl, null, $headers, 'GET');
    $httpCode = $statusResponse['httpCode'];

    if ($httpCode === 200) {
        // Parsowanie odpowiedzi JSON
        return json_decode($statusResponse['response'], true);
    } else {
        // Obsługa błędów
        echo "Błąd w sprawdzaniu statusu sesji. Kod odpowiedzi HTTP: $httpCode\n";
        echo "Treść odpowiedzi: " . $statusResponse['response'] . "\n";
        return false;
    }
}
0

Sugestie poprawki proszę dodać do : https://github.com/bsdnetpl/KSeFClient

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.