Help
Troubleshooting
Each entry starts from what you see, explains why it happens, and gives the fix. Start with the three checks; most problems are configuration, a firewall in the wrong place, a cache that isn't shared, or a worker that isn't running.
First, three checks
- Do the credentials work?
php bin/console adobe-sign:verify php bin/console adobe-sign:verify --user=jane@example.com # an OAuth connectionIt calls Adobe and prints the user, or the exact error, including every
MisconfiguredException. - Is the configuration you think you have loaded?
php bin/console debug:config adobe_sign --env=prod php bin/console debug:container AdobeSignManager --show-argumentsRestart PHP-FPM, FrankenPHP workers and Messenger consumers after configuration changes; they keep what they loaded.
- Are the routes there, and who guards them?
php bin/console debug:router | grep adobe_sign php bin/console debug:firewallYou should see
adobe_sign_webhookand, if you use OAuth, the twoadobe_sign_oauth_*routes. The webhook path must not be behind a login.
Find your problem
25 problems in 6 groups. Type an error code, an exception name or a word from the message to narrow them down, or pick one from the index.
Setup and configuration
MisconfiguredException
What you see
Conceptlz\AdobeSign\Symfony\MisconfiguredException from an injected client, the manager, adobe-sign:verify, the OAuth routes or the webhook route (as a 500).
Why it happens
A setting the feature needs is empty or invalid. The bundle checks when a feature is first used, not at boot. The message names the setting:
| Message starts with | Fix |
|---|---|
| Set adobe_sign.integration_key… | Set ADOBE_SIGN_INTEGRATION_KEY in .env.local or the server environment, or set auth: oauth if you meant OAuth. |
| adobe_sign.auth is "oauth" but no user is signed in… | You're in a command, worker, listener or anonymous request. Use $manager->forUser($key) there, and inject the manager rather than AdobeSign. |
| adobe_sign.auth must be "integration_key" or "oauth"… | Fix the value. Case doesn't matter; other words do. |
| Set adobe_sign.oauth.client_id and client_secret… | Add both credentials of your API application. |
| Set adobe_sign.oauth.redirect_uri: the router is not available… | Set oauth.redirect_uri explicitly; the router isn't there to generate it. |
| Set adobe_sign.webhooks.client_ids… | Add the client id(s) Adobe sends, or oauth.client_id. |
| adobe_sign.signature_level must be ses, aes or qes… | Fix the value; it is checked every time a client is built. |
| adobe_sign.oauth.encryption_key is empty… | APP_SECRET is empty in this environment (recent skeletons leave it empty in .env), or encryption_key points at an empty variable. Set one. |
How to fix it
Set the value, clear the cache in production (cache:clear --env=prod, or re-run composer dump-env prod if you use it) and restart long-running workers. adobe-sign:verify confirms the fix.
No PSR-18 client found
What you see
Http\Discovery\Exception\NotFoundException (or a DiscoveryFailedException) on the first API call, saying no PSR-18 client or no PSR-17 factory could be found.
Why it happens
The SDK needs a PSR-18 client and PSR-17 factories. http_client isn't set, FrameworkBundle didn't register psr18.http_client (it needs symfony/http-client plus a PSR-17 implementation), and discovery found nothing else installed. A typo in http_client leads here too, because a missing service id falls back to discovery.
How to fix it
composer require symfony/http-client nyholm/psr7, clear the cache, and check that php bin/console debug:container psr18.http_client finds the service. If you set http_client, check the id the same way. See the HTTP client.
Routes not found
What you see
No route found for "POST /adobe-sign/webhook" (Adobe's verification fails), or Unable to generate a URL for the named route "adobe_sign_oauth_callback" as such route does not exist from oauth(), the redirect route or your own generateUrl('adobe_sign_webhook').
Why it happens
The bundle ships its routes but never loads them by itself. Without a route import, neither path exists. The default OAuth redirect URI is generated from adobe_sign_oauth_callback, so it fails if only the webhook routes are imported.
How to fix it
Import @AdobeSignBundle/config/routes.php (or the file you need) in config/routes/adobe_sign.yaml, then debug:router | grep adobe_sign. With a prefix, register the prefixed URLs at Adobe. See importing the routes.
Container build fails: non-existent service
What you see
cache:clear or the first request fails with, for example, The service "Conceptlz\AdobeSign\Symfony\Controller\WebhookController" has a dependency on a non-existent service "messenger.default_bus", or the same for doctrine.dbal.default_connection or a cache pool id.
Why it happens
A configuration key names a service that doesn't exist: webhooks.messenger without Messenger installed (or a wrong bus), token_store: doctrine without DoctrineBundle (or a wrong doctrine.connection), a misspelt cache_pool or token_cache_pool, or a token_store / user_key_resolver id that isn't a service.
How to fix it
Install the missing package (symfony/messenger, symfony/orm-pack) or correct the id. debug:container <id> and debug:messenger list what exists.
Key rejected: INVALID_ACCESS_TOKEN
What you see
adobe-sign:verify or any call fails with AuthenticationException and code INVALID_ACCESS_TOKEN.
Why it happens
The Integration Key is wrong, truncated, revoked, or belongs to a user who was deactivated. Quotes or spaces copied into .env.local are a common cause.
How to fix it
Create a new key in Acrobat Sign (Account → Acrobat Sign API → API Information), paste it without quotes, and check the owning user is active. Make sure it has the scopes your calls need.
OAuth
Callback answers 401
What you see
After consenting at Adobe, the user lands on a 401 page: "Sign in before connecting Acrobat Sign."
Why it happens
When the callback runs, security.token_storage holds no user. Either the callback path isn't covered by a firewall that authenticates users (a security: false firewall, or a pattern that misses /adobe-sign/oauth), SecurityBundle isn't installed, or the user's session ended while they were at Adobe (expired, or a different host lost the cookie).
How to fix it
Make sure /adobe-sign/oauth/* is under your main firewall and guarded with access_control (ROLE_USER), so signed-out users log in before going to Adobe. debug:firewall shows which firewall matches. The state is consumed even on a 401, so the user must click "Connect" again. See protecting the routes.
Callback answers 400 (state mismatch)
What you see
"OAuth state mismatch: the callback was not started by this session." (or "OAuth callback has no authorization code.") after returning from Adobe.
Why it happens
The state in the URL doesn't match the one in this session, or the session has none. Typical causes: the flow started on www.example.com and the redirect URI points at example.com (different cookie, different session); the session expired or wasn't persisted (a stateless firewall, a session handler that drops writes); the user started connecting in two tabs (the second overwrites the first state); or they refreshed or bookmarked the callback URL (the state is single-use).
How to fix it
Use one canonical host for both the app and the registered redirect URI, keep sessions enabled on the OAuth routes, and start again from your app's "Connect" link. If it happens to everyone, check the session cookie's secure and samesite settings and that the redirect route's response actually sets it.
Redirect URI rejected by Adobe
What you see
Adobe's page shows an error about an invalid redirect URI instead of the consent screen, or the callback answers 403 after the code exchange fails.
Why it happens
The redirect URI the bundle sends doesn't match the one registered on the API application character for character. With redirect_uri: ~ it is generated from the current request: behind a TLS-terminating proxy without trusted_proxies it becomes http://…; on another host or port, or under a route prefix, it differs too.
How to fix it
Set oauth.redirect_uri explicitly to the registered URL, or configure framework.trusted_proxies and trusted_headers so Symfony sees the public scheme and host. Compare the redirect_uri parameter in the redirect to Adobe with the registered value. See the redirect URI.
SessionNotFoundException on the redirect route
What you see
"There is currently no session available." when opening /adobe-sign/oauth/redirect.
Why it happens
The OAuth controller keeps the state in the session, and sessions are disabled (framework.session: false or not configured), as in API-only apps.
How to fix it
Enable framework.session. If your app must stay sessionless, build the flow yourself with $manager->oauth() and keep the state somewhere tied to the user (for example a short-lived signed cookie).
Connection expired: OAuthException
What you see
OAuthException with NO_TOKEN, NO_REFRESH_TOKEN, INVALID_TOKEN or EXPIRED_TOKEN when using forUser().
Why it happens
NO_TOKEN: nothing readable is stored under that key: the user never connected, the key changed (a new UserKeyResolver, a changed user identifier), the token was lost or can't be decrypted (see lost and unreadable tokens). The others: the refresh token no longer works, because the user or an admin revoked access or it went unused for too long.
How to fix it
Send the user through adobe_sign_oauth_redirect again; catching OAuthException and redirecting is the usual pattern. Check isConnected() before calls to avoid NO_TOKEN in the first place.
Token storage
Users appear disconnected after a deploy or cache clear
What you see
isConnected() returns false and forUser() fails with NO_TOKEN for users who connected recently; or it works on one server and not another, or in the web app but not in workers.
Why it happens
With token_store: cache, tokens live in oauth.token_cache_pool, cache.app by default: a filesystem pool under var/cache on each machine. A new container, a deploy that replaces var/, cache:pool:clear, an array or APCu pool, a pool with a default_lifetime, or Redis evicting keys all lose them; separate servers never shared them.
How to fix it
Switch to token_store: doctrine, or point token_cache_pool at a dedicated, shared, persistent pool without a default lifetime (and a Redis eviction policy that doesn't evict it). Affected users reconnect once. See persistent pools.
Everyone disconnected after rotating APP_SECRET
What you see
After changing APP_SECRET (or encryption_key), or on a server with a different value, every user appears disconnected. Rows are still in the table; nothing throws.
Why it happens
Tokens are encrypted with a key derived from oauth.encryption_key, which defaults to %kernel.secret%. Values encrypted with another key decrypt to null by design, so the store reports "no token".
How to fix it
Use the same secret on every server and worker. If the old secret is still known, restore it, or re-encrypt the tokens once with both keys. Otherwise users reconnect. For the future, set a dedicated encryption_key so rotating APP_SECRET doesn't touch tokens. See rotating the secret.
Token table missing
What you see
A DBAL error such as SQLSTATE[42S02]: Base table or view not found, relation "adobe_sign_tokens" does not exist or no such table: adobe_sign_tokens, from the OAuth callback, forUser() or isConnected().
Why it happens
token_store: doctrine is set but the table was never created on that connection: no migration was generated, it wasn't run in this environment, a schema_filter hid it, or oauth.doctrine.connection points at a different database than the one you migrated.
How to fix it
With the ORM: doctrine:migrations:diff then migrate. Without it: php bin/console adobe-sign:create-token-table (idempotent, safe on every deploy). For a test database, run the same before the suite. See the Doctrine store.
Webhooks
Webhook answers 403: client id not allowed
What you see
webhooks()->create() fails because Adobe's verification got a 403, or deliveries fail. The adobe_sign log shows "Rejected Acrobat Sign webhook" with the reason "Missing X-AdobeSign-ClientId header." or "Webhook from an unexpected client id."
Why it happens
The client id Adobe sends isn't in webhooks.client_ids (or oauth.client_id when the list is empty). Common reasons: webhooks created with an Integration Key carry a different application's id than you assumed; an explicit client_ids list replaced the OAuth client id; staging and production use different applications; or a proxy strips the header.
How to fix it
Find the id Adobe actually sends (log the header once, see the allow-list), add it to webhooks.client_ids, clear the cache, then re-create the webhook or re-enable it with updateState($id, 'ACTIVE').
Webhook redirected to the login page
What you see
Adobe's verification fails, and your access log shows /adobe-sign/webhook answering 302 to /login, or 401. The adobe_sign log shows nothing, because the controller never ran.
Why it happens
A firewall or an access_control rule requires a user for the webhook path: a catch-all such as { path: ^/, roles: ROLE_USER }, a rule for ^/adobe-sign meant for the OAuth routes, or a stateless API firewall that demands a token for every path.
How to fix it
Give the path its own firewall with security: false before main, or add { path: ^/adobe-sign/webhook, roles: PUBLIC_ACCESS } above any broader rule, and narrow OAuth rules to ^/adobe-sign/oauth. Check with debug:firewall. See firewalls and access_control.
"Session was used while the request was declared stateless"
What you see
In the dev environment, posting to the webhook throws UnexpectedSessionUsageException; in prod, a warning is logged and a session cookie is sent to Adobe.
Why it happens
The webhook route is stateless, but something started the session for it: usually a non-lazy stateful firewall covering the path, or one of your own listeners reading the session or the user on every request.
How to fix it
Put the path under a security: false firewall, or make main lazy: true. Make your own global listeners skip requests whose _stateless attribute is true.
"Acrobat Sign only delivers webhooks to HTTPS URLs"
What you see
WebhookInfo::forAgreement() (or another constructor) throws before any request.
Why it happens
The URL starts with http://. With generateUrl(..., ABSOLUTE_URL) that means Symfony thinks the request was plain HTTP (a proxy without trusted_proxies), or you're in the console or a worker without framework.router.default_uri, so the router used http://localhost.
How to fix it
Configure trusted_proxies and trusted_headers, and set framework.router.default_uri to your public https:// origin. Locally, use an HTTPS tunnel. See HTTPS and the generated URL.
The same notification is handled twice
What you see
A listener or handler runs twice for one signature: two emails, two rows, two downloads.
Why it happens
Duplicate detection lives in cache_pool. If each server has its own (cache.app on local disk), a retry that lands on another server looks new. It also happens after the pool was cleared, when dedupe_ttl is shorter than Adobe's 72-hour retry window, or when Messenger redelivers a message after a crash (deduplication can't help there).
How to fix it
Point cache_pool at a shared pool (Redis, Memcached, database) used by every web server, keep dedupe_ttl at 259200, and make handlers idempotent. See idempotency.
Webhooks worked, then stopped
What you see
No more deliveries for some or all agreements, and nothing in your logs.
Why it happens
Adobe disables a webhook after 72 hours of failed deliveries: an outage, an expired certificate, a deploy that put the path behind a login, a 403 after an allow-list change. Notifications while it is disabled are not sent later.
How to fix it
Fix the cause, find disabled webhooks with webhooks()->all(showInActiveWebhooks: true), re-enable them with updateState($id, 'ACTIVE'), and reconcile agreements your records still show as open by asking the API. notifyOnProblems() on the builder makes Adobe email you before it gets that far.
Messenger
Webhook answers 200, but the handler never runs
What you see
Adobe's deliveries succeed, but your #[AsMessageHandler] for AdobeSignWebhookMessage does nothing: no PDF, no log line.
Why it happens
In order of likelihood: no worker is consuming the transport the message is routed to; webhooks.messenger isn't enabled in this environment (only WebhookReceivedEvent fires); the message went to another bus than the one your handler is registered on; the handler returned early (wrong event type, agreement not found); the message failed and sits in the failure transport; or it was a duplicate.
How to fix it
Check debug:config adobe_sign (messenger enabled, bus), debug:messenger (handler and routing on that bus), messenger:stats (messages waiting), messenger:failed:show, and run messenger:consume async -vv to watch. Restart workers after deploying handler changes. See running workers.
Webhook answers 500: NoHandlerForMessageException
What you see
"No handler for message Conceptlz\AdobeSign\Symfony\Message\AdobeSignWebhookMessage" in your error log, and Adobe's delivery fails.
Why it happens
webhooks.messenger is on, the message isn't routed to a transport, and the bus has no handler for it, so Messenger refuses it inside the webhook request. The bundle doesn't record a notification it failed to handle, so Adobe keeps retrying, but it gives up after 72 hours.
How to fix it
Route the message to a transport and deploy at least one handler, or dispatch to a bus with allow_no_handlers: true. Deliveries Adobe retries after the fix are processed normally; for a long outage, reconcile affected agreements by asking the API for their status. See Enabling Messenger.
API errors
403 INVALID_API_ACCESS_POINT
What you see
An ApiException with status 403 and code INVALID_API_ACCESS_POINT.
Why it happens
The request went to the wrong data centre. The SDK already handles the normal case: on this error it forgets the cached data centre, rediscovers it and retries once. If you still see it, the rediscovery itself points somewhere the credentials don't belong: a wrong bootstrap_url (for example an account in a separate Adobe environment against the commercial host), or an OAuth token whose stored access point no longer matches the account.
How to fix it
Leave bootstrap_url at its default unless your account lives in a separate Adobe environment. For an OAuth user, have them connect again, which stores a fresh access point. Clearing cache_pool is safe; the data centre is rediscovered on the next call.
AccountCapabilityException
What you see
AccountCapabilityException with codes such as HYBRID_ROUTING_NOT_ALLOWED, GOV_ID_LIMIT_EXCEEDED or SEND_LIMIT_EXCEEDED, or a message saying something is not allowed by account settings.
Why it happens
The request is valid, but the Acrobat Sign account's plan or settings don't allow it: mixed sequential and parallel signing (hybrid routing), an identity method that isn't enabled or is out of quota, digital signatures not enabled for AES or QES, a sending limit.
How to fix it
Enable the feature in Acrobat Sign admin or upgrade the plan, or change the request (for example a purely sequential order). Show users a clear message rather than a 500; $e->errorCode tells you which capability is missing. In a Messenger handler, treat it as unrecoverable. See turning exceptions into responses.
AGREEMENT_NOT_EXPOSED right after creating an agreement
What you see
A call made seconds after agreements()->create() (signing URLs, members, documents) fails with AGREEMENT_NOT_EXPOSED, or members are empty.
Why it happens
Adobe processes new agreements asynchronously. The status is DOCUMENTS_NOT_YET_PROCESSED for a few seconds, and participant data lags behind the status.
How to fix it
Don't fetch signing URLs in the same request that creates the agreement. Dispatch a Messenger message with a short DelayStamp, or retry a few times with a pause. The core's downloads page shows a retry loop.
RateLimitException
What you see
RateLimitException, often during bulk jobs, with $retryAfter set.
Why it happens
Adobe throttled the account. The SDK retries 429 and 503 itself up to retries.max times, waiting up to retries.max_delay_seconds each; it throws when Adobe asks for a longer wait or the retries run out.
How to fix it
In a request, answer 503 with Retry-After. In a worker, re-dispatch the message with new DelayStamp($e->retryAfter * 1000). Spread bulk work out, and use webhooks instead of polling.
Common questions
How can I see what the SDK sends?
Route the adobe_sign Monolog channel at debug level to log every request (method, path, status, duration), never headers or bodies. For full requests during development, use the Symfony profiler's HTTP Client panel (with psr18.http_client, calls go through Symfony's HttpClient) or a mock client in tests. Never log bodies in production: they contain documents and personal data.
Will an exception in my error tracker leak the key or tokens?
No. The SDK redacts the Authorization header and OAuth secrets from every exception it creates, tokens and the encryption secret are hidden from dumps, and adobe-sign:verify never prints the key. Your own code can still leak them if it logs configuration wholesale.
Why do agreement names start with [DEMO USE ONLY]?
Adobe adds that prefix to agreements created from developer accounts. It disappears on a production account; nothing in your code causes it.
It works in dev but not in prod. Where do I start?
Compare debug:config adobe_sign --env=prod with dev, check the production environment variables or secrets exist, clear the prod cache, and check the three classic differences: a shared cache pool, the webhook firewall, and trusted proxies for generated URLs.
None of this matches my problem. What next?
Check the core's troubleshooting page for API-level issues, then open an issue with the exception class, Adobe's error code, the package version, your Symfony version and what you expected. Leave out keys, tokens and personal data.