Adobe Sign PHP SDKconceptlz/adobe-sign GitHub

Help

Frequently asked questions

Short answers first, detail after, and a link to the page that covers each topic in depth. The questions are grouped by theme; every one has its own link, so you can send a colleague straight to the answer.

General & licensing

What is conceptlz/adobe-sign?

A PHP SDK for the Adobe Acrobat Sign REST API v6. It uploads documents, sends them for signature in any signing order, lets you require SES, AES or QES, receives webhooks and downloads the signed PDF.

It also takes care of the parts that are easy to get wrong in production: finding the account's data centre, retrying rate-limited calls, sending ETags on updates, streaming large files and mapping errors to typed exceptions. Start with Getting started.

Is this an official Adobe SDK?

No. It is developed independently and is not affiliated with or endorsed by Adobe. It uses Adobe's public REST API and Adobe's published OpenAPI specification. Questions about your Acrobat Sign account, plan or contract go to Adobe; questions about this SDK go to its issue tracker.

What licence is it under?

MIT. You can use it in commercial and closed-source applications; keep the copyright notice. The licence covers the SDK only: using Acrobat Sign itself is governed by your agreement with Adobe.

Which PHP versions are supported?

PHP 8.2 and later. CI runs the test suite on PHP 8.2, 8.3, 8.4 and 8.5, plus a run with the lowest allowed dependency versions.

Do I need a framework?

No. The core package depends only on PSR interfaces (HTTP client and factories, cache, logger, clock) and php-http/discovery, and a test fails the build if a framework import ever creeps in. It works in Symfony, Slim, WordPress plugins, CLI tools or plain PHP. Laravel users can add the bridge package (Laravel & frameworks).

Do I need a paid Acrobat Sign plan?

Not to build and test: a free Acrobat Sign developer account is enough. For production you need an Acrobat Sign plan with API access. Some features depend on the plan or on account settings, including digital signatures, government-ID checks (a paid quota) and mixed signing orders. When a feature is missing, the SDK throws AccountCapabilityException rather than a vague error (Errors & exceptions).

Accounts & authentication

Should I use an Integration Key or OAuth?

An Integration Key when your application sends from your own Acrobat Sign account; OAuth when your users connect their own accounts.

An Integration Key is one long-lived credential for one account: simple, but treat it like a password. OAuth gives each user a token pair that the SDK refreshes automatically. Both are covered in Authentication.

Where do I find the Integration Key?

In Acrobat Sign, under Account → Acrobat Sign API → API Information → Integration Key. Create one with the scopes your application needs, and store it in your secrets manager or environment, never in code. See Authentication.

Where are OAuth tokens stored?

Wherever you decide. The SDK defines a two-method TokenStore interface (get() and put()) and ships InMemoryTokenStore for scripts and tests.

In a real application, implement it on your database and encrypt the tokens: a refresh token is a long-lived credential. AccessToken::toArray() and fromArray() make storage simple. The Laravel bridge provides an encrypted table for you.

How long do OAuth connections last?

As long as they're used. Access tokens are short-lived and refreshed automatically, 60 seconds before expiry or after a 401. Adobe expires a refresh token after 60 days without use, and the user can revoke access at any time; either way you get an OAuthException and the user must reconnect (Troubleshooting).

Can I send on behalf of another user in my account?

Yes, with an account administrator's credentials: $sign->asUser('jane@example.com') returns a client that sends the x-api-user header on every request. The original client is unchanged, and copies are cheap. See Authentication.

Do I need to know which data centre my account is on?

No. The SDK asks Adobe once (GET /baseUris) and caches the answer; OAuth tokens already carry it. If the account moves, the SDK notices the INVALID_API_ACCESS_POINT error and looks again. Pass a shared PSR-16 cache so the lookup isn't repeated on every request (Data-centre discovery).

Sending & signing order

How do I make people sign one after another?

Give each participant set an order number: 1, then 2, then 3. The shortcut ->signers('a@example.com', 'b@example.com', 'c@example.com') does exactly that.

Adobe requires orders to start at 1 with no gaps; the builder checks this before sending. See Signing order.

How do several people sign at the same time?

Give their sets the same order number. Sets sharing a number sign in parallel, in any order. If instead any one person from a group may sign for all of them, put them in one set: ParticipantSetInfo::signer('a@example.com', 'b@example.com') (Signing order).

Why does "client first, then CFO and legal together" fail with HYBRID_ROUTING_NOT_ALLOWED?

Because mixing sequential and parallel steps, which Adobe calls hybrid routing, is an account setting and it is off on your account. An Acrobat Sign admin can enable it; until then use a purely sequential or purely parallel order. The SDK reports it as AccountCapabilityException (Troubleshooting).

Can I create an agreement without sending it yet?

