shureban/laravel-sumsub-sdk
Laravel SDK that implements Sumsub API routes for identity verification workflows. Install via Composer, register the service provider, and publish the config to swap interfaces for concrete implementations.
Install and wire it up
composer require shureban/laravel-sumsub-sdk
php artisan vendor:publish --provider="Shureban\LaravelSumsubSdk\SumsubServiceProvider" --force
php artisan vendor:publish --provider="Shureban\LaravelObjectMapper\ObjectMapperServiceProvider" --force
The README tells you to register Shureban\LaravelSumsubSdk\SumsubServiceProvider::class in config/app.php. On Laravel 5.5+ package auto-discovery usually handles this — check bootstrap/cache/packages.php before adding it manually, and don't register it twice.
Where to look first, in order
config/sumsub.php (after publishing). This is the only place credentials and bindings live. Expect: app token, secret key, base URL / sandbox toggle, and the interface→class bindings the package advertises. Set them via env() in .env, never hard-code.src/ for the DTO/model layer — Info, CompanyInfo, ReviewResult, Questionnaire, plus the document-type, review-status and reject-label enums. These classes are the documentation; the README's ## Usage section is empty, so the DTOs and the client class are your API reference.First use case to build end-to-end
Applicant creation + WebSDK token issuance + webhook receipt. That single vertical slice exercises config, HTTP auth, response mapping, and signature verification, and it's the one every KYC integration needs on day one:
levelName and persist the returned applicantId on your users table (indexed, unique).kyc_status column.Treat the SDK as a repository layer, not a facade sprinkled through controllers
Wrap it in your own KycService / VerificationGateway interface bound in a service provider. The package's config already lets you swap interface→class implementations, but a domain-level wrapper is what lets you upgrade (or drop) the SDK without touching controllers. The underlying call is plain REST against Sumsub, so a fallback implementation is cheap.
Applicant lifecycle as a state machine
Model it explicitly: not_started → applicant_created → pending → (approved | rejected | resubmitted). Drive transitions only from webhook events plus a periodic reconciliation poll (GET applicant status) for events you missed. Sumsub is at-least-once on webhooks, and your own create call can race the first event — do not assume ordering.
Webhook controller: verify, persist, dispatch, 200
public function __invoke(Request $request)
{
$raw = $request->getContent(); // NOT $request->all()
$digest = $request->header('X-Payload-Digest');
$expected = hash_hmac('sha256', $raw, config('sumsub.secret_key'));
abort_unless(is_string($digest) && hash_equals($expected, $digest), 401);
KycWebhook::create([
'type' => $request->json('type'),
'applicant_id' => $request->json('applicantId'),
'payload' => $raw,
'received_at' => now(),
]);
ProcessKycWebhook::dispatch($raw)->onQueue('kyc');
return response()->noContent();
}
Verify with hash_equals, not ===. Keep the raw body — store it verbatim so you can replay/reduce an incident later without asking Sumsub for a resend.
Queue all outbound API calls
Applicant creation, document fetches, and status checks belong in jobs with a timeout and retry policy, not in an HTTP request a user is waiting on. Sumsub calls are network-bound and rate-limited; a slow one shouldn't take down your signup path.
Idempotency on your side
Add a unique index on (applicant_id, type, correlation_id) where available, or on a hash of the raw payload. Duplicate applicantReviewed events are normal, and the second one should be a no-op, not a second email or a second ledger entry.
Multi-jurisdiction / multi-tenant
Recent releases added per-jurisdiction API keys and a separate secret key. If you operate in several regions, don't put one client in the container as a singleton — resolve the client per tenant/jurisdiction at call time from a small factory, and key your idempotency records by the same tenant key.
Testing
Use Http::fake() with payloads captured from Sumsub's sandbox and assert against the package's DTOs. The published DTO classes double as excellent webhook fixtures — constructing a ReviewResult in a test is faster and more accurate than hand-writing JSON. Swap the gateway binding in TestCase so feature tests never hit the network.
src/ and the config file before estimating any work. Do not infer method names from Sumsub's REST paths; they won't match.laravel-object-mapper does the JSON→object hydration. Publishing only the Sumsub config leaves you with mapping that fails on nested fixedInfo, companyInfo, or questionnaire structures. Publish both, and re-publish after upgrading.config:cache is a trap. Credentials and bindings are read from config at resolution time. Change .env → rerun php artisan config:cache, or you'll debug a "wrong token" for twenty minutes.$request->getContent(). Re-encoding $request->all() with json_encode reorders keys and strips whitespace, and the HMAC will never match. Also confirm no middleware is rewriting the request body, and that your webhook route is exempt from CSRF (api middleware group, not web).v0.0.33 "remove reject labels" release in the history. A pinned enum class can throw when Sumsub sends a reject label or document subtype you don't have a case for. Never let an unknown label crash the webhook worker — catch the mapping exception, store the raw string, and alert.levelName doesn't exist in the target environment. Keep the level name in config with separate values per environment.applicant reset is destructive. It wipes submitted data and is not reversible. If you expose it, put it behind an explicit admin action with an audit record, never on a retry path.^2.3 and upgrade deliberately with the full compare diff read, not just the release headline.composer update stays safe.How can I help you explore Laravel packages today?