Start here
Configuration
Everything the bundle does is driven by the adobe_sign configuration tree. This page lists every key with its default and what it changes, shows how to feed secrets through %env()%, and gives complete examples for local development, a multi-server production setup and a SaaS where customers connect their own accounts.
How configuration is read
The bundle is an AbstractBundle whose extension alias is adobe_sign. Symfony reads
your YAML (or PHP) files, merges them per environment, validates them against the tree the bundle
declares, and the bundle turns the result into service definitions when the container is compiled. Three
consequences follow:
- Types are checked at build time. A string where an integer belongs (
base_uri_ttl,dedupe_ttl,retries.max) failscache:clearwith a clear message. With an environment variable, cast it:'%env(int:ADOBE_SIGN_DEDUPE_TTL)%'. - Values are checked when used.
authandsignature_levelare plain strings in the tree. The manager validates them the first time it builds a client and throwsMisconfiguredExceptionnaming the key, so a typo can't silently fall back to a default. - Services are referenced by id.
cache_pool,oauth.token_cache_pool,http_client,oauth.token_store,oauth.user_key_resolverandwebhooks.messenger.bustake service ids. A missing id fails the container build, excepthttp_client, which falls back to discovery (see the HTTP client).
Two console commands show what Symfony actually resolved:
php bin/console config:dump-reference adobe_sign # the tree with defaults and help texts
php bin/console debug:config adobe_sign # your merged values for the current env
The full tree with defaults
Every value below is the default. You only write the keys you change; for an Integration Key setup that
is usually just integration_key.
adobe_sign:
auth: integration_key # "integration_key" or "oauth"
integration_key: ~ # usually '%env(ADOBE_SIGN_INTEGRATION_KEY)%'
oauth:
client_id: ~
client_secret: ~
redirect_uri: ~ # default: absolute URL of route adobe_sign_oauth_callback
scopes:
- 'user_login:self'
- 'agreement_read:self'
- 'agreement_write:self'
- 'agreement_send:self'
- 'webhook_read:self'
- 'webhook_write:self'
redirect_after: /
user_key_resolver: ~ # service id; default keys tokens by getUserIdentifier()
token_store: cache # "cache", "doctrine" or a TokenStore service id
token_cache_pool: cache.app
encryption_key: '%kernel.secret%'
doctrine:
connection: default
table: adobe_sign_tokens
signature_level: ses # ses, aes or qes (any case)
qualified_issuers: []
bootstrap_url: 'https://api.adobesign.com'
base_uri_ttl: 86400 # seconds
retries:
max: 3
max_delay_seconds: 60.0
cache_pool: cache.app
http_client: ~ # PSR-18 service id
webhooks:
client_ids: [] # empty: [oauth.client_id]
dedupe_ttl: 259200 # 72 hours
messenger:
enabled: false
bus: messenger.default_bus
Authentication
| Key | Default | Effect |
|---|---|---|
auth | integration_key | What the default client (AdobeSign service, $manager->client()) uses. integration_key: the key below. oauth: the OAuth tokens of the user currently signed in. Case and surrounding spaces are ignored; any other word throws MisconfiguredException. |
integration_key | null | The Integration Key. Required when auth is integration_key and you use the default client. Empty or null throws MisconfiguredException on first use, not at boot, so commands that never touch Acrobat Sign keep working. |
auth: oauth changes only what the default client is. $manager->forUser($user)
works in either mode, as long as the oauth section is filled in. A common mix: keep
auth: integration_key for system jobs and call forUser() where a user sends from
their own account. See injecting the manager.
OAuth application and routes
| Key | Default | Effect |
|---|---|---|
oauth.client_id | null | Your Acrobat Sign API application's client id. Also the default webhook allow-list, because Adobe sends it in X-AdobeSign-ClientId. |
oauth.client_secret | null | The application's secret. Both id and secret are needed for anything OAuth; otherwise MisconfiguredException. |
oauth.redirect_uri | null | Where Adobe sends users back. When null, the manager generates the absolute URL of adobe_sign_oauth_callback with the router, from the current request's scheme and host. Set it explicitly when the app sits behind a proxy or is reached under several hosts. Must match the URI registered at Adobe exactly. |
oauth.scopes | six :self scopes | The scopes requested on the consent screen. A list replaces the default entirely. See choosing scopes. |
oauth.redirect_after | / | Path or URL the callback redirects to after storing the tokens. An empty string also means /. |
oauth.user_key_resolver | null | Service id of a UserKeyResolver. The default resolver keys tokens by UserInterface::getUserIdentifier(). Use your own to share one connection per team. See user keys. |
Token storage
| Key | Default | Effect |
|---|---|---|
oauth.token_store | cache | cache: CacheTokenStore in a PSR-6 pool. doctrine: DoctrineDbalTokenStore in a table (needs doctrine/dbal). Anything else is taken as the id of your own service implementing Conceptlz\AdobeSign\Auth\TokenStore. |
oauth.token_cache_pool | cache.app | Pool for token_store: cache. Use a persistent, shared pool (Redis, database) without a default lifetime. Never an array pool. |
oauth.encryption_key | %kernel.secret% | Secret the stored tokens are encrypted with (a libsodium key is derived from it). Changing it makes every stored token unreadable; users then reconnect. Empty throws MisconfiguredException on first use. |
oauth.doctrine.connection | default | DBAL connection name for token_store: doctrine; the bundle uses service doctrine.dbal.<name>_connection. |
oauth.doctrine.table | adobe_sign_tokens | Table name for token_store: doctrine. |
The trade-offs between the stores, and key rotation, are covered in Token storage & Doctrine.
Signature level
| Key | Default | Effect |
|---|---|---|
signature_level | ses | Default level for every agreement the bundle's clients create from an AgreementInfo builder: ses, aes or qes, any case. AES and QES make Adobe require a certificate-based digital signature from each signer that doesn't choose its own level. Any other value throws MisconfiguredException when a client is built. |
qualified_issuers | [] | Certificate issuer names you accept as qualified. Used by $manager->signatureShortfalls() to tell a QES from an AES. Empty means no signature can be confirmed as QES. |
See Signature levels for overrides per agreement and per signer.
Data centres, cache, HTTP and retries
| Key | Default | Effect |
|---|---|---|
cache_pool | cache.app | PSR-6 pool for the data-centre lookup (wrapped as PSR-16, service adobe_sign.cache) and for webhook de-duplication. Share it between servers in production. |
bootstrap_url | https://api.adobesign.com | Where the SDK asks GET /baseUris which data centre the account lives on. Change it only for accounts in a separate Adobe environment. |
base_uri_ttl | 86400 | Seconds to cache that answer. A 403 INVALID_API_ACCESS_POINT triggers rediscovery anyway. |
http_client | null | Service id of a PSR-18 client. Null: psr18.http_client if registered, otherwise discovery. |
retries.max | 3 | How many times the SDK retries a 429 or 503 before throwing. 0 disables retries. |
retries.max_delay_seconds | 60.0 | The longest single wait the SDK accepts, from Retry-After or its own backoff. A longer Retry-After surfaces as RateLimitException so a worker can reschedule instead of blocking. |
cache.app is per machine by default
Out of the box cache.app is a filesystem pool under var/cache. That's fine on one server. Behind a load balancer each server keeps its own copy, so a webhook retry that lands on another server is not recognised as a duplicate, and OAuth tokens stored on one server are invisible to the others. Point cache_pool (and oauth.token_cache_pool, if you store tokens in a cache) at a shared pool.
framework:
cache:
pools:
adobe_sign.pool: # data centres + webhook dedupe
adapter: cache.adapter.redis
provider: '%env(REDIS_URL)%'
adobe_sign.tokens: # OAuth tokens: no default_lifetime!
adapter: cache.adapter.redis
provider: '%env(REDIS_URL)%'
adobe_sign:
cache_pool: adobe_sign.pool
oauth:
token_cache_pool: adobe_sign.tokens
Logging
There is no logging key. The manager and the webhook controller are tagged with the Monolog channel
adobe_sign, so with MonologBundle their logs go to monolog.logger.adobe_sign, which
you route like any channel. The SDK logs each request at debug, throttling retries at
warning and requests that could not be sent at all at error; it never logs headers or bodies. The webhook
controller logs rejected client ids (warning) and unreadable bodies (error). See
Logging.
Webhooks
| Key | Default | Effect |
|---|---|---|
webhooks.client_ids | [] | Client ids allowed in X-AdobeSign-ClientId. Empty: [oauth.client_id]. Neither set: the webhook route answers 500 with MisconfiguredException. A non-empty list replaces the OAuth client id; include it yourself if you need both. |
webhooks.dedupe_ttl | 259200 | Seconds to remember a notification id, stored in cache_pool. Adobe retries failed deliveries for up to 72 hours, hence the default. |
webhooks.messenger.enabled | false | Also dispatch an AdobeSignWebhookMessage per new notification. webhooks: { messenger: true } is shorthand for enabling it. |
webhooks.messenger.bus | messenger.default_bus | Service id of the bus to dispatch to. Setting a bus enables Messenger dispatch too. |
Environment variables and secrets
Any scalar value can come from the environment. The bundle doesn't read environment variables itself; it receives whatever your config resolves to. The usual set:
adobe_sign:
integration_key: '%env(ADOBE_SIGN_INTEGRATION_KEY)%'
signature_level: '%env(default:adobe_sign_default_level:ADOBE_SIGN_SIGNATURE_LEVEL)%'
oauth:
client_id: '%env(ADOBE_SIGN_CLIENT_ID)%'
client_secret: '%env(ADOBE_SIGN_CLIENT_SECRET)%'
encryption_key: '%env(ADOBE_SIGN_TOKEN_KEY)%'
webhooks:
client_ids: ['%env(ADOBE_SIGN_CLIENT_ID)%'] # one variable per list item
dedupe_ttl: '%env(int:ADOBE_SIGN_DEDUPE_TTL)%'
parameters:
adobe_sign_default_level: ses
- List keys (
client_ids,qualified_issuers,scopes) can't take one variable for the whole list: Symfony rejects "a dynamic value" on array nodes, so'%env(csv:VAR)%'fails the build. Put one%env()%per item instead. int:is needed for integer keys, because environment variables are strings.default:param:VARfalls back to a parameter when the variable isn't set.- Secrets (
php bin/console secrets:set NAME) are read through the same%env()%syntax, so production can use the vault without changing the config.
Examples per environment
Symfony merges config/packages/adobe_sign.yaml with
config/packages/<env>/adobe_sign.yaml, or with a when@<env>: block in the
same file. Use whichever your project already uses.
Local development
adobe_sign:
# A developer account: agreements get "[DEMO USE ONLY]" and aren't legally meant.
integration_key: '%env(ADOBE_SIGN_INTEGRATION_KEY)%'
oauth:
# The tunnel's host, so Adobe's redirect matches what you registered.
redirect_uri: 'https://my-tunnel.example.com/adobe-sign/oauth/callback'
The test environment
when@test:
adobe_sign:
integration_key: 'test-key'
http_client: test.adobe_sign_http # a mock PSR-18 client, see Testing
webhooks:
client_ids: ['test-client-id']
services:
test.adobe_sign_http:
class: Http\Mock\Client
public: true
The Testing page explains why the mock is public and how to keep it alive between requests.
Production on several servers
adobe_sign:
integration_key: '%env(ADOBE_SIGN_INTEGRATION_KEY)%'
signature_level: aes
cache_pool: adobe_sign.pool # Redis, shared by every web server and worker
retries:
max: 5
max_delay_seconds: 30 # longer waits become RateLimitException
webhooks:
client_ids: ['%env(ADOBE_SIGN_CLIENT_ID)%']
messenger: true # route the message to an async transport
A SaaS where customers connect their own accounts
adobe_sign:
auth: oauth # AdobeSign service = the signed-in user's account
oauth:
client_id: '%env(ADOBE_SIGN_CLIENT_ID)%'
client_secret: '%env(ADOBE_SIGN_CLIENT_SECRET)%'
redirect_uri: 'https://app.example.com/adobe-sign/oauth/callback'
redirect_after: /settings/integrations
token_store: doctrine
encryption_key: '%env(ADOBE_SIGN_TOKEN_KEY)%' # rotate APP_SECRET without disconnecting users
user_key_resolver: App\AdobeSign\TeamKeyResolver
scopes:
- 'user_login:self'
- 'agreement_read:account'
- 'agreement_write:account'
- 'agreement_send:account'
- 'webhook_read:account'
- 'webhook_write:account'
webhooks:
messenger: true
Changing values at runtime
You can't. The configuration is compiled into the container, and the manager receives it as a constructor argument. That is deliberate: credentials in one place, validated once. If different tenants need different Integration Keys, build a core client per tenant with the core's named constructor and pass the bundle's services in, rather than mutating configuration:
use Conceptlz\AdobeSign\AdobeSign;
$client = AdobeSign::withIntegrationKey($tenant->adobeSignKey(), cache: $psr16Cache, logger: $logger);
Inject $psr16Cache as the service adobe_sign.cache (for example with
#[Autowire(service: 'adobe_sign.cache')]) so the data-centre lookup is shared.
Common questions
Does the file have to be called adobe_sign.yaml?
No. Symfony reads every file in config/packages/; what matters is the root key adobe_sign:. Naming the file after the key is just the convention.
Can I write the configuration in PHP instead of YAML?
Yes. In config/packages/adobe_sign.php, return a closure that receives a ContainerConfigurator and call $container->extension('adobe_sign', [...]) with the same keys. The bundle's own test kernel configures itself this way.
I changed .env.local but nothing changed. Why?
In prod, values may be dumped into .env.local.php (composer dump-env prod), and long-running processes keep what they read at start. Re-dump the env file, clear the cache, and restart PHP-FPM workers, FrankenPHP workers and Messenger consumers.
Can one app use both an Integration Key and OAuth?
Yes. Set integration_key and the oauth section, keep auth: integration_key, and call $manager->forUser($user) where a user's own account should send. The default client stays the key.
How do I catch configuration mistakes before production?
Run php bin/console lint:container and cache:clear in CI for type and service-id errors, and adobe-sign:verify after deploy for credentials and the auth/signature_level values, which are checked when the first client is built.