Zatca phase two integration

السلام عليكم ورحمة الله وبركاتة
I’m working with Laravel and doing the second phase of the integration. I’m using CsrRequest from Salla/ZATCA to generate it and that part works fine — all of this is in the sandbox (test) environment. I want to obtain the csid and secret key, but I get an error when I try to use the compliance endpoint from the sandbox:
{
“code”: “Invalid-Request”,
“message”: “Invalid message body, body should contain compliance_request_id field only as json filed”
}

What the Request Jason You are posting to generate Csid.

You need to first generate compliance csid and secret using
https://gw-fatoora.zatca.gov.sa/e-invoicing/core/compliance

Provide OTP and you will receive in reponse a request id ,CSID and Secret which needs to be used in complaince check API

https://gw-fatoora.zatca.gov.sa/e-invoicing/core/compliance/invoices

once Compliance check done then use below

https://gw-fatoora.zatca.gov.sa/e-invoicing/core/production/csids

and pass first step request id in the payload to obtain
Secret and PCSID for PROD

I was using Salla/Zatca for generate csr for sandbox env but i m now using this function to generate csr
public function generateCSR(): array

{

    try {

        if (!is_dir(storage_path('app/private'))) {

            mkdir(storage_path('app/private'), 0755, true);

        }



        $deviceSerial = '1-TST|2-TST|3-' . bin2hex(random_bytes(16));



        *// Configure EC key with P-256 curve (CRITICAL for ZATCA)*

        $config = \[

            "digest_alg" => "sha256",

            "private_key_type" => OPENSSL_KEYTYPE_EC,

            "curve_name" => "prime256v1" *// P-256 / secp256r1*

        \];



        *// Generate private key*

        $privateKey = openssl_pkey_new($config);

        if (!$privateKey) {

            throw new \\Exception('Failed to generate private key: ' . openssl_error_string());

        }



        *// Build DN (Distinguished Name) - ZATCA requirements*

        *// UID MUST be in the main DN, not just in SAN*

        $dn = \[

            "C" => "SA",

            "OU" => $this->business_category,

            "O" => $this->organization_name ?: $this->seller_name,

            "CN" => $this->organization_name ?: $this->seller_name,

            "UID" => $this->tax_number, *// CRITICAL: UID must be here*

        \];



        *// Create temp config file for OpenSSL extensions*

        $configContent = <<<EOT

\[req\]

distinguished_name = req_distinguished_name

req_extensions = v3_req



\[req_distinguished_name\]



\[v3_req\]

subjectAltName = @alt_names

extendedKeyUsage = 1.3.6.1.5.5.7.3.1



\[alt_names\]

dirName.1 = dir_sect



\[dir_sect\]

SN = {$deviceSerial}

UID = {$this->tax_number}

title = 1100

registeredAddress = {$this->registered_address}

businessCategory = {$this->business_category}

EOT;



        $tempConfigPath = storage_path('app/private/openssl_temp.cnf');

        file_put_contents($tempConfigPath, $configContent);



        *// Generate CSR with extensions*

        $csr = openssl_csr_new($dn, $privateKey, \[

            'digest_alg' => 'sha256',

            'config' => $tempConfigPath

        \]);



        *// Clean up temp config*

        @unlink($tempConfigPath);



        if (!$csr) {

            throw new \\Exception('Failed to generate CSR: ' . openssl_error_string());

        }



        *// Export private key and CSR*

        if (!openssl_pkey_export($privateKey, $privateKeyPem)) {

            throw new \\Exception('Failed to export private key');

        }



        if (!openssl_csr_export($csr, $csrPem)) {

            throw new \\Exception('Failed to export CSR');

        }



        *// Save files*

        $privateKeyPath = storage_path('app/private/zatca_private_key.pem');

        $csrPath = storage_path('app/private/zatca_csr.pem');



        file_put_contents($privateKeyPath, $privateKeyPem);

        file_put_contents($csrPath, $csrPem);



        Log::info('CSR generated successfully with P-256 curve', \[

            'device_serial' => $deviceSerial,

            'tax_number' => $this->tax_number

        \]);



        *// Verify the CSR was created correctly*

        $csrData = openssl_csr_get_subject($csrPem, false);

        $publicKey = openssl_csr_get_public_key($csrPem);

        $keyDetails = openssl_pkey_get_details($publicKey);



        Log::info('CSR verification', \[

            'subject' => $csrData,

            'key_type' => $keyDetails\['type'\] === OPENSSL_KEYTYPE_EC ? 'EC' : 'Other',

            'curve' => $keyDetails\['ec'\]\['curve_name'\] ?? 'Unknown'

        \]);



        return \[

            'success' => true,

            'csr_content' => $csrPem,

            'message' => 'CSR generated successfully with P-256 curve',

            'files' => \[

                'private_key' => $privateKeyPath,

                'csr' => $csrPath

            \],

            'verification' => \[

                'subject' => $csrData,

                'curve' => $keyDetails\['ec'\]\['curve_name'\] ?? 'Unknown'

            \]

        \];



    } catch (\\Exception $e) {

        Log::error('CSR generation failed', \[

            'error' => $e->getMessage(),

            'trace' => $e->getTraceAsString()

        \]);



        return \[

            'success' => false,

            'error' => $e->getMessage()

        \];

    }

}

