I got 401 error when sent invoice

I’m working on Phase Two in a simulation environment.
I can generate the CSR and the compliance CSID normally using the OTP.
I’m also able to submit invoices through the compliance invoices endpoint, and they get accepted as valid without any issues.

However, when I try to send the invoice using:
https://gw-fatoora.zatca.gov.sa/e-invoicing/simulation/invoices/reporting/single
I receive a 401 Unauthorized error.

I’m working on a multi-tenant system, and here’s the code I’m using:

public function sendToZATCA(Invoice $invoice): array

{

    try {

        Log::info('Sending to ZATCA API', \['invoice_id' => $invoice->id\]);



        *// Set ICV and Previous Hash if not set*

        if (!$invoice->icv && $this->tenant) {

            $invoice->icv = $this->tenant->getNextICV();

        }

        if (!$invoice->previous_hash && $this->tenant) {

            $invoice->previous_hash = $this->tenant->zatca_previous_invoice_hash ??

                'NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==';

        }



        $threshold = (float) $this->b2b_threshold;

        $isB2B = (float) $invoice->total > $threshold;



        Log::info('Invoice type determined', \[

            'invoice_id' => $invoice->id,

            'total' => $invoice->total,

            'threshold' => $threshold,

            'is_b2b' => $isB2B

        \]);



        *// Generate compliant XML*

        $invoiceXML = $this->generateCompliantInvoiceXML($invoice, $isB2B);

        $invoiceHash = $this->calculateInvoiceHash($invoiceXML);



        *// Store hash for chaining*

        $invoice->update(\['invoice_hash' => $invoiceHash\]);

        if ($this->tenant) {

            $this->tenant->updatePreviousInvoiceHash($invoiceHash);

        }



        if ($isB2B) {

            return $this->clearInvoice($invoice, $invoiceXML, $invoiceHash);

        } else {

            return $this->reportInvoice($invoice, $invoiceXML, $invoiceHash);

        }



    } catch (\\Exception $e) {

        Log::error('ZATCA API call failed', \[

            'invoice_id' => $invoice->id,

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

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

        \]);



        return \[

            'success' => false,

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

        \];

    }

}

private function reportInvoice(Invoice $invoice, string $invoiceXML, string $invoiceHash): array

{

    try {

        Log::info('Reporting invoice (B2C)', \['invoice_id' => $invoice->id\]);



        $invoiceBase64 = base64_encode($invoiceXML);



        $payload = \[

            'invoiceHash' => $invoiceHash,

            'uuid' => $invoice->uuid,

            'invoice' => $invoiceBase64

        \];



        *// التحقق من بيانات الاعتماد وتحديثها إذا لزم الأمر*

        $csid = $this->csid;

        $secret = $this->secret;



        *// إذا لم تكن موجودة في الـ properties، احصل عليها من قاعدة البيانات*

        if (empty($csid) || empty($secret)) {

            if ($this->tenant) {

                $csid = $this->tenant->zatca_csid;

                $secret = $this->tenant->zatca_secret;



                *// حدث الـ properties للمرة القادمة*

                if ($csid && $secret) {

                    $this->csid = $csid;

                    $this->secret = $secret;

                }

            }

        }



        if (empty($csid) || empty($secret)) {

            throw new \\Exception('ZATCA credentials not configured for this tenant');

        }



        *// Create Basic Auth header manually to ensure correct format*

        $authHeader = 'Basic ' . base64_encode($csid . ':' . $secret);



        Log::info('ZATCA Report Request Details', \[

            'tenant_id' => $this->tenant?->id,

            'invoice_id' => $invoice->id,

            'invoice_uuid' => $invoice->uuid,

            'base_url' => $this->base_url,

            'csid_length' => strlen($csid),

            'secret_length' => strlen($secret),

            'auth_header_length' => strlen($authHeader),

            'payload' => \[

                'invoiceHash' => substr($payload\['invoiceHash'\], 0, 20) . '...',

                'uuid' => $payload\['uuid'\],

                'invoice_xml_size' => strlen($invoiceXML)

            \]

        \]);



        *// تحديد الـ endpoint حسب البيئة*

        $endpoint = $this->environment === 'simulation'

            ? '/compliance/invoices'  *// في simulation، استخدم compliance endpoint*

            : '/invoices/reporting/single';  *// في production، استخدم reporting endpoint*



        Log::info('Using endpoint', \[

            'environment' => $this->environment,

            'endpoint' => $endpoint,

            'full_url' => $this->base_url . $endpoint

        \]);



        $response = Http::timeout(30)

            ->withHeaders(\[

                'Accept' => 'application/json',

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

                'Accept-Version' => 'V2',

                'Accept-Language' => 'en',

                'Authorization' => $authHeader

            \])

            ->post($this->base_url . $endpoint, $payload);



        Log::info('ZATCA Report API response', \[

            'invoice_id' => $invoice->id,

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

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

        \]);



        if ($response->successful()) {

            $data = $response->json();



            $invoice->update(\[

                'zatca_reported_at' => now(),

                'zatca_status' => 'REPORTED'

            \]);



            return \[

                'success' => true,

                'type' => 'reporting',

                'validation_results' => $data\['validationResults'\] ?? \[\]

            \];

        }



        $responseData = $response->json() ?? \[\];

        $error = $this->formatErrorMessage($response->status(), $responseData);



        $invoice->update(\[

            'zatca_status' => 'REPORTING_FAILED',

            'zatca_error' => $error

        \]);



        return \[

            'success' => false,

            'error' => $error,

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

            'response' => $responseData

        \];



    } catch (\\Exception $e) {

        Log::error('Invoice reporting failed', \[

            'invoice_id' => $invoice->id,

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

        \]);



        $invoice->update(\[

            'zatca_status' => 'REPORTING_FAILED',

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

        \]);



        return \[

            'success' => false,

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

        \];

    }

}



