Symfony 7.4 & 8 · PHP 8.2+ · Acrobat Sign REST API v6
E-signatures, wired the Symfony way.
conceptlz/adobe-sign-symfony is a bundle that plugs the Acrobat Sign PHP SDK into
Symfony. You get an adobe_sign configuration tree fed by %env()%,
autowirable AdobeSign and AdobeSignManager services, OAuth routes so each
user can connect their own Acrobat Sign account, encrypted token storage in a cache pool or a
Doctrine table, a webhook endpoint that dispatches Symfony events and Messenger messages, and
bin/console adobe-sign:verify to check it all works.
composer require conceptlz/adobe-sign-symfony
What the bundle adds
The core package, conceptlz/adobe-sign, does all the Acrobat Sign work: uploading PDFs,
building agreements with a signing order, choosing a signature level, discovering which data centre
your account lives in, retrying throttled calls and parsing webhooks. It is framework-free on purpose.
It speaks only PSR interfaces, so it knows nothing about your service container, your cache pools,
your firewall or your Messenger buses.
This bundle connects those two worlds. It contains no Acrobat Sign API logic of its own: every request still goes through the core client. What it adds is the wiring a Symfony developer would otherwise write by hand, following the conventions you already use:
- Semantic configuration. One
adobe_signtree inconfig/packages/adobe_sign.yaml, validated by the Config component, with%env()%for secrets and the usual per-environment overrides. - Autowirable services. Type-hint
Conceptlz\AdobeSign\AdobeSignfor the default client, orConceptlz\AdobeSign\Symfony\AdobeSignManagerto choose one. Both come pre-built with your cache pool, your PSR-18 client and a logger on theadobe_signMonolog channel. - OAuth without the plumbing. Two importable routes run the authorization-code flow with a single-use state in the session, store tokens encrypted with libsodium, and dispatch an
OAuthConnectedEvent.$manager->forUser($user)then acts with that user's account and refreshes tokens as needed. - Token storage you choose. A persistent cache pool by default, a Doctrine DBAL table (picked up by
doctrine:migrations:diffwhen you use the ORM), or your ownTokenStoreservice. - A ready-made webhook endpoint. A stateless route that answers Adobe's verification handshake, rejects unknown client ids, drops retried duplicates, dispatches a
WebhookReceivedEventand, if you switch it on, anAdobeSignWebhookMessageon your Messenger bus. - Worker-safe. The manager is tagged
kernel.reset, so Messenger consumers and FrankenPHP worker mode start every message or request with fresh clients. - Console commands.
adobe-sign:verifyprints who your credentials belong to;adobe-sign:create-token-tablecreates the token table for projects without the ORM.
A first taste
Here is a contract going out from a controller. The injected client already knows your Integration Key, your cache pool and your project's default signature level, so the code only describes the business process: who signs, in which order, and which webhook tells you about it.
use Conceptlz\AdobeSign\AdobeSign;
use Conceptlz\AdobeSign\Builder\{AgreementInfo, FileInfo, ParticipantSetInfo, WebhookInfo};
use Conceptlz\AdobeSign\Webhook\EventType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\{JsonResponse, Request};
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class ContractController extends AbstractController
{
#[Route('/contracts', methods: ['POST'])]
public function send(Request $request, AdobeSign $adobeSign): JsonResponse
{
$pdf = $request->files->get('contract');
$documentId = $adobeSign->transientDocuments()->upload($pdf, 'msa.pdf', 'application/pdf');
$agreementId = $adobeSign->agreements()->create(
AgreementInfo::create('Master Services Agreement')
->file(FileInfo::transientDocument($documentId))
->participantSet(ParticipantSetInfo::signer('client@example.com')->order(1))
->participantSet(ParticipantSetInfo::signer('ceo@example.com')->order(2)),
);
$adobeSign->webhooks()->create(
WebhookInfo::forAgreement(
"Agreement $agreementId",
$this->generateUrl('adobe_sign_webhook', [], UrlGeneratorInterface::ABSOLUTE_URL),
$agreementId,
)->events(EventType::AGREEMENT_ALL),
);
return $this->json(['agreement_id' => $agreementId], 201);
}
}
When everyone has signed, Adobe calls the bundle's webhook route. The bundle acknowledges Adobe straight
away and puts an AdobeSignWebhookMessage on your bus. A worker picks it up and does the slow
work outside Adobe's 5-second window:
use Conceptlz\AdobeSign\Symfony\AdobeSignManager;
use Conceptlz\AdobeSign\Symfony\Message\AdobeSignWebhookMessage;
use Conceptlz\AdobeSign\Webhook\AgreementEvent;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class StoreSignedAgreement
{
public function __construct(private readonly AdobeSignManager $adobeSign) {}
public function __invoke(AdobeSignWebhookMessage $message): void
{
$event = $message->event();
if (!$event instanceof AgreementEvent || !$event->isWorkflowCompleted()) {
return;
}
$client = $this->adobeSign->client();
if (!$client->agreements()->get($event->agreementId())->isSigned()) {
return; // webhook payloads aren't signed: confirm with the API first
}
$pdf = $client->agreements()->combinedDocument($event->agreementId(), attachAuditReport: true);
file_put_contents("/var/contracts/{$event->agreementId()}.pdf", $pdf->detach());
}
}
The walkthrough builds the whole loop step by step, with a small storage service, idempotency and tests.
Features
- adobe_sign config treeIntegration Key or OAuth, signature level, qualified issuers, cache pool, PSR-18 client, retries, token storage and webhooks, all validated with defaults. See Configuration.
- Autowiring
AdobeSign(the default client, not shared) andAdobeSignManager(public, resettable) can be injected into controllers, services, handlers and commands. See Using the client. - OAuth connect routes
adobe_sign_oauth_redirectandadobe_sign_oauth_callback: single-use state in the session, a signed-in user required,OAuthConnectedEventon success. See OAuth. - Encrypted tokenslibsodium secretbox with a key derived from
kernel.secret(or your own key), in a cache pool, a Doctrine DBAL table or your own store. See Token storage. - Webhook endpoint
GET|POST /adobe-sign/webhook, stateless: client-id allow-list, the echo Adobe requires, 72-hour duplicate suppression and a typedWebhookReceivedEvent. See Webhooks. - MessengerOpt in and every new notification is also dispatched as an
AdobeSignWebhookMessage, a plain-array message any transport can carry. See Async with Messenger. - Signature levelsSet SES, AES or QES for the whole project, then check what each signer reached with
$manager->signatureShortfalls($id). See Signature levels. - TestablePoint
http_clientat a mock PSR-18 client in the test environment, post fake webhooks withWebTestCaseand assert on the in-memory transport. See Testing.
How it fits together
Your application talks to the autowired services. The bundle builds the core client from your configuration and from services Symfony already has, and it owns the HTTP entry points that Adobe and your users call back on. Everything that speaks to Acrobat Sign lives in the core.
The core package and its documentation
Installing this bundle pulls in conceptlz/adobe-sign. All three packages (core, Laravel
bridge, Symfony bundle) share one version number and are released together; the bundle requires the
core at ^0.5 and first shipped in 0.5.0. All builders
(AgreementInfo, ParticipantSetInfo, FileInfo,
WebhookInfo, Verification), resources (agreements(),
transientDocuments(), webhooks(), …), models and exceptions come from the core
and are documented in the core SDK documentation.
This site covers what is specific to Symfony and shows the core calls in a Symfony setting.
| You want to… | Read |
|---|---|
| Install, configure and verify credentials | Installation, Configuration |
| Inject the client, choose between clients, act as another user | Using the client |
| Let each user connect their own Acrobat Sign account | OAuth, Token storage |
| Receive signing progress in listeners or Messenger handlers | Webhooks, Async with Messenger |
| Learn every builder option, identity checks, downloads | Core: Creating agreements, Identity verification, Downloads |
| Understand SES, AES and QES in legal terms | Core: Signature levels |
| Use Laravel instead | Laravel bridge documentation |
Requirements
- PHP 8.2 or newer, with the
sodiumextension (bundled with PHP since 7.2; it encrypts stored OAuth tokens). - Symfony 7.4 or 8.x (
symfony/*^7.4 || ^8.0). CI runs Symfony 7.4 on PHP 8.2–8.4 and Symfony 8 on PHP 8.4–8.5. - A PSR-18 HTTP client. With
symfony/http-clientandnyholm/psr7installed, the framework's own one is used. - An Acrobat Sign account. A free developer account is enough to build and test; its agreements are marked as demo documents.
- Optional:
doctrine/dbalfor the token table,symfony/messengerfor async webhooks,symfony/security-bundlefor OAuth. A public HTTPS URL only if you receive webhooks.
Independent project
This bundle and the SDK it wraps are developed independently by Conceptualize and are not affiliated with or endorsed by Adobe. Adobe and Acrobat Sign are trademarks of Adobe Inc.