xmlvalid / xmlvalid-php
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
Requires
- php: ^8.2
- ext-json: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-client-implementation: *
- psr/http-factory: ^1.0
- psr/http-factory-implementation: *
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.9
- laravel/pint: ^1.20
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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