Adobe Sign for Laravelconceptlz/adobe-sign-laravel GitHub

Help

Troubleshooting

Each entry starts from what you see, explains why it happens, and gives the fix. Start with the quick lookup; most problems are configuration, a missing queue worker, or something Adobe can't reach.

First, three checks

  1. Do the credentials work?
    php artisan adobe-sign:verify

    It calls Adobe with the default client and prints the user, or the exact error. Add --user=ID for an OAuth connection.

  2. Is the config you think you have loaded?
    php artisan config:clear
    php artisan tinker --execute="dump(config('adobe-sign.auth'), config('adobe-sign.webhooks.client_ids'))"

    Cached config ignores .env edits. Restart Octane and queue workers after config changes.

  3. Are the routes there?
    php artisan route:list --name=adobe-sign

    You should see adobe-sign.webhook and, unless disabled, the two adobe-sign.oauth.* routes.

Quick lookup

You seeGo to
MisconfiguredExceptionMissing configuration
"Base table or view not found" / "no such table: adobe_sign_tokens"Token table missing
AuthenticationException with INVALID_ACCESS_TOKENKey rejected
OAuth callback answers 400State mismatch
Adobe's page says the redirect URI is invalid, or callback answers 403Redirect URI
OAuthException with EXPIRED_TOKEN, INVALID_TOKEN or NO_TOKENConnection expired
webhooks()->create() failsWebhook can't be registered
"Acrobat Sign only delivers webhooks to HTTPS URLs"http:// URL
Webhook answers 403Client id not allowed
Webhook answers 419 or redirects to loginSession middleware on the route
Webhook answers 200 but nothing happensNo events processed
The same event is handled twiceDuplicate events
Webhooks worked, then stoppedWebhook disabled by Adobe
403 INVALID_API_ACCESS_POINTWrong data centre
AccountCapabilityExceptionAccount doesn't allow it
NotFoundException right after creating an agreementNot ready yet
RateLimitExceptionThrottled
DecryptException ("The MAC is invalid")APP_KEY changed

Find your problem

21 problems in 5 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\Laravel\MisconfiguredException from the facade, an injected client, adobe-sign:verify, the OAuth routes or the webhook route.

Why it happens

A setting the feature needs is empty. The message names it:

Message starts withFix
Set ADOBE_SIGN_INTEGRATION_KEY…Add the key, or set ADOBE_SIGN_AUTH=oauth if you meant to use OAuth.
adobe-sign.auth is "oauth" but no user is signed in…You're in a job, command, listener or guest request. Use AdobeSign::forUser($userId) there.
Set ADOBE_SIGN_CLIENT_ID and ADOBE_SIGN_CLIENT_SECRET…Add both OAuth app credentials.
Set ADOBE_SIGN_WEBHOOK_CLIENT_IDS…Add the client id(s) Adobe sends, or ADOBE_SIGN_CLIENT_ID.
ADOBE_SIGN_SIGNATURE_LEVEL must be ses, aes or qes…Fix the value; any case works, other words don't.

How to fix it

Set the value, then php artisan config:clear (or re-cache on servers) and restart long-running workers.

Token table missing

What you see

A database error such as SQLSTATE[42S02]: Base table or view not found or no such table: adobe_sign_tokens, from the OAuth callback, forUser() or isConnected().

Why it happens

The package doesn't load its migration automatically; it has to be published and run. Or you changed oauth.table after migrating.

How to fix it

php artisan vendor:publish --tag=adobe-sign-migrations
php artisan migrate

If you renamed the table, set oauth.table before running the migration, or rename the table to match.

Key rejected: INVALID_ACCESS_TOKEN

AuthenticationException

What you see

AuthenticationException mentioning INVALID_ACCESS_TOKEN; adobe-sign:verify fails.

Why it happens

The Integration Key is wrong, truncated, revoked, or belongs to a different environment. A stray quote or space in .env is common.

How to fix it

Copy the key again from Acrobat Sign (Account → Acrobat Sign API → API Information), clear the config cache and re-run adobe-sign:verify.

OAuth

Callback answers 400 (state mismatch)

HTTP 400

What you see

After consenting at Adobe the user lands on a 400 page.

Why it happens

The callback couldn't match the state in the URL with the one stored in the session. The package pulls the state on first use, so it fails when:

  • the session expired or was regenerated while the user was at Adobe;
  • the session cookie wasn't sent on the redirect back (a SameSite=strict cookie, or the callback is on a different domain or scheme than the redirect route);
  • the user started twice in two tabs and finished the older one;
  • the callback URL was reloaded or replayed, which is the protection working.

