yousign/yousign-api-client
A PHP client for the YouSign electronic signature API that simplifies creating, sending, and managing signature requests, documents, and workflows from Laravel or plain PHP applications with a clean, fluent interface.
composer require yousign/yousign-api-client
Add credentials to .env — sandbox first, never production on day one:
YOUSIGN_API_KEY=...
YOUSIGN_BASE_URL=https://api-sandbox.yousign.app/v3
Wire it into config/services.php so nothing hardcodes URLs:
'yousign' => [
'api_key' => env('YOUSIGN_API_KEY'),
'base_url' => env('YOUSIGN_BASE_URL', 'https://api-sandbox.yousign.app/v3'),
],
The client is generated from Yousign's v3 OpenAPI spec, so the shape you get is: a Configuration object (host + bearer key), then one API class per resource (SignatureRequestApi, DocumentApi, SignerApi, …), plus model/DTO classes for request bodies. Confirm the exact namespaces against vendor/yousign/yousign-api-client/src before writing your first class — the generated code is the real documentation.
Register it once in a service provider and never new it in a controller:
$this->app->singleton(\Yousign\Client\Configuration::class, fn () => \Yousign\Client\Configuration::getDefaultConfiguration()
->setHost(config('services.yousign.base_url'))
->setApiKey('bearerAuth', config('services.yousign.api_key')));
First use case: a signature request for a single PDF. The v3 flow is always the same four calls — create the signature request (draft), upload a document, add a signer, then activate it. Nothing is sent to anyone until activation. Build a throwaway artisan command or tinker session that walks those four steps against sandbox, prints the activation response, and you'll understand 80% of the package.
Wrap it, don't sprinkle it. Put the generated client behind your own App\Services\Yousign\YousignGateway interface with intent-named methods (requestSignature(), cancel(), downloadSignedDocument()). Reasons: the generated classes are verbose, they change on spec bumps, and an interface lets you fake the whole thing in tests without hitting the network.
Model the state machine locally. Yousign signature requests move draft → ongoing → done | declined | expired | canceled. Keep a signature_requests table with yousign_id, status, and your own domain status. Treat Yousign as the source of truth but store enough locally to render a UI without an API call per page load.
Prefer webhooks over polling. Configure the webhook URL in the Yousign dashboard, then in a controller:
Route::post('/webhooks/yousign', YousignWebhookController::class)
->withoutMiddleware(['csrf']);
Verify the signature header against your webhook secret, return 200 fast, and dispatch a job that re-fetches the signature request by ID before mutating anything. Re-fetching is the important bit — it keeps your DB in sync even if a webhook is missed or you replay an old one.
Queue the writes. Document uploads and activations are network-bound and can take seconds. CreateSignatureRequestJob, ActivateSignatureRequestJob — retry 3 times with backoff, and make each job idempotent by checking local state before calling Yousign.
Documents come from storage, not the request body. If a user just uploaded a PDF, keep it on disk (or S3) and have the job read it there. Streaming large files through a queued payload is how you get 20 MB jobs serialized into the queue table.
Config-driven signature level. electronic_signature (simple), advanced_electronic_signature, qualified_electronic_signature are per-signer settings with different legal weight and cost. Put the default in config, let business rules override it per document type.
Sandbox and production in parallel. Because the base URL lives in config, you can run a yousign:sync command against sandbox in CI and against production in a scheduled job without code forks. Just make sure the API keys are scoped to a single environment — they are not interchangeable.
Authorization header, and the SDK expects the raw key — don't prefix it with Bearer yourself if setApiKey does it for you. Double-prefixing yields a 401 that looks like a bad key.getCode() (HTTP status) and a decoded body. Check 422 (validation — read the body, it names the bad field), 429 (throttle — back off, don't retry immediately), and 401 (key or wrong base URL; sandbox keys against the prod host fail here).done. Verify, and also re-fetch by ID as described above — belt and braces.after + limit in v3), not page numbers. Loops written as for ($page = 1; ...) won't terminate the way you expect — walk the cursor until it comes back empty.ongoing you cannot edit documents or signers — you cancel and start over. Validate everything (signer email, phone, signature level, document ordering) before the activate call, ideally with a dry-run path.signer_order / sequential mode is a property of the signature request, not per signer. Get it wrong and the second signer is blocked forever waiting on a step you didn't configure.composer update the client in isolation. Regenerating from a new spec can rename models or change constructor signatures — it's a breaking change disguised as a patch bump. Pin it, diff src/ on upgrade.Http::fake() won't intercept this client if it has its own Guzzle instance. Fake at your YousignGateway interface boundary, and keep a single opt-in integration test that runs against sandbox in CI with a YOUSIGN_SANDBOX_KEY secret.DocumentApi and its hash is what you want in an audit trail. Storage bills and stale-file bugs both come from mirroring files you don't need.How can I help you explore Laravel packages today?