*/\*\**

 *\* Clear invoice with ZATCA (B2B - Standard Invoice)*

 *\*/*

private function clearInvoice(Invoice $invoice, string $invoiceXML, string $invoiceHash): array

{

    try {

        Log::info('Clearing invoice (B2B)', \['invoice_id' => $invoice->id\]);



        $invoiceBase64 = base64_encode($invoiceXML);



        $payload = \[

            'invoiceHash' => $invoiceHash,

            'uuid' => $invoice->uuid,

            'invoice' => $invoiceBase64

        \];



        *// التحقق من بيانات الاعتماد وتحديثها إذا لزم الأمر*

        $csid = $this->csid;

        $secret = $this->secret;



        *// إذا لم تكن موجودة في الـ properties، احصل عليها من قاعدة البيانات*

        if (empty($csid) || empty($secret)) {

            if ($this->tenant) {

                $csid = $this->tenant->zatca_csid;

                $secret = $this->tenant->zatca_secret;



                *// حدث الـ properties للمرة القادمة*

                if ($csid && $secret) {

                    $this->csid = $csid;

                    $this->secret = $secret;

                }

            }

        }



        if (empty($csid) || empty($secret)) {

            throw new \\Exception('ZATCA credentials not configured for this tenant');

        }



        *// Create Basic Auth header manually to ensure correct format*

        $authHeader = 'Basic ' . base64_encode($csid . ':' . $secret);



        Log::info('ZATCA Clearance Request Details', \[

            'tenant_id' => $this->tenant?->id,

            'invoice_id' => $invoice->id,

            'invoice_uuid' => $invoice->uuid,

            'base_url' => $this->base_url,

            'csid_length' => strlen($csid),

            'secret_length' => strlen($secret),

            'payload' => \[

                'invoiceHash' => substr($payload\['invoiceHash'\], 0, 20) . '...',

                'uuid' => $payload\['uuid'\],

                'invoice_xml_size' => strlen($invoiceXML)

            \]

        \]);



        *// تحديد الـ endpoint حسب البيئة*

        $endpoint = $this->environment === 'simulation'

            ? '/compliance/invoices'  *// في simulation، استخدم compliance endpoint*

            : '/invoices/clearance/single';  *// في production، استخدم clearance endpoint*



        Log::info('Using endpoint', \[

            'environment' => $this->environment,

            'endpoint' => $endpoint,

            'full_url' => $this->base_url . $endpoint

        \]);



        $response = Http::timeout(30)

            ->withHeaders(\[

                'Accept' => 'application/json',

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

                'Accept-Version' => 'V2',

                'Accept-Language' => 'en',

                'Authorization' => $authHeader

            \])

            ->post($this->base_url . $endpoint, $payload);



        Log::info('ZATCA Clear API response', \[

            'invoice_id' => $invoice->id,

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

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

        \]);



        if ($response->successful()) {

            $data = $response->json();



            $invoice->update(\[

                'zatca_cleared_at' => now(),

                'zatca_status' => 'CLEARED'

            \]);



            return \[

                'success' => true,

                'type' => 'clearance',

                'cleared_invoice' => $data\['clearedInvoice'\] ?? null,

                'validation_results' => $data\['validationResults'\] ?? \[\]

            \];

        }



        $responseData = $response->json() ?? \[\];

        $error = $this->formatErrorMessage($response->status(), $responseData);



        $invoice->update(\[

            'zatca_status' => 'CLEARANCE_FAILED',

            'zatca_error' => $error

        \]);



        return \[

            'success' => false,

            'error' => $error,

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

            'response' => $responseData

        \];



    } catch (\\Exception $e) {

        Log::error('Invoice clearance failed', \[

            'invoice_id' => $invoice->id,

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

        \]);



        $invoice->update(\[

            'zatca_status' => 'CLEARANCE_FAILED',

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

        \]);



        return \[

            'success' => false,

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

        \];

    }

}

Dear @devmoaboabdo

Thanks for reaching out, I hope you are doing well.