A 400 also happens when the URL has no code, or the api_access_point isn't an Adobe host.

How to fix it

Keep Laravel's default same_site = 'lax' in config/session.php, serve the redirect route and the callback from the same host and scheme, and let the user start again from the Connect button.

Redirect URI rejected

HTTP 403

What you see

Adobe shows an error about the redirect URI instead of the consent screen, or the callback answers 403 after the code exchange.

Why it happens

The redirect URI your app sends doesn't match one registered on the Adobe application character for character. Behind a proxy, route('adobe-sign.oauth.callback') may come out as http:// or with an internal host.

How to fix it

Register the exact URL on the application's OAuth configuration, configure trusted proxies so Laravel generates https:// URLs, or set ADOBE_SIGN_REDIRECT_URI explicitly. If you changed oauth.route_prefix, update the Adobe side too.

Connection expired: OAuthException

OAuthException

What you see

OAuthException with EXPIRED_TOKEN, INVALID_TOKEN, NO_REFRESH_TOKEN or NO_TOKEN when using forUser().

Why it happens

NO_TOKEN: the user never connected (or the key differs, e.g. after changing resolveUserKeyUsing()). The others: the refresh token went unused for 60 days, or the user or an admin revoked access.

How to fix it

Send the user through route('adobe-sign.oauth.redirect') again; catching OAuthException and redirecting is the usual pattern (see Refreshing tokens). Check AdobeSign::isConnected($user) before calls to avoid NO_TOKEN.

DecryptException after deploying

DecryptException

What you see

Illuminate\Contracts\Encryption\DecryptException ("The MAC is invalid.") when reading tokens.

Why it happens

Tokens are encrypted with APP_KEY, and it changed, or differs between servers.

How to fix it

Use the same APP_KEY on every server. When rotating, put the old key in APP_PREVIOUS_KEYS. If the old key is lost, delete the rows and ask users to reconnect.

Webhooks

Webhook can't be registered

What you see

webhooks()->create() throws, while uploading and creating agreements works.

Why it happens

Adobe verifies the URL immediately with a GET and expects a 2xx with the client-id echo. It fails when the URL isn't publicly reachable over HTTPS, when the client id Adobe sends isn't allowed (your route answers 403), when middleware intercepts the request, or when the allow-list isn't configured at all (500).

How to fix it

Check the URL from outside your network, check your log for "Rejected Acrobat Sign webhook", and confirm the route has no session or auth middleware. Locally, use a tunnel and set APP_URL to it (see local development).

"Acrobat Sign only delivers webhooks to HTTPS URLs"

InvalidArgumentException

What you see

InvalidArgumentException from WebhookInfo, before any request.

Why it happens

route('adobe-sign.webhook') produced an http:// URL: APP_URL is http, or a TLS-terminating proxy isn't trusted, or the code runs in a queue or console where only APP_URL is known.

How to fix it

Set APP_URL=https://… and configure trusted proxies. In tests, URL::forceScheme('https').

Webhook answers 403: client id not allowed

HTTP 403

What you see

Adobe's deliveries or your tunnel inspector show 403; your log has "Rejected Acrobat Sign webhook".

Why it happens

The X-AdobeSign-ClientId header is missing or not in ADOBE_SIGN_WEBHOOK_CLIENT_IDS (or ADOBE_SIGN_CLIENT_ID when that list is empty). Typical after adding a second Adobe application, or when webhooks are created with an Integration Key and the allow-list holds an OAuth app's id.

How to fix it

Log the incoming header once (see the allow-list), add the id, clear the config cache. Ids are comma-separated with no quotes around each one.

Webhook answers 419 or redirects to a login page

HTTP 419

What you see

419 "Page Expired" or a 302 to /login on Adobe's calls.

Why it happens

Session, CSRF or auth middleware is running on the route: web or auth was added to webhooks.middleware, or a global middleware applies to every route.

How to fix it

Keep webhooks.middleware free of web and auth, and exclude the webhook path from any global middleware that needs a session or user.

Webhook answers 200, but nothing happens

What you see

Adobe shows successful deliveries (or your tunnel shows 200s), but statuses don't update and no PDFs are stored.

Why it happens

