Help
Troubleshooting
Find the symptom, read the cause, apply the fix. Most entries come from running the SDK against a real
Acrobat Sign account, where the API sometimes behaves differently from its specification. If your
problem isn't here, attach a logger, look at $e->errorCode, and check
Errors & exceptions.
First, get the facts
Almost every problem is easier with three pieces of information: the exception class, its
errorCode, and the request path in its message. A PSR-3 logger on the client adds the
status and timing of every call. None of these contain your credentials, so they are safe to paste into
an issue.
Find your problem
35 problems in 10 groups. Type an error code, an exception name or a word from the message to narrow them down, or pick one from the index.
Installation and setup
No PSR-18 client found
What you see
composer require conceptlz/adobe-sign fails because nothing provides psr/http-client-implementation, or building the client throws Http\Discovery\Exception\NotFoundException: "No PSR-18 clients found".
Why it happens
The SDK depends on the PSR-18 and PSR-17 interfaces, not on a specific HTTP library, so your project has to provide one. Discovery can only find a client that is installed.
How to fix it
Install a client and PSR-17 factories, then require the SDK again:
composer require symfony/http-client nyholm/psr7
# or
composer require guzzlehttp/guzzle
Or skip discovery and pass httpClient, requestFactory and streamFactory yourself (Choosing a PSR-18 client). This exception comes from php-http/discovery, not the SDK, so catch (AdobeSignException) doesn't cover it; it happens once at boot.
Composer refuses the package
What you see
Composer reports that conceptlz/adobe-sign requires php ^8.2.
Why it happens
The SDK targets PHP 8.2 and later (it relies on features such as #[\SensitiveParameter]) and is tested on PHP 8.2 to 8.5.
How to fix it
Upgrade PHP, and check that config.platform.php in your composer.json, if set, says 8.2 or later.
Authentication
401 INVALID_ACCESS_TOKEN
What you see
AuthenticationException with errorCode INVALID_ACCESS_TOKEN. With an Integration Key it often happens on the very first call, GET /api/rest/v6/baseUris.
Why it happens
Adobe doesn't accept the token: the Integration Key is mistyped, was revoked or belongs to a deactivated user; or an OAuth access token expired and the refresh didn't produce a working one.
How to fix it
- Integration Key: copy it again from Acrobat Sign (Account → Acrobat Sign API → API Information → Integration Key), with no surrounding spaces or quotes. Check that the user it belongs to is still active.
- OAuth: the SDK already refreshed once and retried. If it still fails, the user's grant is gone; send them through authorization again.
- Don't retry in a loop: a rejected credential stays rejected.
OAuthException NO_TOKEN or NO_REFRESH_TOKEN
What you see
OAuthException with errorCode NO_TOKEN ("No OAuth token stored for …") or NO_REFRESH_TOKEN.
Why it happens
These codes are set by the SDK, not Adobe. NO_TOKEN: your TokenStore has nothing under the key you passed to withOAuth(). NO_REFRESH_TOKEN: the stored token expired and has no refresh token to renew it with.
How to fix it
Check that you put() and get() with the same key, and that your store round-trips tokens with AccessToken::toArray() and fromArray() without losing refreshToken. Otherwise, the user has to connect again.
OAuth users are disconnected after a while
What you see
Users who connected weeks ago start failing with an OAuthException such as INVALID_TOKEN or EXPIRED_TOKEN during refresh.
Why it happens
Adobe expires refresh tokens after 60 days without use, and a user or admin can revoke access at any time.
How to fix it
Catch OAuthException, mark the connection as broken, and ask the user to reconnect. To keep dormant connections alive, use them (any call refreshes as needed) within the 60 days.
STATE_MISMATCH on the OAuth callback
What you see
handleCallback() throws OAuthException with STATE_MISMATCH.
Why it happens
The state in the callback doesn't match the one you stored when sending the user to Adobe. Usually the session was lost between the two requests (a different domain or port, a cookie with SameSite=Strict, a new session), the user used the back button, or the callback really is forged.
How to fix it
Keep the state in the user's session from OAuthClient::generateState(), make sure the redirect URI is on the same host as the page that started the flow, and use a session cookie that survives a top-level redirect (SameSite=Lax). Don't skip the check.
InvalidArgumentException about api_access_point
What you see
An InvalidArgumentException mentioning the access point during the OAuth callback or a refresh.
Why it happens
The api_access_point isn't HTTPS on one of OAuthConfig::$allowedDomains (adobesign.com, echosign.com, adobesigngov.us). The SDK refuses to send your client secret to any other host.
How to fix it
If someone tampered with the callback URL, this is the SDK doing its job. If your account genuinely uses another Adobe domain, add it to allowedDomains in OAuthConfig.
asUser() is rejected
What you see
AuthenticationException with INVALID_X_API_USER_HEADER or INVALID_ON_BEHALF_OF_USER, even though the same client works without asUser().
Why it happens
The person isn't a user in the token's account, the address is mistyped, or the token isn't an account admin's.
How to fix it
Check the address with $sign->users()->all(), and use an Integration Key or OAuth token that belongs to an account administrator.
Data centres
403 INVALID_API_ACCESS_POINT
What you see
ApiException with status 403 and errorCode INVALID_API_ACCESS_POINT.
Why it happens
The request went to a data centre that doesn't hold the account. The SDK already handled the common case: it dropped the cached data centre, rediscovered it and retried once. Seeing the exception means the retry failed too, typically because the bootstrap host is wrong for this account (a Government environment with the default bootstrapUrl), or because a custom middleware rewrites hosts.
How to fix it
Check ClientOptions::$bootstrapUrl and any middleware that touches the URI. Clearing the shared cache does no harm. See Rediscovery.
GET /baseUris on every request
What you see
Every web request or job makes an extra call to api.adobesign.com before its real work.
Why it happens
No shared cache was passed, so each new client discovers its data centre again, and in PHP-FPM every request builds a new client.
How to fix it
Pass a shared PSR-16 cache (Redis, APCu, files) as cache:. See PSR-16 cache.
Updates and cancellations
412 on update or cancel
What you see
PreconditionFailedException (RESOURCE_MODIFIED) from update(), updateState() or cancel().
Why it happens
The ETag sent in If-Match is older than the resource. Either someone else changed it since your get(), or, very often, you are within a few seconds of sending the agreement: Adobe keeps updating it in the background while it delivers emails, so an ETag read in that window is already stale.
How to fix it
Fetch again and repeat, once or twice with a short pause:
try {
$sign->agreements()->cancel($agreementId, 'Sent in error');
} catch (PreconditionFailedException) {
sleep(3);
$sign->agreements()->get($agreementId); // remembers the current ETag
$sign->agreements()->cancel($agreementId, 'Sent in error');
}
If you are on 0.2.0 and see this on a cancel() right after another state change from the same client, upgrade: 0.2.1 fixed a case where the ETag from a state change was reused. How the ETag memory works: ETags and If-Match.
403 PERMISSION_DENIED
What you see
ApiException with status 403 and PERMISSION_DENIED.
Why it happens
The user behind the token may not see or change this resource: someone else's agreement, or an operation reserved for the sender or an admin. For OAuth, the token may also lack the scope (for example agreement_write).
How to fix it
Act as the right user with asUser() (admin token required), or request the missing scope and have the user reconnect.
Rate limits and slowness
RateLimitException or frequent 429s
What you see
RateLimitException, or warnings "Adobe Sign throttled request, retrying" in your log.
Why it happens
Your account made more calls than Adobe allows in the current period. The SDK retried (default three times) and either ran out of retries or was asked to wait longer than maxRetryDelay.
How to fix it
- Defer the work by
$e->retryAfterseconds (a delayed queue job is ideal) rather than retrying immediately. - Make fewer calls: use webhooks instead of polling status, raise
pageSizeon lists, share the data-centre cache, and reuse fetched data within a request. - Move bulk operations to background jobs so a burst doesn't hit user-facing requests.
A call takes a long time before failing
What you see
A page or command hangs for tens of seconds, then fails or succeeds.
Why it happens
Either the retry middleware is sleeping between attempts (up to maxRetryDelay, 60 s by default, per retry), or the HTTP client has no timeout and is waiting on the network.
How to fix it
In web requests, use new ClientOptions(maxRetries: 1, maxRetryDelay: 5.0) and move slow work to jobs. Set a timeout on your PSR-18 client; the SDK sets none. A logger shows which it is: retry warnings, or one request with a long duration_ms.
Right after creating an agreement
Status is DOCUMENTS_NOT_YET_PROCESSED
What you see
status() returns DOCUMENTS_NOT_YET_PROCESSED instead of OUT_FOR_SIGNATURE.
Why it happens
Adobe converts and prepares documents asynchronously after create(). That usually takes a few seconds, longer for big files.
How to fix it
Nothing is wrong. Poll every couple of seconds until the status changes, or better, subscribe to a webhook and react when the agreement is out for signature. The example scripts poll every 2 seconds, 10 to 15 times.
AGREEMENT_NOT_EXPOSED or AGREEMENT_NOT_SIGNABLE
What you see
signingUrls() or signingUrlFor() throws NotFoundException with one of these codes for an agreement you just created.
Why it happens
The agreement isn't ready to be signed yet, because the document is still processing, or nobody can sign at this moment.
How to fix it
Wait until the status is OUT_FOR_SIGNATURE, then retry after a few seconds. In a web flow, show "preparing your document" and retry from the browser rather than blocking the request.
nextParticipantSets is missing or empty
What you see
members($id) has no nextParticipantSets, so you can't tell whose turn it is.
Why it happens
Two things. The live API leaves the key out unless you ask for it, even though the spec lists it. And for a moment after the status turns OUT_FOR_SIGNATURE, participant data can still lag (sets reading NOT_YET_VISIBLE).
How to fix it
Call members($id, includeNextParticipantSet: true), and if the list is empty right after sending, poll again a few seconds later. Each participant set's own status (WAITING_FOR_MY_SIGNATURE, …) is another way to find the current step.
sendReminder() says nobody is left to remind
What you see
InvalidArgumentException from sendReminder($id) without participant ids.
Why it happens
Without ids, the SDK reminds whoever's turn it is, and found nobody: the agreement is completed or cancelled, or still processing. Before 0.2.1 this also happened on active agreements, because nextParticipantSets wasn't requested.
How to fix it
Check the status first, and upgrade if you are on 0.2.0. To remind specific people, pass their participant ids.
The transient document id is rejected
What you see
create() fails with a ValidationException such as INVALID_FILE_INFO for an id you uploaded earlier.
Why it happens
Transient documents expire 7 days after upload, and an id belongs to the account (and user) that uploaded it.
How to fix it
Upload again right before creating the agreement. For documents you reuse, create a library document and reference it with FileInfo::libraryDocument().
Account settings and plan
403 HYBRID_ROUTING_NOT_ALLOWED
What you see
AccountCapabilityException with HYBRID_ROUTING_NOT_ALLOWED when creating an agreement.
Why it happens
The signing order mixes sequential and parallel steps, for example 1, then 2 and 2 together, then 3. Adobe calls this hybrid routing and it is an account setting, off on some accounts.
How to fix it
An Acrobat Sign administrator can enable it in the account's send settings. Until then, use a purely sequential (1, 2, 3) or purely parallel (1, 1, 1) order. Versions before 0.3.0 reported this as a plain ApiException. See Signing order.
"Not allowed by account settings"
What you see
AccountCapabilityException whose message contains "not allowed by account settings", "enabled for your account" or "not enabled for this account", often with Adobe's generic BAD_REQUEST code.
Why it happens
The request is valid, but the account doesn't enable the feature: typically an identity method (phone, KBA, government ID, a digital identity provider) or digital signatures. Adobe reports some of these only as a 400, and the SDK recognises them by their wording.
How to fix it
Ask an Acrobat Sign administrator to enable the method or feature, or check that your plan includes it. It can't be fixed in code; choosing another method can work around it.
GOV_ID_LIMIT_EXCEEDED
What you see
AccountCapabilityException with GOV_ID_LIMIT_EXCEEDED when a signer must verify with a government ID.
Why it happens
Government-ID verification uses a paid quota, and the account has none left or never had any. Developer accounts usually have none.
How to fix it
Buy or top up the quota through Adobe, or use another identity check, such as Verification::emailOtp(), for this agreement.
KBA_LIMIT_EXCEEDED, PHONE_LIMIT_EXCEEDED, SEND_LIMIT_EXCEEDED
What you see
AccountCapabilityException with KBA_LIMIT_EXCEEDED, PHONE_LIMIT_EXCEEDED, PHONE_AUTH_LIMIT_EXCEEDED, AGREEMENT_PHONE_AUTH_LIMIT_EXCEEDED or SEND_LIMIT_EXCEEDED, usually when creating or sending an agreement. Versions up to 0.3.1 reported these as a plain ApiException.
Why it happens
KBA, phone (SMS) checks and agreement sending are metered, and the account or user has used up its quota.
How to fix it
Buy or top up the quota through Adobe. For the identity checks, another method such as Verification::emailOtp() works around it for this agreement.
DIGITAL_SIGNATURE_ENFORCEMENT_PER_PARTICIPANT_NOT_ALLOWED
What you see
Creating an AES or QES agreement fails with AccountCapabilityException and DIGITAL_SIGNATURE_ENFORCEMENT_PER_PARTICIPANT_NOT_ALLOWED or …_DISABLED. Versions up to 0.3.1 reported these as a plain ApiException.
Why it happens
AES and QES send isDigitalSignatureRequired for the signer, and the sender's account or group settings don't let senders require a digital signature per signer.
How to fix it
Ask an Acrobat Sign administrator to allow digital signatures and per-recipient enforcement, with a cloud signature provider configured, or send at SES. See Signature levels.
[DEMO USE ONLY] in agreement names
What you see
Agreement names come back as [DEMO USE ONLY] Master Services Agreement.
Why it happens
The agreement was created on a free developer account, and Adobe prefixes every agreement name there.
How to fix it
Expected behaviour. Production accounts don't do this. If you compare names in tests, strip the prefix or compare externalId instead.
Signature levels and evidence
signingProvider is empty on digital signatures
What you see
signatureEvidence() reports digitallySigned: true and a certificateIssuer, but SignerEvidence::$signingProvider is null.
Why it happens
In 0.3.0 the SDK read the spec's digitalSignatureInfo.cloudProviderName; the live API sends providerName.
How to fix it
Upgrade to 0.3.1 or later, which reads both.
Evidence says SES although you asked for AES
What you see
shortfalls(SignatureLevel::AES) says a signer "signed with an electronic signature … not a digital one".
Why it happens
Check $signer->digitalSignatureRequired. If it is false, the requirement never reached Adobe: the agreement was sent as a raw array or generated model (the default level applies only to AgreementInfo), or the level was set on a different participant set. If it is true, compare with the agreement's events: the signature may have been completed in a way Adobe records differently.
How to fix it
Build agreements with AgreementInfo and set the level there or in ClientOptions; for raw payloads, set isDigitalSignatureRequired: true on each signer set yourself. See Signature levels.
QES check throws InvalidArgumentException
What you see
meets(SignatureLevel::QES) or shortfalls(SignatureLevel::QES) throws.
Why it happens
Whether a certificate is qualified depends on who issued it, which the SDK can't know. Checking for QES without the list of issuers you accept as qualified would be meaningless.
How to fix it
Pass your qualified issuers: $evidence->meets(SignatureLevel::QES, ['DigiCert', 'TrustPro QTSP']). In Laravel, set ADOBE_SIGN_QUALIFIED_ISSUERS.
InvalidArgumentException when setting a level
What you see
The builder throws before anything is sent: a set "does not sign, so it cannot be held to AES", or a digital level combined with written signatures.
Why it happens
AES and QES are properties of a signature. Approvers, acceptors and form fillers don't sign, and a written (wet ink) signature can't carry a digital certificate.
How to fix it
Set the level only on signer sets, and keep signatureType at ESIGN for digital levels.
Webhooks
Creating a webhook fails verification
What you see
webhooks()->create() fails, with Adobe's message saying the URL couldn't be verified or reached.
Why it happens
Before creating the webhook, Adobe sends a GET to your URL with an X-AdobeSign-ClientId header and expects a 2xx answer that echoes the value within 5 seconds. The URL isn't public HTTPS, the endpoint rejected the client id, or it didn't echo.
How to fix it
Make the URL reachable over HTTPS (a tunnel for local development), allowlist the right client id in WebhookHandler, and return the handler's response unchanged. Test the endpoint first with a curl GET carrying the header (example).
Notifications stopped arriving
What you see
Events used to arrive and then stopped; webhooks()->get($id) shows the webhook is no longer active.
Why it happens
Adobe retries a failed delivery with growing intervals for 72 hours, then disables the webhook. Failures include non-2xx answers, a missing echo, and answers slower than 5 seconds.
How to fix it
Fix the endpoint first: acknowledge immediately and do slow work on a queue. Then reactivate with $sign->webhooks()->updateState($id, 'ACTIVE'). Notifications from the disabled period are lost, so reconcile by fetching the affected agreements. One webhook per agreement limits the damage from a failure to that agreement.
Your endpoint answers 403
What you see
WebhookHandler::handle() returns a 403 and $result->error says the client id is unexpected or missing.
Why it happens
The X-AdobeSign-ClientId header doesn't match the allowlist, often because the webhook was created by a different API application than the one whose id you configured.
How to fix it
Allowlist the client id of every application that creates your webhooks (the handler accepts a list). Requests without the header aren't from Adobe.
The same event arrives twice
What you see
Your handler processes one signature twice.
Why it happens
Adobe retries deliveries it considers failed, including slow ones, so at-least-once delivery is normal.
How to fix it
Store $event->notificationId and skip ones you have seen, and make your processing idempotent (re-fetch the agreement and act on its current status).
Downloads
The saved PDF is empty
What you see
The file you wrote has zero bytes, or getContents() returns an empty string.
Why it happens
A PSR-7 stream has a read position. If something already read it (a debug dump, a size check), the position is at the end.
How to fix it
Read it once, or call $stream->rewind() first when isSeekable() is true. Write with a chunked loop or detach() as shown in Downloads.
Memory runs out on large downloads
What you see
"Allowed memory size exhausted" while saving a combined PDF.
Why it happens
Converting the stream to a string ((string) $stream, getContents()) loads the whole file into memory.
How to fix it
Copy in chunks (read(65536) in a loop) or pass detach() to your storage layer. Also check the PSR-18 client: some buffer whole responses unless configured to stream.
Common questions
What should I include when reporting a bug?
The SDK version (AdobeSign::VERSION), the PHP version, the exception class, its
errorCode and message, and Adobe's response body ((string) $e->response?->getBody()).
Remove personal data such as signer emails. Never include your Integration Key, tokens or client secret;
the SDK keeps them out of exceptions, so you shouldn't have to.
Is there a sandbox environment?
Adobe's free developer account serves that purpose for most teams: it is a real account that sends real
emails, with agreement names marked [DEMO USE ONLY], meant for building and testing. Ask your
Adobe contact whether your plan includes a separate sandbox environment.
It works on my machine but not on the server. Where do I look?
Usually the environment: a different key or account, outbound HTTPS blocked by a firewall or proxy (you
will see TransportException), an old CA bundle breaking TLS, or a missing PSR-18 package
because dev dependencies weren't installed. A logger on the client shows quickly whether requests leave
the server at all.
The API returned something the spec doesn't describe. Is that a bug?
Sometimes it is just Adobe's API: several such differences are known and handled (missing
nextParticipantSets, providerName versus cloudProviderName). The
generated models ignore unknown fields, and $raw holds the full response. If the SDK
mishandles it, please report it with the response body.