scanlyser / php-sdk
PHP SDK for the ScanLyser API — accessibility, SEO, performance, UX, and security scanning.
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- laravel/pint: ^1.0
- pestphp/pest: ^3.0
Suggests
- illuminate/support: Required for the Laravel service provider (^11.0|^12.0|^13.0)
README
Official PHP SDK for the ScanLyser API. Run accessibility, SEO, performance, UX, and security scans programmatically.
Requirements
- PHP 8.2+
- Guzzle 7.0+
Installation
composer require scanlyser/php-sdk
Quick Start
use ScanLyser\Client; use ScanLyser\Enums\ScanCategory; $client = new Client(apiKey: 'your-api-token'); // List your sites $sites = $client->sites($teamId)->list(); foreach ($sites->data as $site) { echo "{$site->name}: {$site->url}\n"; } // Trigger a scoped scan. Omit categories to assess all five public categories. $scan = $client->scans($teamId)->trigger( $siteId, wcagLevel: 'AA', categories: [ScanCategory::Accessibility, ScanCategory::SEO], ); // Wait for completion $scan = $client->scans($teamId)->awaitCompletion($scan->id); // Get issues $issues = $client->issues($teamId)->list($scan->id, severity: 'critical'); // Inspect scanner diagnostics independently from findings $diagnostics = $client->diagnostics($teamId)->list($scan->id);
API Reference
Client
$client = new Client( apiKey: 'your-api-token', maxRetries: 3, // optional, retries on 429 );
Teams
$teams = $client->teams()->list(); $team = $client->teams()->get($teamId);
Sites
$sites = $client->sites($teamId)->list(perPage: 15); $site = $client->sites($teamId)->create(name: 'My Site', url: 'https://example.com'); $site = $client->sites($teamId)->get($siteId); $client->sites($teamId)->delete($siteId);
Scans
use ScanLyser\Enums\ScanCategory; $scans = $client->scans($teamId)->list($siteId); $scan = $client->scans($teamId)->trigger( $siteId, wcagLevel: 'AA', webhookUrl: 'https://example.com/webhooks/scanlyser', categories: [ScanCategory::SEO, ScanCategory::Performance], ); $scan = $client->scans($teamId)->get($scanId); // Poll until complete (default: 600s timeout, 10s interval) $scan = $client->scans($teamId)->awaitCompletion( $scanId, timeoutSeconds: 600, pollIntervalSeconds: 10, ); if ($scan->hasUsableScore()) { echo "Score: {$scan->scores->overall}\n"; } else { echo "Outcome: {$scan->assessmentOutcome?->value}\n"; } if ($scan->hasUsableCategoryScore(ScanCategory::SEO)) { echo "SEO: {$scan->scores->seo}\n"; } else { $seoOutcome = $scan->categoryCoverage?->for(ScanCategory::SEO)->outcome->value ?? 'unavailable'; echo "SEO: {$seoOutcome}\n"; }
Polling stops for completed, failed, and cancelled scans. A completed lifecycle does not by itself guarantee a score:
inspect assessmentOutcome, coverage, or use hasUsableScore() before presenting score data.
When a scan fails, failure contains only the stable code, customer-safe message, and support correlationId.
The optional categories argument is last in the trigger signature, preserving existing positional calls. Omitting it
requests all public categories. Category score members (wcag, seo, performance, ux, sitewide, and other)
are nullable: null means that bucket was not scored, never zero. Use requestedCategories, categoryCoverage,
scoredCategoryScope, and hasUsableCategoryScore() to distinguish assessed, partial, inconclusive, and not-scanned
categories. IssueCategory remains the separate six-value finding/report vocabulary and must not be used to select a
scan scope.
Pages
$pages = $client->pages($teamId)->list($scanId); $page = $client->pages($teamId)->get($scanId, $pageId);
Detailed pages keep issues and diagnostics as separate collections. failure uses the same safe lifecycle-failure
object as scans; it never exposes raw exception text or a query-bearing page URL.
Issues
$issues = $client->issues($teamId)->list($scanId); $issues = $client->issues($teamId)->list($scanId, category: 'wcag', severity: 'critical');
Every issue response is a readonly Finding (which extends the backwards-compatible Issue class) and carries a
required FindingResultEnvelope in $issue->result. Issue hydration rejects diagnostic envelopes instead of presenting
them as findings. Its nested data objects retain versioned check identity, explicitly nullable qualification, safe
reasoning and limitations, structured evidence, reproduction context, remediation parameters, and references.
use ScanLyser\Enums\ResultOutcome; $result = $issues->data[0]->result; if ($result->outcome === ResultOutcome::ManualReview) { echo $result->explanation->reasoning; }
Hydration rejects unsupported schema versions and invalid kind/outcome combinations. A null qualification property means the scanner explicitly declined to claim it; do not infer a value from the issue source.
Diagnostics
$diagnostics = $client->diagnostics($teamId)->list($scanId, perPage: 50, page: 2); foreach ($diagnostics->data as $diagnostic) { echo "{$diagnostic->code}: {$diagnostic->detail->message} ({$diagnostic->correlationId})\n"; }
Diagnostic is a separate readonly resource with kind === ResultKind::Diagnostic, an inconclusive or error outcome,
string index, check and scope identity, non-null scope link, safe detail, optional recovery action, and support
correlation ID. The page argument selects the API page. Diagnostics never contribute to issue counts.
Reports
$report = $client->reports($teamId)->json($scanId); $client->reports($teamId)->pdf($scanId, saveTo: '/path/to/report.pdf');
Webhook Verification
Verify webhook signatures from scan completion callbacks:
use ScanLyser\Webhooks\WebhookSignature; $isValid = WebhookSignature::verify( payload: $request->getContent(), signature: $request->header('X-Signature'), secret: $tokenHash, );
Error Handling
The SDK throws typed exceptions for API errors:
use ScanLyser\Exceptions\AuthenticationException; use ScanLyser\Exceptions\ForbiddenException; use ScanLyser\Exceptions\NotFoundException; use ScanLyser\Exceptions\RateLimitException; use ScanLyser\Exceptions\ValidationException; try { $site = $client->sites($teamId)->get('nonexistent'); } catch (NotFoundException $exception) { // 404 } catch (ValidationException $exception) { // 422 - $exception->errors contains field-level errors } catch (RateLimitException $exception) { // 429 - automatic retries exhausted }
Rate-limited requests (429) are automatically retried up to 3 times with the Retry-After delay.
Laravel Integration
The SDK includes an optional service provider with auto-discovery.
Publish the config:
php artisan vendor:publish --tag=scanlyser-config
Configure .env:
SCANLYSER_TOKEN=your-api-token SCANLYSER_TEAM=your-team-id
Usage:
use ScanLyser\Client; class ScanController extends Controller { public function trigger(Client $client): void { $scan = $client->scans(config('scanlyser.team_id')) ->trigger($siteId, wcagLevel: 'AA'); } }
License
MIT