PHP code example of taxora / sdk-php

1. Go to this page and download the library: Download taxora/sdk-php library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

taxora / sdk-php example snippets


use Taxora\Sdk\TaxoraClientFactory;
use Taxora\Sdk\Enums\Environment;

$client = TaxoraClientFactory::create(
    apiKey: 'YOUR_X_API_KEY',
    environment: Environment::SANDBOX // or PRODUCTION
);

// 1️⃣ Authenticate
$client->auth()->login('[email protected]', 'superSecret');

// 2️⃣ Validate a VAT number
$vat = $client->vat()->validate('ATU12345678', 'Example GmbH');
echo $vat->state->value;        // valid / invalid
echo $vat->company_name; // Official company name
echo $vat->score;        // Overall confidence score (float)
var_dump($vat->has_api_error); // true when the official provider had a technical failure
echo $vat->error_message; // technical provider error details, if returned
echo $vat->next_api_recheck_at; // planned retry timestamp, if returned

foreach ($vat->breakdown ?? [] as $step) {
    echo $step->stepName.' gave '.$step->scoreContribution.PHP_EOL;
}

// Optional typed address input for fallback name scoring
$addressInput = new \Taxora\Sdk\ValueObjects\VatValidationAddressInput(
    addressLine1: 'Example GmbH',
    addressLine2: '2nd Floor',
    postalCode: '1010',
    city: 'Vienna',
    countryCode: 'AT'
);
$vatWithAddress = $client->vat()->validate('ATU12345678', 'John Doe', 'vies', $addressInput);

// 3️⃣ Access company info
$company = $client->company()->get();
echo $company['api_rate_limit'] ?? 'n/a';
echo $company['vat_rate_limit'] ?? 'n/a';

// 4️⃣ Export certificates (returns a VatCertificateExport object)
$export = $client->vat()->certificatesBulkExport('2024-01-01', '2024-12-31');
$pdfZip = $client->vat()->downloadBulkExport($export->exportId);
file_put_contents('certificates.zip', $pdfZip);

// Single lookup
$job = $client->smartEnrichment()->lookup(
    'Example Company GmbH',
    'AT',
    street: 'Main Street 10',
    postalCode: '1010',
    city: 'Vienna',
);

if ($job->isProcessing()) {
    // Resolved asynchronously — poll (or wait for the webhook)
    $job = $client->smartEnrichment()->get($job->jobId);
}

$result = $job->result();
echo $result?->status->value;   // found | no_vat_exists | not_found
echo $result?->vatNumber;       // e.g. ATU12345678
echo $result?->confidence;      // 0–100

// Search mode: `default` searches with one AI provider and only consults a second one
// when the first finds nothing. `complex` has two independent providers search from the
// start and puts every number either of them proposes through the tax authority's checks.
// Complex finds companies a single search misses — typically ones whose VAT only appears
// in a website Impressum — and costs more per lookup, so it is opt-in.
$job = $client->smartEnrichment()->lookup(
    'Vodafone D2 GmbH',
    'DE',
    postalCode: '40547',
    mode: SmartEnrichmentMode::COMPLEX,
);

// When more than one provider searched, you can see what each one concluded on its own.
// Two of them reporting the same number is the strongest confirmation this layer produces.
foreach ($job->result()?->providerVerdicts ?? [] as $verdict) {
    echo $verdict['provider'] . ': ' . ($verdict['vatNumber'] ?? 'nothing') . PHP_EOL;
}

// The API also grades the address you sent. Worth writing back into your own records:
// a `postal_code_city_mismatch` / `postal_code_unassigned` warning means the postal code
// in your source data is wrong, and `derivedPlace` is the area it actually points at.
$quality = $job->result()?->addressQuality;
if ($quality !== null && $quality['warnings'] !== []) {
    echo implode(', ', $quality['warnings']) . ' → ' . ($quality['derivedPlace'] ?? '?') . PHP_EOL;
}

// Bulk lookup (always async → webhook + polling). The batch-level mode applies to every
// row; a row carrying its own 'mode' overrides it, so you can pay for `complex` on just
// the rows that need it.
$bulk = $client->smartEnrichment()->bulkLookup([
    ['companyName' => 'A GmbH', 'country' => 'DE', 'city' => 'Berlin'],
    ['companyName' => 'B SARL', 'country' => 'FR', 'mode' => 'complex'],
]);
$done = $client->smartEnrichment()->get($bulk->jobId);
foreach ($done->results as $r) {
    echo $r->status->value . ': ' . ($r->vatNumber ?? '—') . PHP_EOL;
}

