bluerock / superpdp-php-client
The Official Super PDP API PHP Client/SDK
Package info
github.com/bluerocktel/SuperPDP-PHP-Api-Client
pkg:composer/bluerock/superpdp-php-client
Requires
- php: ^8.2
- illuminate/collections: >=12.0
- league/oauth2-client: ^2.7
- saloonphp/saloon: ^4.0
Requires (Dev)
- pestphp/pest: ^3.0
This package is auto-updated.
Last update: 2026-05-26 08:34:12 UTC
README
PHP client/SDK for the Super PDP API — the platform for sending and receiving electronic invoices in compliance with France's e-invoicing reform and the Peppol network.
Built on Saloon PHP.
Installation
Requires PHP ^8.2.
composer require bluerock/superpdp-php-client
Authentication
Super PDP uses OAuth2. Two authentication modes are available:
Mode 1 — Simple bearer token
If you already have a valid access token (e.g. from a Client Credentials flow or a pre-existing session):
use Bluerock\SuperPdp\SuperPdpConnector; $api = new SuperPdpConnector(accessToken: 'your-access-token'); // or with the factory helper: $api = SuperPdpConnector::withToken('your-access-token');
Mode 2 — Authorization Code flow with token refresh (OAuth 2.1)
Super PDP uses rotating refresh tokens (OAuth 2.1). On each refresh the old refresh token is invalidated and replaced with a new one. Your application is responsible for persisting the new refresh token after every refresh call.
Step 1 — Redirect the user
use Bluerock\SuperPdp\SuperPdpConnector; $connector = new SuperPdpConnector( clientId: 'your-client-id', clientSecret: 'your-client-secret', redirectUri: 'https://your-app.com/oauth/callback', ); $authorizationUrl = $connector->getAuthorizationUrl(); $state = $connector->getState(); // Store $state in the session, then redirect the user to $authorizationUrl.
Step 2 — Handle the callback
// Validate the state from the session, then: $authenticator = $connector->getAccessToken( code: $request->get('code'), state: $request->get('state'), expectedState: $session->get('oauth_state'), ); // $authenticator is a Saloon AccessTokenAuthenticator. // Persist the refresh token — this is your responsibility: $myStorage->save([ 'access_token' => $authenticator->getAccessToken(), 'refresh_token' => $authenticator->getRefreshToken(), 'expires_at' => $authenticator->getExpiresAt()?->getTimestamp(), ]);
Step 3 — Authenticate subsequent requests
use Saloon\Http\Auth\AccessTokenAuthenticator; $authenticator = new AccessTokenAuthenticator( accessToken: $myStorage->get('access_token'), refreshToken: $myStorage->get('refresh_token'), expiresAt: new DateTimeImmutable('@' . $myStorage->get('expires_at')), ); $connector = new SuperPdpConnector( clientId: 'your-client-id', clientSecret: 'your-client-secret', redirectUri: 'https://your-app.com/oauth/callback', ); $connector->authenticate($authenticator);
Step 4 — Refresh when expired
Refresh tokens rotate on every use — always persist the new one immediately:
if ($authenticator->hasExpired()) { $authenticator = $connector->refreshAccessToken($authenticator); // Persist the NEW tokens right away (old refresh token is now invalid): $myStorage->save([ 'access_token' => $authenticator->getAccessToken(), 'refresh_token' => $authenticator->getRefreshToken(), 'expires_at' => $authenticator->getExpiresAt()?->getTimestamp(), ]); $connector->authenticate($authenticator); }
Using the League OAuth2 Provider
If your application uses league/oauth2-client directly (e.g. alongside another framework or custom OAuth middleware), you can use the bundled SuperPdpProvider:
use Bluerock\SuperPdp\OAuth\SuperPdpProvider; $provider = new SuperPdpProvider([ 'clientId' => 'your-client-id', 'clientSecret' => 'your-client-secret', 'redirectUri' => 'https://your-app.com/oauth/callback', // optional: 'baseUrl' => 'https://api.superpdp.tech', ]); // Build the authorization redirect: $authUrl = $provider->getAuthorizationUrl(); $state = $provider->getState(); // Store in session for CSRF check // Exchange the authorization code: $token = $provider->getAccessToken('authorization_code', ['code' => $code]); $accessToken = $token->getToken(); $refreshToken = $token->getRefreshToken(); $expiresAt = $token->getExpires(); // Unix timestamp // Refresh: $newToken = $provider->getAccessToken('refresh_token', [ 'refresh_token' => $refreshToken, ]);
The SuperPdpProvider implements League\OAuth2\Client\Provider\AbstractProvider and its getResourceOwner() returns a SuperPdpResourceOwner with getId(), getNumber(), getFormalName(), and getEnv() accessors.
Usage
Resources
The connector exposes resource classes that group related API endpoints:
$api->company() // CompanyResource $api->session() // SessionResource $api->invoice() // InvoiceResource $api->invoiceEvent() // InvoiceEventResource $api->directoryEntry() // DirectoryEntryResource $api->frenchDirectory() // FrenchDirectoryResource
Each resource method returns a Saloon\Http\Response instance.
Companies
// Get the company associated with the current access token $response = $api->company()->me(); $company = $response->dtoOrFail(); // Bluerock\SuperPdp\Entities\Company
Sessions
// Check the onboarding/verification status of the current OAuth2 session $response = $api->session()->me(); $session = $response->dtoOrFail(); // Bluerock\SuperPdp\Entities\OauthSession echo $session->company_verification_status; // 'verified' | 'needs_review' | 'failed'
Invoices
// List invoices (cursor-based pagination) $response = $api->invoice()->index( direction: 'out', // 'in' | 'out' | null date: '2025-06', // YYYY, YYYY-MM, or YYYY-MM-DD order: 'desc', startingAfterId: 100, limit: 50, expand: ['en_invoice'], ); $invoices = $response->dtoOrFail(); // EntityCollection of InvoiceOverview // Get a single invoice $response = $api->invoice()->show(id: 42); $invoice = $response->dtoOrFail(); // Bluerock\SuperPdp\Entities\Invoice // Send an invoice (XML) $xmlContent = file_get_contents('invoice.xml'); $response = $api->invoice()->send( content: $xmlContent, contentType: 'application/xml', externalId: 'my-internal-ref', ); $invoice = $response->dtoOrFail(); // Send an invoice (PDF/Factur-X) $pdfContent = file_get_contents('invoice.pdf'); $response = $api->invoice()->send($pdfContent, contentType: 'application/pdf'); // Download raw invoice file $response = $api->invoice()->download(id: 42); $rawContent = $response->body(); // XML or PDF bytes // Generate a test invoice (sandbox only) $response = $api->invoice()->generateTest(format: 'ubl'); $xmlContent = $response->body(); // Convert between invoice formats $response = $api->invoice()->convert( content: $xmlContent, from: 'ubl', to: 'cii', ); // Validate one or more invoices $response = $api->invoice()->validate([ 'invoice.xml' => file_get_contents('invoice.xml'), ]); $report = $response->dtoOrFail(); // Bluerock\SuperPdp\Entities\ValidationReport
Cursor pagination
Super PDP uses cursor-based pagination. The list response contains has_after / has_before flags:
$startingAfterId = null; do { $response = $api->invoice()->index( startingAfterId: $startingAfterId, limit: 100, ); $body = $response->json(); $invoices = $response->dtoOrFail(); // EntityCollection foreach ($invoices as $invoice) { // process ... $startingAfterId = $invoice->id; } } while ($body['has_after']);
Invoice Events
// List all events (optionally filtered by invoice) $response = $api->invoiceEvent()->index( invoiceId: 42, startingAfterId: null, limit: 100, ); $events = $response->dtoOrFail(); // EntityCollection of InvoiceEvent // Create an invoice event (lifecycle status update) $response = $api->invoiceEvent()->store( invoiceId: 42, statusCode: 'fr:205', // Accepted ); $event = $response->dtoOrFail(); // Bluerock\SuperPdp\Entities\InvoiceEvent // Create a payment received event with details $response = $api->invoiceEvent()->store( invoiceId: 42, statusCode: 'fr:212', details: [ [ 'vat_rate' => '20.0', 'net_amount' => '1000.00', 'currency_code' => 'EUR', 'type_code' => 'MEN', ], ], );
Directory Entries
// List your company's directory entries $response = $api->directoryEntry()->index(); $entries = $response->dtoOrFail(); // EntityCollection of DirectoryEntry // Get a specific entry $response = $api->directoryEntry()->show(id: 5); $entry = $response->dtoOrFail(); // Bluerock\SuperPdp\Entities\DirectoryEntry // Create an entry (register an address on Peppol or PPF) $response = $api->directoryEntry()->store( directory: 'peppol', // 'peppol' | 'ppf' identifier: '0225:853322915', ); // Delete an entry $api->directoryEntry()->delete(id: 5);
French Directory
These endpoints are public (no authentication required) and allow you to look up companies registered in the French e-invoicing directory.
// Search companies by name $response = $api->frenchDirectory()->companies( formalNameStartsWith: 'Acme', postCodeStartsWith: '75', limit: 50, ); $companies = $response->dtoOrFail(); // EntityCollection of FrenchDirectoryCompany // Search by SIREN number $response = $api->frenchDirectory()->companies(number: '853322915'); // List electronic addresses for a company (by SIREN) $response = $api->frenchDirectory()->entries(number: '853322915'); $entries = $response->dtoOrFail(); // EntityCollection of FrenchDirectoryEntry foreach ($entries as $entry) { if ($entry->is_active) { echo $entry->identifier; // e.g. '0225:853322915' } }
Entities
Entities are typed Data Transfer Objects (DTOs). Each request returns a Saloon\Http\Response; call dtoOrFail() to get the entity.
| Entity | Description |
|---|---|
Company |
Your company profile |
OauthSession |
Current OAuth2 session and verification status |
Invoice |
Full invoice with events and en_invoice payload |
InvoiceOverview |
Lightweight invoice listing object |
InvoiceEvent |
A lifecycle event on an invoice |
DirectoryEntry |
A Peppol or PPF address entry |
FrenchDirectoryCompany |
A company from the French e-invoicing directory |
FrenchDirectoryEntry |
An active address from the French directory |
ValidationReport |
Result of invoice validation |
All entities can be created from arrays and exported:
$company = Company::fromArray($data); $arr = $company->toArray(); // all properties $arr = $company->toArray(filter: true); // non-null only
The en_invoice field on Invoice and InvoiceOverview is exposed as a raw array matching the EN 16931 structure described in the API spec.
Responses
All resource methods return a Saloon\Http\Response:
$response->ok(); // true for 2xx $response->failed(); // true for 4xx/5xx $response->status(); // HTTP status code $response->json(); // response body as array $response->body(); // raw string body $response->dto(); // entity or null $response->dtoOrFail(); // entity or throws on non-2xx
Testing
composer install ./vendor/bin/pest
Tests use Saloon's built-in MockClient to mock HTTP responses without making real network calls:
use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; $mockClient = new MockClient([ MockResponse::make(['id' => 1, ...], 200), ]); $connector = new SuperPdpConnector('test-token'); $connector->withMockClient($mockClient); $response = $connector->company()->me();