Yes: ->state(AgreementState::AUTHORING) creates it without sending, for example so someone can place fields in Adobe's editor. Send it later with agreements()->updateState($id, 'IN_PROCESS'). The default is to send immediately (Creating agreements).

Can people sign inside my application instead of from the email?

Yes. agreements()->signingUrlFor($id, $email) returns that signer's link when it's their turn (or null if it isn't), which you can redirect to or embed. Right after creation Adobe may still be processing, so be ready to retry (Managing agreements).

How do I remind people to sign?

Automatically, with ->reminderFrequency(ReminderFrequency::DAILY_UNTIL_SIGNED) when you create the agreement, or on demand with agreements()->sendReminder($id), which reminds whoever's turn it currently is (Managing agreements).

How do I cancel an agreement that was sent by mistake?

$sign->agreements()->cancel($id, 'Sent to the wrong person'). The comment is shown to participants; pass notifyOthers: false to skip the emails. Within a few seconds of sending you may get a PreconditionFailedException; fetch the agreement and cancel again (Troubleshooting).

Signature levels & legal

What are SES, AES and QES?

The three levels of electronic signature defined by the EU's eIDAS regulation and laws modelled on it.

  • SES (simple): any electronic signature, such as a typed name, a drawn signature or a click. Acrobat Sign's normal e-signature.
  • AES (advanced): uniquely linked to the signer, able to identify them, and detects later changes. In this SDK, AES means a certificate-based digital signature from a cloud signature provider.
  • QES (qualified): an AES made with a qualified certificate from a qualified trust service provider. In the EU it has the same legal effect as a handwritten signature.

See Signature levels for the full explanation.

Which level does the SDK use by default?

SES. Raise it for the whole project with new ClientOptions(defaultSignatureLevel: SignatureLevel::AES), for one agreement with AgreementInfo::signatureLevel(), or for one signer with ParticipantSetInfo::signatureLevel() (Configuration).

Does an email code (OTP) make a signature AES?

No. An email code, SMS, knowledge questions or a government-ID scan check who opens the document; they don't change how it is signed. Identity checks and signature levels are separate settings, and you can combine them, for example AES plus an email code.

A strongly verified SES may still satisfy a particular legal requirement; that is a question for your legal team. What the SDK reports as AES is a certificate-based digital signature (Identity verification).

Is a simple e-signature valid in the EU?

Generally yes. Under eIDAS, an electronic signature can't be refused legal effect or as evidence just because it is electronic. What the levels change is the burden of proof: with SES, the party relying on the signature may have to prove who signed; a QES is treated like a handwritten signature in every member state.

Some documents, and some national rules, require a specific level or exclude e-signatures altogether. Check with your legal team for each document type (Signature levels).

What about countries outside the EU?

Most countries give electronic signatures legal effect, but not all use eIDAS's three tiers. Some are tiered in a similar way (the UK, Switzerland, Brazil, the UAE), some favour certificates from licensed providers (India, Saudi Arabia), and some are technology-neutral with no levels at all (the United States, Australia, most of Canada). In technology-neutral regimes an SES with a good identity check is the norm; elsewhere, use AES or QES with a provider recognised in that country where a document needs it. Beyond the EU sketches each one; confirm the requirement for your documents with local counsel.

Can different signers on one agreement sign at different levels?

Yes. Set a level on the agreement and override it per signer in either direction: a QES borrower and an SES witness on the same loan agreement, for example. Levels apply to signers only; approvers and other non-signing roles can't be given one (Signature levels).

Why does the signer see a list of signature providers?

Because the agreement requires a digital signature (AES or QES). The signer chooses a cloud signature provider, proves their identity to it, and signs with a certificate it issues to them; that certificate is embedded in the PDF. Which providers appear is configured by your Acrobat Sign administrator in the account's digital signature settings, not by the API.

Why isn't my "QES" agreement qualified?

The API request for QES is the same as for AES. What makes a signature qualified is the provider: a qualified trust service provider on the EU Trusted List, issuing a qualified certificate on a qualified device. If the account's configured provider isn't qualified, the result is AES.

Configure a QTSP in Acrobat Sign admin, then check each signature with signatureEvidence() and your list of qualified issuers (Signature levels).

How do I prove which level each person actually signed at?

$sign->agreements()->signatureEvidence($id) reports, per signer, the signing time, input type, whether a digital signature was applied, the certificate issuer, the provider and the identity check. meets() and shortfalls() compare every signer against a required level. For a formal check, validate the signed PDF against the relevant trusted list (Signature levels).

Documents & downloads

Can I upload large PDFs?

Yes, within the limits Adobe sets for your account. The SDK streams uploads through a temporary file rather than loading the document into memory, and a rate-limited upload is retried with the complete file (Streaming uploads and downloads).

How long can I wait between uploading and sending?

Seven days. A transient document expires a week after upload. For a document you send again and again, use a library document (libraryDocuments()->create(), then FileInfo::libraryDocument($id)) (Uploading documents).

How do I get the signed PDF?

agreements()->combinedDocument($id, attachAuditReport: true) once the status is SIGNED. It returns a PSR-7 stream: copy it to disk in chunks or pass detach() to your storage layer. auditTrail($id) gives the audit report on its own (Downloading signed files).

Where is the signer's certificate for an AES signature?

In the signed document itself, from combinedDocument() or document(). The separate audit report carries only Adobe's own seal. Open the PDF's signature panel to see the signer's certificate chain (Downloading signed files).

Can I stream the signed PDF straight to S3 or another store?

Yes. $stream->detach() gives you a PHP stream resource that Flysystem, Laravel Storage or the AWS SDK accept directly, so the file never needs to sit in memory (Downloading signed files).

Webhooks

Are Adobe's webhooks signed?

No. Adobe doesn't sign payloads, so WebhookHandler accepts only requests whose X-AdobeSign-ClientId is on your allowlist. Treat the payload as a hint: re-fetch the agreement through the API before acting on it (Webhooks).

How do I receive webhooks on my laptop?

Adobe only calls public HTTPS URLs, so put a tunnel such as ngrok or cloudflared in front of your local server. examples/webhook-receiver.php is a ready-made endpoint, and you can post a sample notification to it with curl (Example scripts).

One webhook per agreement, or one for the whole account?

Adobe recommends one per agreement. If deliveries keep failing, Adobe disables the webhook, and with one per agreement that only affects one agreement. WebhookInfo::forAgreement() builds it; there are also account, group, user and resource variants (Webhooks).

Why did my webhook stop?

Adobe retries failed deliveries for 72 hours, then disables the webhook. A delivery fails if your endpoint doesn't answer 2xx with the client id echo within 5 seconds. Acknowledge first, queue the work, and reactivate with webhooks()->updateState($id, 'ACTIVE') (Troubleshooting).

Errors & reliability

How do I catch every error from the SDK?

catch (\Conceptlz\AdobeSign\Exception\AdobeSignException $e). Every exception the SDK throws implements it. Catch narrower types first when you want to treat them differently (Recommended catch patterns).

What does PreconditionFailedException mean?

The agreement changed after you read it, so Adobe refused your write rather than overwrite someone else's change. Call get() again and repeat the update. It is common for a few seconds right after sending (ETags and If-Match).

What happens when I hit Adobe's rate limit?

The SDK waits as long as Adobe asks and retries, three times by default. If that isn't enough, or Adobe asks for a wait longer than maxRetryDelay, you get a RateLimitException with $retryAfter in seconds (Retries and rate limits).

Can my Integration Key end up in logs or error trackers?

Not through the SDK. Exceptions replace the Authorization header with Bearer ***, OAuth exceptions drop request bodies, credential objects mask themselves in dumps, and the logger never receives headers or bodies. A test enforces all of this (Credential redaction).

Laravel & frameworks

Is there a Laravel package?

Yes: conceptlz/adobe-sign-laravel, for Laravel 12 and 13. It adds configuration through ADOBE_SIGN_* environment variables, a facade, OAuth routes with an encrypted token table, a webhook route with duplicate suppression, and events. It has its own documentation.

How do I use it with Symfony?

Register AdobeSign as a service built with a factory such as AdobeSign::withIntegrationKey(), and inject Symfony's Psr18Client, a Psr16Cache and the logger. There is no bundle; the PSR interfaces make one unnecessary (Configuration).

Can I use one client in a long-running worker?

Yes. The client keeps little state: the data-centre lookup (in your cache) and up to 500 remembered ETags. OAuth tokens refresh themselves. For a worker serving many OAuth users, build one client per user per job rather than keeping them all forever (Configuration recipes).

Contributing & versioning

How is the SDK versioned?

With semantic versioning, currently in the 0.x series, where a minor version (0.4 to 0.5) may contain breaking changes. Require ^0.4 to get fixes without surprises. The core and Laravel packages always share one version number and are released together; the changes are listed in the changelog on GitHub.

What if the SDK doesn't cover a field or endpoint I need?

For a missing field, use the builder's with() to add raw spec fields, or start from a generated model, which covers the whole spec. For a missing endpoint, open an issue; adding one follows a short, well defined recipe (Models & raw data).

Where do I report a bug or ask for a feature?

On the GitHub issue tracker. Include the SDK and PHP versions, the exception class, errorCode and Adobe's response body, never credentials (what to include). The package repositories are mirrors of a development monorepo, so discuss larger changes in an issue before writing code.

Can I fix a bug in a generated model?

Not by editing it. The 313 models are regenerated from Adobe's spec with composer generate; fixes go into the spec normaliser, and CI fails if the generated code drifts (Regenerating the models).