// Or block until the job is done (polls get() every 2s, up to 120s by default;
// throws Taxora\Sdk\Exceptions\TimeoutException when the wait runs out — the job
// keeps running server-side and can still be polled afterwards)
$done = $client->smartEnrichment()->waitUntilComplete(
    $bulk->jobId,
    timeoutSeconds: 120.0,
    pollIntervalSeconds: 2.0,
);

// Lookup history (paginated, newest first; optional free-text search over the
// queried company name and the resolved VAT number / matched name; perPage is
// capped at 100 server-side)
$history = $client->smartEnrichment()->history(page: 1, perPage: 25, search: 'Example');
echo $history->total . ' lookups' . PHP_EOL;
echo ($history->stats?->found ?? 0) . ' of ' . ($history->stats?->total ?? 0) . ' ever matched' . PHP_EOL;
foreach ($history as $row) {
    echo $row->queryCompanyName . ' → ' . ($row->result?->vatNumber ?? $row->status->value) . PHP_EOL;
}

// Quota usage for the current billing period (+ recent monthly billing history)
$usage = $client->smartEnrichment()->usage();
echo $usage->used . '/' . $usage->

// Prefill company data from the French SIRENE registry by SIRET, SIREN or FR VAT
// number (rate limit: 3 requests/minute).
$prefill = $client->eReporting()->sireneLookup('12345678900012');

$enrollment = $client->eReporting()->createEnrollment(
    email: '[email protected]',                    // notification e-mail (ret: $prefill->siret,                  // 14 chars — at least one of siret/siren
    siren: $prefill->siren,                  // 9-char fallback when no head office SIRET exists
    companyName: $prefill->companyName,
    address: $prefill->address,
    city: $prefill->city,
    postalcode: $prefill->postalcode,
    autoActivate: true,                      // false = create the account only
);

echo $enrollment->status->value;             // e.g. regime_activated
echo $enrollment->statusLabel;               // human-readable label from the API

// Later: list / fetch enrollments (paginated, perPage capped at 100)
$enrollments = $client->eReporting()->listEnrollments(page: 1, perPage: 25);
$enrollment  = $client->eReporting()->getEnrollment($enrollment->id);

use Taxora\Sdk\Enums\ComplianceTransactionType;
use Taxora\Sdk\ValueObjects\ComplianceInvoiceLine;
use Taxora\Sdk\ValueObjects\ComplianceLineTax;

$tx = $client->eReporting()->createTransaction(
    enrollmentId: $enrollment->id,
    transactionType: ComplianceTransactionType::B2C_OUTBOUND, // or 'b2c_outbound'
    invoiceNumber: 'TKT-2026-00001',        // max 50 chars
    invoiceDate: '2026-06-15',              // Y-m-d or DateTimeInterface
    subtotal: '100.00',
    total: '120.00',
    invoiceLines: [
        new ComplianceInvoiceLine(
            description: 'Museum ticket',
            quantity: 2,
            price: 50,
            taxes: [new ComplianceLineTax(name: 'TVA', percent: 20, category: 'S')],
        ),
    ],
    currency: 'EUR',                        // optional, defaults to EUR
    taxAmount: '20.00',
    submitNow: false,                       // defer provider submission
);

// Creation is idempotent: retrying the same invoice (enrollment + type + number +
// date + counterparty VAT) returns the existing transaction instead of a duplicate.

// Pending transactions can still be edited (PATCH semantics — only what you pass
// is changed; nullable fields cannot be cleared back to null through the SDK)
// or deleted; submit (or retry) explicitly when ready. Note: per-invoice
// submission 

// Semicolon-separated CSV, max 2 MB; rows are grouped into invoices by
// invoice_number, re-uploads are idempotent. Required columns: invoice_number,
// invoice_date, transaction_type, subtotal, total, line_description,
// line_quantity, line_price, tax_name, tax_percent, tax_category.
$result = $client->eReporting()->importTransactions(
    $enrollment->id,
    file_get_contents('transactions.csv'),  // raw CSV content
    'transactions.csv',
);
echo $result->created . ' created, ' . $result->skippedDuplicates . ' duplicates' . PHP_EOL;
foreach ($result->errors as $error) {
    echo $error . PHP_EOL;                  // per-row problems, e.g. bad tax category
}