but when im trying to generate csid with
https://gw-fatoora.zatca.gov.sa/e-invoicing/simulation/compliance
i got invalid csr i don’t know why ?
this function
public function getComplianceCSID(string $otp): array

{

    try {

        if (!$this->hasValidCSR()) {

            return \[

                'success' => false,

                'error' => 'No valid CSR found. Please generate CSR first.'

            \];

        }



        $csrPath = storage_path('app/private/zatca_csr.pem');

        $rawPem = file_get_contents($csrPath);



        *// Clean CSR for ZATCA API - remove headers and all whitespace*

        $csrContent = preg_replace('/-----BEGIN CERTIFICATE REQUEST-----/i', '', $rawPem);

        $csrContent = preg_replace('/-----END CERTIFICATE REQUEST-----/i', '', $csrContent);

        $csrContent = trim(preg_replace('/\\s+/', '', $csrContent));



        *// Validate base64 payload strictly; sandbox rejects otherwise*

        $decoded = base64_decode($csrContent, true);

        if ($decoded === false || base64_encode($decoded) !== $csrContent) {

            return \[

                'success' => false,

                'error' => 'CSR invalid: payload is not valid base64 content expected by ZATCA.'

            \];

        }



        Log::info('Cleaned CSR length', \['length' => strlen($csrContent)\]);



        $payload = \['csr' => $csrContent\];



        Log::info('Requesting Compliance CSID from ZATCA Sandbox');



        $response = Http::timeout(30)

            ->withHeaders(\[

                'Accept' => 'application/json',

                'Content-Type' => 'application/json',

                'Accept-Version' => 'V2',

                'Accept-Language' => 'en',

                'OTP' => $otp

            \])

            ->post($this->base_url . '/compliance', $payload);



        Log::info('ZATCA Compliance CSID response', \[

            'status' => $response->status(),

            'response' => $response->json()

        \]);



        if ($response->successful()) {

            $data = $response->json();



            return \[

                'success' => true,

                'binarySecurityToken' => $data\['binarySecurityToken'\] ?? null,

                'secret' => $data\['secret'\] ?? null,

                'request_id' => $data\['requestID'\] ?? null,

                'disposition' => $data\['disposition'\] ?? null

            \];

        }



        $responseData = $response->json();

        $error = $responseData\['message'\] ?? 'Failed to get Compliance CSID - HTTP ' . $response->status();



        return \[

            'success' => false,

            'error' => $error,

            'status_code' => $response->status(),

            'response' => $responseData

        \];



    } catch (\\Exception $e) {

        Log::error('Compliance CSID request failed', \[

            'error' => $e->getMessage(),

            'trace' => $e->getTraceAsString()

        \]);



        return \[

            'success' => false,

            'error' => $e->getMessage()

        \];

    }

}

so i need help im working in local env not production

for laravel project:

composer require sevaske/zatca
php artisan zatca:install

usage:

php artisan zatca:generate-csr
php artisan zatca:compliance-certificate
php artisan zatca:production-certificate

more details GitHub - sevaske/zatca: Zatca laravel package

1 Like

Hello taron,

Hope that you are well.

It’s nice to hear that you have used the Sevaske package for laravel. I was also trying with it, but finally I didn’t get the proper result.

If you have properly completed both Phase 1 and Phase 2, that is great. So I need some help from you about it, and this is very urgent.

Looking forward to your response.

Best regards,