Most often your listeners are queued and no worker is processing that queue. Other causes: the listener isn't registered (discovery off, or the event cache is stale), the delivery was a duplicate and correctly ignored, the listener returns early because the event isn't the type it expects, or the job failed.

How to fix it

Work through it in order:

  1. Is a worker running for the listener's queue? php artisan queue:work --queue=webhooks,default, or check Horizon/Supervisor.
  2. Is the listener registered? php artisan event:list --event=WebhookReceived should list it. After adding listeners in production, run php artisan event:cache again.
  3. Did the job fail? php artisan queue:failed.
  4. Did the event fire? Temporarily log in the listener's first line, or in tests use Event::fake([WebhookReceived::class]).

The same event is handled twice

What you see

Two emails, two downloads, two status changes for one signature.

Why it happens

Dedupe keys live in the cache. With a per-server store (file, array, APCu) a retry landing on another server isn't recognised, and php artisan cache:clear forgets every key. A queued job can also run twice after a timeout, and two different notifications can lead to the same action.

How to fix it

Point ADOBE_SIGN_CACHE_STORE at a store all servers share (Redis, Memcached, database, DynamoDB), and make listeners and jobs idempotent: check your own record before acting, or use ShouldBeUnique.

Cache store not shared between servers

What you see

Duplicate events as above, plus every server making its own GET /baseUris call.

Why it happens

The default cache store is local to each machine (for example file), so neither the data-centre lookup nor the webhook ids are shared.

How to fix it

ADOBE_SIGN_CACHE_STORE=redis (or another shared store). Only the SDK's keys move; the rest of your app can keep its default store.

Webhooks worked, then stopped

What you see

No more deliveries for some or all agreements.

Why it happens

Adobe disables a webhook after 72 hours of failed deliveries (an outage, an expired certificate, a broken deploy, a 403 after an allow-list change). Notifications while it is disabled are not sent later.

How to fix it

Fix the cause, then AdobeSign::webhooks()->updateState($webhookId, 'ACTIVE'), and reconcile agreements your records still show as open by asking the API for their status. notifyOnProblems() on the webhook builder makes Adobe email you before it gets that far.

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 a Gov Cloud account against the commercial host), or an OAuth token whose stored api_access_point no longer matches the account.

How to fix it

Leave ADOBE_SIGN_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 the SDK's cache store 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 or GOV_ID_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 has no quota, digital signatures not enabled for AES or QES.

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. See turning exceptions into responses.

NotFoundException right after creating an agreement

NotFoundException

What you see

signingUrlFor() or similar calls throw NotFoundException (AGREEMENT_NOT_SIGNABLE) within seconds of create(); other calls may report AGREEMENT_NOT_EXPOSED.

Why it happens

Adobe is still processing the document; the status is DOCUMENTS_NOT_YET_PROCESSED for a few seconds.

How to fix it

Retry after a short delay, or answer "try again shortly" (the example controller returns 409). In a job, $this->release(5).

RateLimitException

RateLimitException

What you see

RateLimitException, usually during bulk work.

Why it happens

Adobe throttled the account and asked for a wait longer than retries.max_delay_seconds, or retries ran out.

How to fix it

Do bulk work in queued jobs and release them for $e->retryAfter seconds. Spread schedules out; don't raise max_delay_seconds for web requests.

Production and performance

Adobe reports webhook timeouts

What you see

Deliveries are retried even though your code ran.

Why it happens

The whole request took more than 5 seconds, usually because a listener isn't queued, or QUEUE_CONNECTION=sync in production.

How to fix it

Implement ShouldQueue on every listener that does real work and use a real queue connection.

Octane or workers use old settings

What you see

A changed key or level only applies to some requests.

Why it happens

Long-running processes loaded config at start. The manager itself is rebuilt per request and per job, but from the config the process already has.

How to fix it

php artisan octane:reload and php artisan queue:restart after changing config, as part of every deploy.

Common questions

How can I see what the SDK sends?

Set ADOBE_SIGN_LOG_CHANNEL to a channel at debug level to log requests and retries (never headers or bodies). To see full requests during development, bind a Guzzle client with a logging or history middleware as the PSR-18 client; never do that in production, since bodies 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, and the token model hides tokens from serialisation. Your own code can still leak them if it logs config('adobe-sign') wholesale.

Why do agreement names start with [DEMO USE ONLY]?

Adobe adds that prefix to agreements created from developer accounts. It disappears on a paid production account; nothing in your code causes it.

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 and what you expected. Leave out keys, tokens and personal data.