filecheck / filecheck-php
Filecheck server SDK for PHP — jobs, uploads, webhooks, and server-side job verification
Requires
- php: >=8.1
- ext-json: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5
Suggests
- guzzlehttp/guzzle: A PSR-18 HTTP client implementation
- php-http/discovery: Auto-discovers an installed PSR-18 HTTP client instead of the bundled cURL transport
README
Filecheck server SDK for PHP 8.1+ — jobs, uploads, webhooks, and the server-side job verification
your fulfillment path must run before trusting a browser-submitted job id. Mirrors
filecheck-node
method-for-method.
HTTP via any PSR-18 client (auto-discovered with php-http/discovery, or injected), with a
bundled cURL fallback so the package works with zero extra dependencies.
Install
composer require filecheck/filecheck-php
Quickstart — verify a job in 5 lines
$fc = new \Filecheck\FilecheckClient(getenv('FILECHECK_SECRET_KEY')); $result = $fc->jobs->verify($_POST['filecheck_job_id'], ['workflow_id' => 'wf_…']); if (!$result->ok) { http_response_code(422); exit("Files not accepted ({$result->reason})"); } // fulfil the order; $result->state is 'ready' or 'partial' (warnings accepted by policy)
Laravel
// AppServiceProvider $this->app->singleton(FilecheckClient::class, fn () => new FilecheckClient(config('services.filecheck.secret'))); // CheckoutController public function store(Request $request, FilecheckClient $fc) { $result = $fc->jobs->verify($request->input('filecheck_job_id'), ['workflow_id' => 'wf_…']); abort_unless($result->ok, 422, "Files not accepted ({$result->reason})"); // … }
verify() is the encoded docs checklist: the job must be terminal
(status ∈ done|skipped|error), proceedable (the browser Element's canProceed equivalent —
ready/partial after applying the job's onFail policy), and — with workflow_id — must have
run the expected workflow. Options: policy (override the resolved onFail), strict (fail
closed on status: 'error' jobs).
Uploading + processing files
// Two-leg upload (presign + S3) in one call → fileRef $upload = $fc->uploads->create('/tmp/artwork.pdf', ['mime_type' => 'application/pdf']); // Validate against PDF/X profiles — waits for the result by default $res = $fc->jobs->validate(['sources' => [['fileRef' => $upload->fileRef, 'profile' => ['1b']]]]); echo $res->job->outcome; // Preflight + autofix — async by default; 'wait' => true to block until terminal $fixed = $fc->jobs->fix([ 'sources' => [['fileRef' => $upload->fileRef, 'profileId' => 'default']], 'wait' => true, ]);
The wait option
The raw API inconsistently flips sync/async with sync: true (create/preflight/previews/fix —
async default) and async: true (validate/optimize — sync default). The SDK normalizes all six
behind 'wait' / 'wait_timeout' (seconds, default 120) with defaults matching each endpoint.
When the server's ~27 s wait budget expires (HTTP 202), the SDK keeps polling GET /jobs/{id}
client-side. Every submit returns a JobResult { job, pending }.
Blocking waits tie up the PHP worker — mind FPM time limits; prefer webhooks for long jobs.
API surface
| Method | Endpoint |
|---|---|
$fc->jobs->create($params) |
POST /jobs |
$fc->jobs->preflight/previews/fix/validate/optimize($params) |
POST /jobs/… sugar endpoints |
$fc->jobs->retrieve($id) |
GET /jobs/{id} → Data\Job |
$fc->jobs->retrieveRuns($id) |
GET /jobs/{id}?expand=runs → Data\JobRuns |
$fc->jobs->list(['limit', 'next_key']) / ->iterate() |
GET /jobs (+ auto-pagination) |
$fc->jobs->delete($id) |
DELETE /jobs/{id} |
$fc->jobs->waitUntilTerminal($id, $opts) |
polling helper |
$fc->jobs->verify($id, $opts) |
fulfillment gate → Data\VerifyResult |
$fc->uploads->create($pathOrBytesOrStream, $opts) |
POST /uploads + S3 leg → Data\Upload |
$fc->orders->create($orderId, $params) |
POST /orders/{id} |
$fc->workflows/connectors/rules/profiles/optimizePresets->all()/->get($id) |
read-only library |
Webhooks::constructEvent($rawBody, $sig, $secret, $opts) |
webhook parsing/verification |
Client options: new FilecheckClient('sk_…', ['base_url', 'timeout' /* seconds */, 'max_retries', 'http_client' /* PSR-18 */, 'transport']). Secret keys are sk_…; passing a
publishable pk_… key throws immediately. Keys are never echoed in full — masked to sk_…abc4.
Responses are readonly value objects (Data\Job, Data\Task, Data\Step, Data\JobRuns,
Data\VerifyResult, …) that keep the raw payload in ->raw for forward compatibility.
Webhooks
Signing status: Filecheck's per-job
job.completedwebhooks are delivered unsigned today; the signature scheme is not finalized.constructEventis structured so verification becomes the default without breaking changes once it ships. Until then,['verify' => false]is the explicit, temporary escape hatch.
$event = \Filecheck\Webhook\Webhooks::constructEvent( file_get_contents('php://input'), // ALWAYS the raw body $_SERVER['HTTP_X_FILECHECK_SIGNATURE'] ?? null, null, ['verify' => false], // TEMPORARY until Filecheck ships webhook signing ); if ($event->type === \Filecheck\Data\WebhookEvent::TYPE_JOB_COMPLETED) { // $event->payload: id, status, outcome, taskIds, tasks[], finalized, … }
In Laravel, exempt the route from CSRF and use $request->getContent() for the raw body.
Verification uses hash_equals() (constant-time compare).
Errors & retries
Typed exceptions in Filecheck\Exception: AuthenticationException (401/403 — including API
Gateway's bare {"message":"Forbidden"} shape), InvalidRequestException (400),
NotFoundException (404), RateLimitException (429), ApiException (5xx / non-JSON bodies),
ConnectionException (network/timeout), WebhookSignatureException. The API has no
machine-readable error codes — branch on exception class, not message text.
Retries: idempotent GETs only (default 2×, full-jitter backoff, Retry-After honored) on
429/502/503/504 and connection failures. POSTs are never auto-retried — the API has no
idempotency keys, and a duplicated POST /jobs creates and bills a second job.
Development
composer install composer test # PHPUnit composer phpstan # level 8
Test fixtures in fixtures/ are a synced copy of the shared fixtures in the
filecheck-integrations monorepo, so this SDK and filecheck assert against identical
payloads.
License
MIT