$reports = $client->eReporting()->listTaxReports(state: 'refused'); // new | sent | acknowledged | registered | refused | error
foreach ($reports as $report) {
    echo $report->state->value . ' — ' . ($report->refusalReason ?? 'ok') . PHP_EOL;
}

$report = $client->eReporting()->getTaxReport(5);
if ($report->state->isFailure()) {
    echo $report->refusalReason;            // e.g. CDV 301 details
}

$stats = $client->eReporting()->getRevenueStatistics(
    dateFrom: '2026-01-01',          // optional — defaults to 12 months ago (server-side)
    dateTo: new DateTimeImmutable(), // optional — defaults to today
    interval: 'month',               // 'day' | 'week' | 'month' (default 'month')
    transactionType: 'b2c_outbound', // optional filter (string or ComplianceTransactionType)
    state: 'submitted',              // optional filter (string or ComplianceTransactionState)
);

echo $stats->primaryCurrency;        // e.g. EUR
echo $stats->totals->total;          // e.g. 120000.0000
foreach ($stats->timeSeries as $bucket) {
    echo $bucket->bucket . ': ' . $bucket->total . PHP_EOL; // 2026-01: 10800.0000
}
foreach ($stats->byCountry as $row) {
    echo $row['country'] . ' → ' . $row['total'] . PHP_EOL; // 'unknown' groups NULL partners
}

