Adobe Sign for Symfonyconceptlz/adobe-sign-symfony GitHub

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

  1. Do the credentials work?
    php bin/console adobe-sign:verify
    php bin/console adobe-sign:verify --user=jane@example.com     # an OAuth connection

    It calls Adobe and prints the user, or the exact error, including every MisconfiguredException.

  2. Is the configuration you think you have loaded?
    php bin/console debug:config adobe_sign --env=prod
    php bin/console debug:container AdobeSignManager --show-arguments

    Restart PHP-FPM, FrankenPHP workers and Messenger consumers after configuration changes; they keep what they loaded.

  3. Are the routes there, and who guards them?
    php bin/console debug:router | grep adobe_sign
    php bin/console debug:firewall

    You should see adobe_sign_webhook and, if you use OAuth, the two adobe_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

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 withFix
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

NotFoundException

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

HTTP 404RouteNotFoundException

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

ServiceNotFoundException

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

HTTP 401AuthenticationException

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

HTTP 401UnauthorizedHttpException

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)

HTTP 400STATE_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

HTTP 403redirect_uri

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

SessionNotFoundException

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

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

CacheTokenStoreNO_TOKEN

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

TokenEncryptor

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

TableNotFoundException

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

HTTP 403WebhookException

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

HTTP 302HTTP 401

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"

UnexpectedSessionUsageException

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"

InvalidArgumentException

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

cache_pool

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

AdobeSignWebhookMessage

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

HTTP 500NoHandlerForMessageException

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

HTTP 403ApiException

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

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

ApiException

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

HTTP 429RateLimitException

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.