Help
FAQ
The questions Symfony developers ask most, answered in a sentence first and in detail after, with a link to the page that covers each topic in depth. Every answer can be linked to directly.
Installation & versions
Which Symfony and PHP versions are supported?
Symfony 7.4 and 8.x, on PHP 8.2 or newer.
The bundle requires 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. Symfony 8 itself needs PHP 8.4. Older Symfony versions can use the framework-agnostic core directly. See Requirements.
Do I need to install conceptlz/adobe-sign separately?
No. Requiring the bundle installs the core at a compatible version.
You use the core's builders, resources and exceptions directly in your code (Conceptlz\AdobeSign\…), so importing from it is expected; you just don't need a second composer require.
How do the packages' versions relate?
The core, the Laravel bridge and the Symfony bundle share one version number and are released together.
The current release is 0.5.0, the first to include the Symfony bundle; it requires the core at ^0.5. Before 1.0 a minor bump may break things, so update the bundle and the core together and read the changelog. See Upgrading.
Is this an official Adobe package?
No. It is developed independently by Conceptualize and is not affiliated with or endorsed by Adobe.
It talks to Adobe's public Acrobat Sign REST API v6 with your own account and credentials. Adobe and Acrobat Sign are trademarks of Adobe Inc.
Is there a Flex recipe?
Not yet. Flex still registers the bundle in config/bundles.php, because the package type is symfony-bundle.
You create config/packages/adobe_sign.yaml and config/routes/adobe_sign.yaml yourself; both are a few lines. See Install in six steps.
Configuration
What is the smallest working configuration?
One key: adobe_sign: { integration_key: '%env(ADOBE_SIGN_INTEGRATION_KEY)%' }.
That is enough to send agreements. Webhooks additionally need the route import and a client id; OAuth needs the oauth section, the routes and a firewall. See Configuration.
Can I put a list of webhook client ids in one environment variable?
No. Symfony doesn't accept one %env()% for a whole list node, so '%env(csv:IDS)%' fails the build.
Use one variable per item: client_ids: ['%env(ADOBE_SIGN_CLIENT_ID)%', '%env(ADOBE_SIGN_CLIENT_ID_STAGING)%']. Integer keys need a cast, for example '%env(int:ADOBE_SIGN_DEDUPE_TTL)%'. See Environment variables and secrets.
Can each tenant use its own Integration Key?
Yes, by building a core client per tenant instead of using the configured default.
AdobeSign::withIntegrationKey($tenant->key(), cache: $psr16, logger: $logger) with the bundle's adobe_sign.cache service gives you a client that shares the data-centre cache. Configuration is compiled, so it can't be switched per request. See Changing values at runtime.
Is the default cache.app pool good enough?
On one server, yes. On several servers, or for OAuth tokens, no.
cache.app is a filesystem pool on each machine. Duplicate detection and stored tokens then aren't shared and don't survive a fresh container. Use Redis or another shared pool for cache_pool, and Doctrine or a persistent pool for tokens. See cache configuration.
Auth & OAuth
Should I use an Integration Key or OAuth?
Integration Key if everything is sent from one account you control; OAuth if your users send from their own Acrobat Sign accounts.
The key is one secret and never expires. OAuth needs an API application, a redirect URI, token storage and a firewall, but agreements then live in each user's account. You can mix them: the key as the default client, forUser() for connected users. See Integration Key or OAuth?
How do I act as a user in a Messenger handler or command?
Call $manager->forUser($key) with the user's token key, which you stored when they sent the agreement.
There is no signed-in user in a worker, so client() in OAuth mode throws. Store $manager->userKey($user) on your record when sending. See OAuth in handlers.
My app is a stateless API with JWT auth. Can I use the OAuth routes?
Not as they are: they keep the OAuth state in the session, and the browser must carry the user's authentication through Adobe's redirect.
Either serve the two routes from a small session-based firewall the front end opens in a browser window, or build the flow with $manager->oauth() (authorizationUrl(), handleCallback()) and store tokens with $manager->tokens()->put(). See SessionNotFoundException.
Can a whole team share one connected account?
Yes: implement UserKeyResolver to return a team key and set oauth.user_key_resolver.
Every member then uses the same tokens. Restrict the connect routes to team admins, because the last person to connect replaces the team's tokens. See One connection per team.
How does a user disconnect?
Revoke the token at Adobe with $manager->oauth()->revoke($token), then forget() it in the store.
The bundle has no disconnect route, because what "disconnect" means (and which confirmation it needs) is app-specific. A ten-line controller is in Disconnecting.
Webhooks & Messenger
Why does Adobe get a redirect to my login page?
Because a firewall or access_control rule requires a user for /adobe-sign/webhook.
Give the path its own security: false firewall before main, or a PUBLIC_ACCESS rule above any catch-all. See Firewalls and access_control.
Should I use the event listener or a Messenger handler?
A Messenger handler for anything that calls Adobe, touches files or sends email; a listener only for tiny, local updates.
Listeners run inside Adobe's 5-second window and a failure there can't be retried. Handlers run in a worker with retries and a failure transport. See Why a message.
How do I receive webhooks on my laptop?
Through an HTTPS tunnel that forwards a public address to your local server.
Make Symfony generate the tunnel's URL (open the app through it, or set framework.router.default_uri), allow your client id, and delete webhooks pointing at old tunnel addresses afterwards. See Local development.
Can a notification get lost?
Rarely, and you should plan for it with a periodic status check.
If a listener or the bus fails, the bundle answers 500 without recording the notification, so Adobe's retry is processed normally (If a listener or the bus throws). The real risk is a long outage: Adobe gives up after 72 hours of failures and disables the webhook. A scheduled task that re-checks open agreements covers that.
Why does the Messenger message carry an array instead of the event object?
So that any transport and serializer can carry it, and SDK upgrades between dispatch and handling can't break it.
Call $message->event() in the handler to get the typed AgreementEvent back. See The message.
Can I trust the webhook payload?
Treat it as a hint. Adobe doesn't sign payloads; the client-id check is the only authenticity control.
Before acting on anything important, such as marking a contract signed or releasing goods, read the agreement's status through the API, as the walkthrough's handler does.
Token storage
Which token store should I use in production?
token_store: doctrine, if you use Doctrine.
Tokens then live next to your users, are backed up with them, and no cache clear can lose them. The cache store is fine with a durable, shared Redis pool. See Choosing a store.
Do I have to write a migration for the token table?
Not with Doctrine ORM: doctrine:migrations:diff includes it automatically.
The bundle adds the table to the ORM schema through a postGenerateSchema listener. With DBAL only, run adobe-sign:create-token-table. See Migrations.
What happens to connected users if I rotate APP_SECRET?
They all appear disconnected and must connect again, unless you set a dedicated oauth.encryption_key.
Tokens are encrypted with a key derived from kernel.secret by default; values from another key decrypt to nothing. See Rotating APP_SECRET.
Are tokens encrypted in the database?
Yes, with libsodium secretbox (encrypted and authenticated) in both shipped stores.
A database dump alone doesn't reveal them; the user key column is plain text. Your own store is encrypted only if it uses TokenEncryptor. See Encryption.
Signature levels
How do I require AES for every agreement?
Set signature_level: aes.
Every agreement built with AgreementInfo then asks each signer for a certificate-based digital signature, unless the agreement or the signer sets its own level. The account needs digital signatures enabled. See the project's default level.
How do I know a signature really is qualified?
List the certificate issuers you accept in qualified_issuers and check with $manager->signatureShortfalls($id).
The API request for QES is the same as for AES; only the issuer tells them apart. For a formal check, validate the PDF against the relevant trusted list. See qualified_issuers.
Which level does my contract legally need?
That is a question for your counsel; it depends on the document and the jurisdiction.
The core's signature levels page explains the EU's tiers and how other jurisdictions approach electronic signatures, as background for that conversation.
Testing & deployment
How do I test without calling Adobe?
Point adobe_sign.http_client at a mock PSR-18 client in the test environment and queue responses.
Everything above the HTTP layer, including the bundle's routes, runs for real. Keep the mock public and call disableReboot() in multi-request tests. See Setting up the mock client.
What changes when I run several web servers?
Share the cache pool and the token storage; nothing else.
Set cache_pool to a shared pool so duplicate deliveries are recognised on any server, and use token_store: doctrine (or a shared Redis pool) so every server and worker sees the same tokens. Use the same APP_SECRET or encryption_key everywhere. See Production on several servers.
Does it work with FrankenPHP worker mode or other long-running runtimes?
Yes. The manager is tagged kernel.reset, so its cached clients are dropped between requests.
What you must avoid is keeping a client yourself: a shared service that took AdobeSign in its constructor keeps it for the worker's life, and with auth: oauth it would even belong to the first request's user. Inject AdobeSignManager and call client() or forUser() per request. See Long-running workers.
Anything special on Platform.sh, Symfony Cloud or similar platforms?
Plan for an ephemeral, read-only application directory, and keep state in platform services.
On such platforms var/cache is rebuilt on every deploy and usually not shared between instances, so cache.app is the wrong home for tokens and duplicate detection: attach a Redis or database service and point cache_pool and the token store at it. Configuration is resolved from %env()% at runtime, so building the cache during the build step without secrets is fine. Run Messenger consumers as the platform's worker processes, store signed PDFs in a mount or object storage rather than var/, and run adobe-sign:verify in a post-deploy hook. These follow from how the bundle works; check your platform's documentation for the exact service and mount syntax.
The Docker build fails on ext-sodium. What do I add?
Install the extension in your image, for example docker-php-ext-install sodium on the official PHP images.
It is required even with an Integration Key, because the token encryptor is part of the bundle. Most distributions' PHP packages already include it.
Do I lose webhooks during a deploy?
No. Adobe retries failed deliveries for up to 72 hours.
Keep downtime short, restart workers with messenger:stop-workers, and make sure a deploy never leaves the webhook path behind a login. See Deploys and downtime.