Can you please confirm if you are using in the Authorization BasicAuth and the UserName and password are PCSID not the CSID?

Thanks,
Ibrahem Daoud.

I don’t think so.
From the questions I found, I understood that I need to test all 6 types of invoices using the CCSID that I generated, and then get the PCSID — but I don’t understand why.

Right now, only one type of invoice (the Simplified Invoice) works correctly, and the other types fail. So I can’t get the PCSID, which is a big problem.

Is it really necessary to test all 6 invoice types in order to obtain the PCSID?

And here’s my code — I’m working with Laravel, please take a look.
And how come I have only one invoice in the table, but it’s supposed to be six?
private function reportInvoice(Invoice $invoice, string $invoiceXML, string $invoiceHash): array

{

    try {

        Log::info('Reporting invoice (B2C)', \['invoice_id' => $invoice->id\]);



        $invoiceBase64 = base64_encode($invoiceXML);



        $payload = \[

            'invoiceHash' => $invoiceHash,

            'uuid' => $invoice->uuid,

            'invoice' => $invoiceBase64

        \];



        *// التحقق من بيانات الاعتماد وتحديثها إذا لزم الأمر*

        $csid = $this->csid;

        $secret = $this->secret;



        *// إذا لم تكن موجودة في الـ properties، احصل عليها من قاعدة البيانات*

        if (empty($csid) || empty($secret)) {

            if ($this->tenant) {

                $csid = $this->tenant->zatca_csid;

                $secret = $this->tenant->zatca_secret;



                *// حدث الـ properties للمرة القادمة*

                if ($csid && $secret) {

                    $this->csid = $csid;

                    $this->secret = $secret;

                }

            }

        }



        if (empty($csid) || empty($secret)) {

            throw new \\Exception('ZATCA credentials not configured for this tenant');

        }



        *// Create Basic Auth header manually to ensure correct format*

        $authHeader = 'Basic ' . base64_encode($csid . ':' . $secret);



        Log::info('ZATCA Report Request Details', \[

            'tenant_id' => $this->tenant?->id,

            'invoice_id' => $invoice->id,

            'invoice_uuid' => $invoice->uuid,

            'base_url' => $this->base_url,

            'csid_length' => strlen($csid),

            'secret_length' => strlen($secret),

            'auth_header_length' => strlen($authHeader),

            'payload' => \[

                'invoiceHash' => substr($payload\['invoiceHash'\], 0, 20) . '...',

                'uuid' => $payload\['uuid'\],

                'invoice_xml_size' => strlen($invoiceXML)

            \]

        \]);



        *// $hasPCSID = !empty($this->tenant?->zatca_production_csid) && !empty($this->tenant?->zatca_production_secret);*

        *// if ($this->environment === 'simulation') {*

        *//     $endpoint = $hasPCSID ? '/invoices/reporting/single' : '/compliance/invoices';*

        *// } else {*

            $endpoint = '/invoices/reporting/single';

        *// }*



        Log::info('Using endpoint', \[

            'environment' => $this->environment,

            'endpoint' => $endpoint,

            'full_url' => $this->base_url . $endpoint

        \]);



        $response = Http::timeout(30)

            ->withHeaders(\[

                'Accept' => 'application/json',

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

                'Accept-Version' => 'V2',

                'Accept-Language' => 'en',

                'Authorization' => $authHeader

            \])

            ->post($this->base_url . $endpoint, $payload);



        Log::info('ZATCA Report API response', \[

            'invoice_id' => $invoice->id,

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

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

        \]);



        if ($response->successful()) {

            $data = $response->json();



            $invoice->update(\[

                'zatca_reported_at' => now(),

                'zatca_status' => 'REPORTED'

            \]);



            return \[

                'success' => true,

                'type' => 'reporting',

                'validation_results' => $data\['validationResults'\] ?? \[\]

            \];

        }



        $responseData = $response->json() ?? \[\];

        $error = $this->formatErrorMessage($response->status(), $responseData);



        $invoice->update(\[

            'zatca_status' => 'REPORTING_FAILED',

            'zatca_error' => $error

        \]);



        return \[

            'success' => false,

            'error' => $error,

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

            'response' => $responseData

        \];



    } catch (\\Exception $e) {

        Log::error('Invoice reporting failed', \[

            'invoice_id' => $invoice->id,

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

        \]);



        $invoice->update(\[

            'zatca_status' => 'REPORTING_FAILED',

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

        \]);



        return \[

            'success' => false,

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

        \];

    }

}

Morning @devmoaboabdo

Thanks for following up.

Kindly note that you can’t obtain the PCSID until you complete all the compliance checks, The 401 you are receiving is regarding the Authorization part.
To provide comprehensive support as usual, kindly request a one-to-one meeting via the below mail based on this post:

SP mail: sp_support@zatca.gov.sa

Additionally, please share here the email address that you will reach out from.

Thanks,
Ibrahem Daoud.

i reached out with devmoaboabdo@gmail.com please help me fast as u can