ets/echosign-bundle
ETSEchoSignBundle is a Symfony wrapper for Adobe EchoSign APIs, offering services to send, inspect, and remove documents. Install via Composer, configure API credentials and recipients, then use the bundled client services.
First, calibrate expectations: this is not a Laravel package. ets/echosign-bundle is a Symfony 2 bundle that wraps the Adobe EchoSign SOAP API (EchoSignDocumentService16). It ships an ETSEchoSignBundle class meant for AppKernel::registerBundles() and config.yml — there is no service provider, no facade, and no config/*.php publishable config. Your first task is deciding whether to (a) run it inside a Symfony app that already exists, or (b) harvest its service/model classes into a small Laravel adapter that you own.
Where to look first, in order:
src/ETS/EchoSignBundle/Resources/config/services.xml — this is the real API surface. It tells you the service IDs (typically ets_echo_sign.api or similar) and which constructor arguments (api key, gateway, wsdl) get injected.SoapClient wrapper with methods like sendDocument, getSigningUrl, getDocumentInfo, getDocumentsForUser, sendReminder, cancelDocument.DocumentCreationInfo / FileInfo / RecipientInfo model classes in the same namespace — these map one-to-one onto the SOAP request payload.Tests/ — for a package this small, the tests are usually the only real documentation of the expected call shape.composer.json — check the Symfony/PHP constraints before you commit. Assume dev-master only, no tags.Minimal bring-up in Laravel (since the bundle won't install cleanly into a Laravel app):
composer require ets/echosign-bundle:dev-master
// config/services.php
'echosign' => [
'key' => env('ECHOSIGN_API_KEY'),
'gateway' => env('ECHOSIGN_GATEWAY'), // shard-specific, see Gotchas
'wsdl' => env('ECHOSIGN_WSDL'),
],
// app/Providers/EchoSignServiceProvider.php
$this->app->singleton(EchoSignClient::class, fn ($app) => (new EchoSignClient())->configure(
config('services.echosign.key'),
config('services.echosign.gateway'),
config('services.echosign.wsdl'),
));
First use case to build: send a PDF for signature, persist the returned documentKey, then request a getSigningUrl for a specific recipient and redirect the user there. Once that round-trip works against a sandbox account, everything else (widgets, status sync, reminders) is variation on the same three calls.
Wrap, don't sprinkle. The single highest-leverage pattern is a DocumentService / SignatureProvider interface in your app that owns the EchoSign client, so controllers and jobs never touch the bundle's classes directly:
interface SignatureProvider
{
public function send(Envelope $envelope): string; // returns documentKey
public function signingUrl(string $key, string $email): string;
public function status(string $key): DocumentStatus;
}
This is what makes the package swappable later (Adobe Sign REST API, DocuSign, or a fake in tests).
Sending flow. Build a DocumentCreationInfo with name, message, recipients (signers vs approvers vs CCs are separate arrays and the order of signers defines signing order), and a FileInfo — either DOCUMENT (base64 PDF inline) or URL (a signed S3/public URL EchoSign fetches itself). Strongly prefer URL for anything non-trivial; base64 bloats the SOAP envelope and hits memory/timeout limits fast. Then:
$key = $client->sendDocument($creationInfo); // -> documentKey
Document::create(['key' => $key, 'status' => 'OUT_FOR_SIGNATURE', ...]);
Embedded signing. Two variants exist: createWidget + getWidgetSigningUrl (reusable, good for a form embedded in your UI) or getSigningUrl on an existing document (per-recipient, one-shot). Render the returned URL in an iframe. Because these URLs are short-lived/single-use, request them at render time inside the controller — never cache them in your DB or a response cache.
Callback reconciliation (the pattern that actually keeps your DB honest). EchoSign POSTs documentKey + status to a publicly reachable callback URL. Do not trust the payload: on receipt, immediately enqueue a job that calls getDocumentInfo($key) and updates your record from the authoritative response.
Route::post('/webhooks/echosign', function (Request $request) {
SyncDocumentJob::dispatch($request->input('documentKey'));
return response('', 200); // must be 200, must be fast
})->withoutMiddleware([VerifyCsrfToken::class]);
Belt-and-braces polling. EchoSign retries callbacks only a handful of times and gives up. A scheduled command that pulls getDocumentsForUser over a recent date window (or re-checks every non-terminal document hourly) is standard practice, not paranoia.
Queue the writes. Every outbound call is network I/O against an external SOAP endpoint. Dispatch sends and reminders as queued jobs with $tries = 3 and backoff; make the send idempotent by keying on your own envelope UUID rather than the documentKey (which you don't have until the call succeeds).
Credential hygiene. EchoSign uses a bare API key, no OAuth. Keep separate keys for sandbox and production, keep them in env() only, and log request metadata (documentKey, status, fault code) — never the full SOAP envelope in production, since the key travels inside it.
dev-master only, no releases. Pin a specific commit (dev-master#abc1234) in composer.json; better still, fork the handful of classes you need into app/EchoSign/ so a broken upstream can't break you. Budget for owning this code.secure.echosign.com, secure.na1.echosign.com, secure.eu1.adobesign.com are not interchangeable — gateway and WSDL must both point at the shard your API key was issued on. Mismatches produce terse faults that read like auth errors.ext-soap (often not enabled by default on shared/PaaS hosts). The SoapClient will try to fetch and cache the WSDL on first use — set cache_wsdl => WSDL_CACHE_DISK and, for CI or flaky networks, vendor a local WSDL copy. Also set connection_timeout explicitly; the default can hang a queue worker indefinitely.SoapFault (or the generated EchoSignDocumentService16Exception), and the messages are frequently a code plus nothing useful. Develop with trace => true and dump __getLastRequest() / __getLastResponse() locally. Persist the fault code and a truncated message for production debugging.200 from the controller after dispatching the sync job, not after doing the work inline.OUT_FOR_SIGNATURE, SIGNED, APPROVED, DELIVERED, FORM_FILLED, RECALLED, CANCELLED, EXPIRED. SIGNED is not "done" if you have approvers or countersigners — model the terminal states explicitly and only stop polling on CANCELLED/EXPIRED/RECALLED/final SIGNED.getSigningUrl / getWidgetSigningUrl results are time-boxed and sometimes single-use. A user who refreshes the iframe gets a blank page. Re-fetch per render.sendReminder errors if the document isn't currently out for signature. Don't wire it to a button that can be mashed.vendor/ets/echosign-bundle as read-only reference material and copy what you need.SoapClient construction inside the main client class is your single seam — inject a mockable client there rather than trying to stub the bundle's services through Symfony DI from a Laravel container. One integration test against the sandbox, gated behind an env flag, plus unit tests against a faked SignatureProvider, is the right test pyramid for this.How can I help you explore Laravel packages today?