// Canonical VAT rates & DGFiP tax categories for a reporting country — the valid
// `category` values for ComplianceLineTax (an `E` rate needs a VATEX comment).
$rates = $client->eReporting()->getVatRates('FR');
foreach ($rates->rates as $rate) {
    echo $rate->percent . '% (' . $rate->category . ')' . ($rate->

// The one compliance endpoint that works WITHOUT the e-reporting feature —
// it files an activation request with the Taxora team. Identity defaults to
// the authenticated user; all parameters are optional context.
$client->eReporting()->requestEReportingAccess(
    company: 'Acme SARL',
    phone: '+33 1 23 45 67 89',
    message: 'We need Flux 10 e-reporting starting September.',
    language: 'fr',
    service: 'e_reporting',                 // optional: e_reporting (default) | e_invoicing
    countries: ['FR'],                      // optional: ISO alpha-2 codes you need
);

// Brønnøysund register lookup by org number (spaces / "NO…MVA" accepted → one
// result) or by name (search). 404 = nothing found; rate limit 30/minute.
$companies = $client->eReporting()->registryLookup('NO', '923 609 016');
$company   = $companies[0];                  // RegistryCompany

$enrollment = $client->eReporting()->createNorwayEnrollment(
    email: '[email protected]',             // notification e-mail
    orgNumber: $company->orgNumber,          // also the Peppol id (scheme 0192)
    companyName: $company->companyName ?? 'Example AS',
    address: $company->address ?? 'Storgata 1',
    city: $company->city ?? 'Oslo',
    postalcode: $company->postalcode ?? '0155',
    vatRegistered: $company->vatRegistered,  // MVA-registered (default true)
    enterpriseRegister: $company->enterpriseRegister, // adds "Foretaksregisteret" to invoices
    reception: false,                        // true = also receive e-invoices over Peppol
);

echo $enrollment->service?->value;           // e_invoicing
echo $enrollment->regime;                    // peppol_bis3

use Taxora\Sdk\Enums\PeppolLookupStatus;

$peppol = $client->eReporting()->peppolLookup('NO', '974760673'); // scheme defaults to 0192

if ($peppol->status === PeppolLookupStatus::PENDING) {
    sleep(3);                                 // the directory resolves asynchronously — ask again
    $peppol = $client->eReporting()->peppolLookup('NO', '974760673');
}

if ($peppol->status->isSuccess()) {           // reachable: buyer is in ELMA → send EHF
    print_r($peppol->documentTypes);          // e.g. xml.ubl.invoice.bis3, xml.ubl.credit_note.bis3
}

use Taxora\Sdk\ValueObjects\ComplianceInvoiceLine;
use Taxora\Sdk\ValueObjects\ComplianceLineTax;

$tx = $client->eReporting()->createTransaction(
    enrollmentId: $enrollment->id,
    transactionType: 'b2b_domestic_outbound',  // the only type for Norway
    invoiceNumber: 'NO-2026-0001',
    invoiceDate: '2026-10-01',
    dueDate: '2026-10-31',
    currency: 'NOK',
    subtotal: '1000.00',
    taxAmount: '250.00',
    total: '1250.00',
    invoiceLines: [
        new ComplianceInvoiceLine(
            description: 'Consulting',
            quantity: 10,
            price: 100,
            // Norwegian rates: getVatRates('NO') — 25/15/12 % are category S; AE = reverse charge
            taxes: [new ComplianceLineTax(name: 'MVA', percent: 25, category: 'S')],
        ),
    ],
    counterpartyName: 'Buyer AS',             // 

$credit = $client->eReporting()->createTransaction(
    enrollmentId: $enrollment->id,
    transactionType: 'b2b_domestic_outbound',
    invoiceNumber: 'NO-CN-2026-0001',
    invoiceDate: '2026-10-05',
    currency: 'NOK',
    subtotal: '1000.00',
    taxAmount: '250.00',
    total: '1250.00',
    invoiceLines: [/* the credited lines */],
    counterpartyName: 'Buyer AS',
    counterpartyRegisterId: '974760673',
    isCreditNote: true,
    amendedNumber: 'NO-2026-0001',            // the corrected invoice — 

use GuzzleHttp\Client as GuzzleAdapter;
use Http\Factory\Guzzle\RequestFactory;
use Http\Factory\Guzzle\StreamFactory;
use Taxora\Sdk\TaxoraClientFactory;

$client = TaxoraClientFactory::create(
    apiKey: 'YOUR_X_API_KEY',
    http: new GuzzleAdapter(),
    requestFactory: new RequestFactory(),
    streamFactory: new StreamFactory()
);

$dto = $client->vat()->validate('ATU12345678');
print_r($dto->toArray());

$retryCase = $client->vat()->validate('ATU44000001', 'Example GmbH');
if ($retryCase->state === \Taxora\Sdk\Enums\VatState::INVALID && $retryCase->has_api_error) {
    echo $retryCase->error_message.PHP_EOL;
    echo $retryCase->next_api_recheck_at.PHP_EOL;
}

   $client->auth()->login('email', 'password', device: 'my-server-01');
   // Passing device is optional; omitted value falls back to a generated host-based identifier.
   

   $client->auth()->loginWithClientId('client_abc123', 'client-secret', device: 'integration-box');
   

   $client->auth()->refresh();
   

   use Symfony\Component\Cache\Adapter\FilesystemAdapter;
   use Symfony\Component\Cache\Psr16Cache;
   use Taxora\Sdk\Http\Psr16TokenStorage;

   $cache = new Psr16Cache(new FilesystemAdapter());
   $storage = new Psr16TokenStorage($cache);
   $client = new TaxoraClient($http, $reqF, $strF, 'YOUR_KEY', $storage);
   

$client = new TaxoraClient(..., environment: Environment::PRODUCTION);

try {
    $vat = $client->vat()->validate('ATU12345678');
} catch (ValidationException $e) {
    // 422 — bad input
    $errors = $e->getErrors();
} catch (HttpException $e) {
    // e.g. "Taxora VAT validation failed after 3 attempts (HTTP 504 Gateway Timeout)."
    $logger->warning($e->getMessage(), ['status' => $e->getStatusCode()]);
}

use Taxora\Sdk\Http\RetryPolicy;

$client = TaxoraClientFactory::create(
    apiKey: 'YOUR_X_API_KEY',
    retryPolicy: new RetryPolicy(
        maxAttempts: 5,
        initialDelayMs: 250,
        retryableStatusCodes: [502, 503, 504],  // add or drop status codes
        respectRetryAfter: true,                // false = ignore the header entirely
        maxRetryAfterMs: 10_000,                // longest wait accepted from Retry-After
        retryOnNetworkErrors: true,             // false = surface transport errors at once
    ),
);

// or, for callers that do their own retrying (queue workers, cron jobs):
$client = TaxoraClientFactory::create(apiKey: '…', retryPolicy: RetryPolicy::disabled());
bash
composer