ums-lspl / photosign-verification-client
Laravel client for PhotoSign Verification — validate passport photos and handwritten signatures before storing them.
Package info
github.com/rajkumarchanda/ums-photosign-verification-client
pkg:composer/ums-lspl/photosign-verification-client
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.8
- illuminate/support: ^10.0|^11.0|^12.0
- psr/log: ^1.1|^2.0|^3.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
Laravel package for consumer applications (SmartExam / UMS) that validate passport photos and handwritten signatures via PhotoSign Verification.
Images are checked in memory on PhotoSign and never stored. Do not put OpenCV in Laravel.
Requirements
- PHP 8.2+
- Laravel 10, 11, or 12
Installation
From this monorepo (path)
In your consumer app's composer.json:
{
"repositories": [
{
"type": "path",
"url": "packages/ums/photosign-verification-client",
"options": { "symlink": true }
}
],
"require": {
"ums-lspl/photosign-verification-client": "@dev"
}
}
composer require ums-lspl/photosign-verification-client:@dev php artisan vendor:publish --tag=photosign-config
From Packagist / VCS (when published)
composer require ums-lspl/photosign-verification-client php artisan vendor:publish --tag=photosign-config
Configuration
.env on your consumer app:
PHOTOSIGN_URL=https://photosign.example.com PHOTOSIGN_KEY=ps_live_.... PHOTOSIGN_PHOTO_PROFILE=passport_student PHOTOSIGN_SIGN_PROFILE=signature_student PHOTOSIGN_TIMEOUT=8 PHOTOSIGN_RETRIES=1 PHOTOSIGN_LOCALE=en PHOTOSIGN_FAIL_MODE=shadow PHOTOSIGN_SHADOW=false PHOTOSIGN_WEBHOOK_SECRET=
Create an API key in PhotoSign → API keys (assign to the university client). Start with PHOTOSIGN_FAIL_MODE=shadow so enrolment is not blocked while you tune thresholds.
PHOTOSIGN_LOCALE (en or hi) sets student-facing issue copy. Per-call locale overrides it.
fail_mode:
| Mode | Behaviour |
|---|---|
closed |
Validation failures and outages block the upload |
shadow |
Call PhotoSign, log would-be rejects, never block the user |
open |
Never block; still call PhotoSign when possible |
PHOTOSIGN_SHADOW=true is an alias for shadow mode.
Usage
use PhotoSign\Facades\PhotoSign; use PhotoSign\Exceptions\ValidationFailedException; use PhotoSign\Exceptions\PhotoSignUnavailableException; try { $result = PhotoSign::validatePhoto($this->photo, [ 'profile' => config('photosign.photo_profile'), 'reference_id' => auth()->user()->STUDENTCODE, 'locale' => app()->getLocale(), // en | hi ]); // Soft tips when ok is true but lighting/contrast is imperfect: // foreach ($result->tipMessages() as $tip) { ... } } catch (ValidationFailedException $e) { $this->addError('photo', $e->getMessage()); foreach ($e->issues() as $issue) { // $issue->code, $issue->message, $issue->hint, $issue->severity } return; } catch (PhotoSignUnavailableException $e) { $this->addError('photo', 'Photo validation is temporarily unavailable.'); return; }
Hard rejects throw. Soft warnings / warning_issues do not throw — use $result->hasWarnings() or $result->tipMessages() when you want coaching tips after accept.
Livewire temporary uploads on S3/GCS are supported: the client prefers readStream() / get() when the SplFileInfo path is not a real local file. You can pass $this->photo directly — no app-side materialize helper is required.
Capture widget dataUrl values are also accepted by validatePhoto(). Decode for storage with:
PhotoSign::validatePhoto($dataUrl, [ 'reference_id' => auth()->user()->STUDENTCODE, ]); $decoded = PhotoSign::decodeDataUrl($dataUrl); Storage::disk('s3')->put("profile/{$code}.{$decoded['extension']}", $decoded['contents']);
PhotoSign::validateSignature($this->signature, [ 'reference_id' => auth()->user()->STUDENTCODE.'-sign', ]);
Batch and signature enrolment:
PhotoSign::enrollSignature($enrolmentSignature, $studentCode); PhotoSign::validateSignature($examSignature, ['reference_id' => $studentCode]); $batch = PhotoSign::validateBatch([ ['file' => $photo, 'kind' => 'photo'], ['file' => $signature, 'kind' => 'signature', 'reference_id' => $studentCode], ]);
Webhooks
HMAC header: X-PhotoSign-Signature: t=<unix>,v1=<hex> over {timestamp}.{rawBody}. Also sent as X-PhotoSign-Timestamp. Reject signatures older than ~5 minutes (replay protection). Secrets are shown once in PhotoSign Settings and encrypted at rest:
$raw = $request->getContent(); $ok = PhotoSign::verifyWebhookSignature( config('photosign.webhook_secret'), $raw, (string) $request->header('X-PhotoSign-Signature'), );
Live capture widget
Do not put API keys in the browser. Your server creates a short-lived session:
$session = PhotoSign::createCaptureSession([ 'kind' => 'photo', 'reference_id' => auth()->user()->STUDENTCODE, 'allowed_origin' => config('app.url'), 'locale' => app()->getLocale(), // appends ?locale=hi to embed_url ]);
<div id="photosign"></div> <script src="{{ $session['loader_url'] }}"></script> <script> PhotoSignWidget.mount("#photosign", { embedUrl: @json($session['embed_url']), locale: @json($session['locale'] ?? 'en'), onComplete({ items }) { const photo = items.find((item) => item.kind === "photo"); if (!photo?.result.ok) return; // Soft tips: photo.result.warning_issues // POST photo.dataUrl to your Laravel route, then store on S3 }, }); </script>
SmartExam integration point
Call PhotoSign in PhotoSignature::updatedPhoto() / updatedSignature() (and profile media flows) before uploading to S3:
- Validate mime/size locally (existing rules).
PhotoSign::validatePhoto(...)/validateSignature(...).- If
ValidationFailedException, show$e->getMessage()and do not upload. - Prefer shadow mode for the first rollout.
Testing
cd packages/ums/photosign-verification-client
composer install
./vendor/bin/phpunit
License
MIT