bluerock/superpdp-php-client

The Official Super PDP API PHP Client/SDK

Maintainers

Package info

github.com/bluerocktel/SuperPDP-PHP-Api-Client

pkg:composer/bluerock/superpdp-php-client

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-05-26 08:31 UTC

This package is auto-updated.

Last update: 2026-05-26 08:34:12 UTC


README

Software License Latest Version on Packagist Total Downloads

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();