Search by

xmlvalid / xmlvalid-php

alexbabintsev

PHP client for the xmlvalid API: validate XML and e-invoices (Peppol, XRechnung, Factur-X, UBL, CII) against XSD, DTD, RelaxNG and Schematron business rules

v1.2.0 2026-10-19 19:13 UTC

This package is auto-updated.

Last update: 2026-10-19 19:20:04 UTC


README

PHP client for the xmlvalid API: validate XML documents and e-invoices against XSD, DTD, RelaxNG and Schematron business rules. Peppol BIS, XRechnung, Factur-X / ZUGFeRD, EN 16931, UBL, CII and national rules (France, Italy, Croatia, Romania and more) are in the schema catalog; your own schemas work too.

  • PHP 8.2+, any PSR-18 HTTP client (Guzzle, Symfony HttpClient, …)
  • Files are streamed, never loaded into memory
  • Waits for the result for you, retries when rate limited
  • Typed results and exceptions, webhook signature checks

Install

composer require xmlvalid/xmlvalid-php

If your project has no PSR-18 client yet, Composer adds one for you (php-http/discovery). Create an API key under Dashboard → API keys.

Validate

use Xmlvalid\Client;
use Xmlvalid\Document;

$xmlvalid = new Client(getenv('XMLVALID_API_KEY'));

$validation = $xmlvalid->validate(
    Document::file('invoice.xml'),
    schema: 'peppol-bis-3-invoice',
);

if ($validation->isValid()) {
    echo "Valid\n";
} else {
    foreach ($validation->errors() as $error) {
        echo $error, PHP_EOL; // Line 139: [BR-CO-16] Amount due for payment (BT-115) = …
    }
}

The document can be:

Document::file('invoice.xml');                       // a file (XML, .gz, .zip, EPUB, Factur-X PDF)
Document::string($xml, 'invoice.xml');               // a string
Document::stream($resource, 'invoice.xml');          // an open stream
Document::url('https://example.com/feed.xml');       // a public URL, downloaded by xmlvalid

The schema:

Argument Validates against
schema: 'peppol-bis-3-invoice' A catalog entry, with its business rules
schema: 'auto' The catalog profile an e-invoice names (BT-24), for mixed invoices
schema: 'my-12' One of your saved schemas
schemaFile: 'order.xsd' An XSD, DTD or RelaxNG file sent with the document
doctype: true The DOCTYPE the document declares
none Well-formedness only

Your own Schematron runs after the schema check: rules: 'rules.sch', or for rules with includes, relative path => file: rules: ['main.sch' => '/path/main.sch', 'parts/totals.sch' => '/path/parts/totals.sch'].

A catalog schema's business rules move to new releases (Peppol BIS, XRechnung). To fail instead of getting findings from rules that changed, pin the releases a check was written against: rulesVersion: 'peppol-bis-3-ubl@3.0.21' (or a list). If one runs another release, RulesVersionMismatchException is thrown before anything runs or counts. schemas() lists each schema's ruleSets with their current releases.

The result

$validation->status;               // done, failed (on xmlvalid's side) or cancelled
$validation->result->outcome;      // valid, warnings, invalid, schema, input or failed
$validation->result->errorsTotal;  // all of them; the first 500 are listed
foreach ($validation->errors() as $error) {
    $error->line; $error->column; $error->level; $error->type; // xsd, schematron, …
    $error->rule;                  // BR-CO-16 for a business rule
    $error->path;                  // where it fired: /Invoice/cac:LegalMonetaryTotal
    $error->message;
    $error->ruleUrl;               // the rule's page on xmlvalid.com
    $error->helpUrl;               // how to fix this kind of error
}
$validation->warnings();
foreach ($validation->ruleSets as $set) {  // the business rules run, at the releases used
    echo $set;                     // Peppol BIS Billing 3.0 rules 3.0.21; $set->pin() for rulesVersion
}
$validation->result->sha256;       // SHA-256 of the file as sent
$validation->result->engine;       // Engine: xmlvalid 0.1.0, libxml2 2.13.9, saxonjs 2.7.0 (null without rules)
$validation->raw;                  // the API's JSON, as an array

For a .zip, $validation->result->items has one result per document; for a PDF, $validation->result->pdf names the invoice found in it.

Long validations, reports and the account

validate() waits up to 15 minutes (timeout:); a TimeoutException carries the validation, which goes on on the server. To queue and come back later:

$validation = $xmlvalid->submit(Document::file('big.xml'), schema: 'ubl-2.1-invoice');
// …
$validation = $xmlvalid->waitFor($validation->id);   // or $xmlvalid->validation($id), $xmlvalid->cancel($id)

$html = $xmlvalid->report($validation->id);          // the HTML report
$link = $xmlvalid->share([$validation->id]);         // a public link for 30 days (Pro): $link->url

$usage = $xmlvalid->usage();                         // $usage->used, $usage->limit, $usage->remaining()
$schemas = $xmlvalid->schemas();                     // your saved schemas, then the catalog

Errors

Every exception extends Xmlvalid\Exception\XmlvalidException. API refusals are ApiExceptions with $statusCode, $errorCode and, for invalid_request, $fields:

Exception When
AuthenticationException 401: no key, a revoked key
PlanLimitException 403: not in your plan
NotFoundException 404: no such validation, or it expired (24 hours)
NotFinishedException 409: the report of a validation still running
RulesVersionMismatchException 409: a rule set pinned with rulesVersion runs another release; $ruleSet, $pinned, $current
DocumentTooLargeException 413: over your plan's size limit
InvalidRequestException 422: unknown schema, a bad URL, …
QuotaExceededException 429: this month's validations are used up
RateLimitedException 429: still limited after the retries; $retryAfter seconds
ServerException 5xx
TransportException The request could not be sent or its answer read
TimeoutException Still running after timeout:

Webhooks

On Pro, xmlvalid can call your endpoint when a validation finishes or a URL monitor changes state. Check the signature with the endpoint's secret and the raw body:

use Xmlvalid\Model\Event;
use Xmlvalid\Webhook;

$event = Webhook::verify(
    file_get_contents('php://input'),
    $_SERVER['HTTP_XMLVALID_SIGNATURE'] ?? '',
    getenv('XMLVALID_WEBHOOK_SECRET'),
); // throws SignatureVerificationException

if ($event->type === Event::VALIDATION_COMPLETED) {
    $validation = $event->validation();
}

Options

new Client(
    apiKey: 'xv_…',
    baseUrl: 'https://xmlvalid.com/api/v1',
    httpClient: $psr18Client,        // with requestFactory: and streamFactory: (PSR-17)
    timeout: 900,                    // seconds validate() waits in all
    wait: 10,                        // seconds the API holds a new request (0-60)
    maxRetries: 3,                   // retries of a 429 rate_limited
);

Development

composer install
composer test        # PHPUnit
composer analyse     # PHPStan, level max
composer lint        # Pint (PER)

License

MIT