Start here
Getting started
By the end of this page you will have the SDK installed, a free Acrobat Sign developer account connected, and a real agreement that has gone out for signature, been signed and been saved to disk as a PDF. Allow about fifteen minutes, most of it spent in Adobe's sign-up pages.
What you need
- PHP 8.2 or newerWith
ext-json, which every normal PHP build has. The SDK is tested on PHP 8.2 to 8.5. - ComposerThe SDK is a Composer package; there is no PHAR or manual download.
- A PSR-18 HTTP clientAny client that implements the PHP-FIG HTTP client standard, plus PSR-17 factories. Most projects already have one; see below if yours doesn't.
- An Acrobat Sign accountA free developer account is enough for everything on this page. You will create one in a moment.
- A PDF and an inboxAny small PDF, and an email address you can read. Acrobat Sign sends the signing request by email, so use an address you control.
Install the SDK
composer require conceptlz/adobe-sign
The package itself depends only on PSR interfaces (HTTP client, message factories, cache, logger,
clock) and php-http/discovery. It ships no HTTP client of its own. That is deliberate:
your application probably has one already, and two clients with different proxy, timeout and TLS
settings is a common source of "works on my machine" bugs.
Choose an HTTP client
When you build a client, the SDK asks php-http/discovery for a PSR-18 client and PSR-17
factories that are already installed. If Composer can't find any, install one of these pairs:
| Option | Install | Pick it when |
|---|---|---|
| Symfony HttpClient + Nyholm PSR-7 | composer require symfony/http-client nyholm/psr7 | You have no preference, or you use Symfony. It is lightweight and what the SDK's own tests use. |
| Guzzle 7 | composer require guzzlehttp/guzzle | Guzzle is already in your project (every Laravel and many WordPress or Drupal installs have it). Guzzle 7 brings its own PSR-17 factories. |
| Anything else PSR-18 | Your choice | Buzz, the PHP-HTTP curl client and others work the same way, through discovery or injection. |
Automatic discovery is convenient, but in production it is better to pass the client in yourself, so
you control timeouts and proxies and nothing changes when another package adds a second client to
vendor/. Every named argument of the AdobeSign constructor can be passed
through withIntegrationKey():
use Conceptlz\AdobeSign\AdobeSign;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Psr18Client;
// Psr18Client is also a PSR-17 request and stream factory.
$http = new Psr18Client(HttpClient::create(['timeout' => 30]));
$sign = AdobeSign::withIntegrationKey(
(string) getenv('ADOBE_SIGN_INTEGRATION_KEY'),
httpClient: $http,
requestFactory: $http,
streamFactory: $http,
);
use Conceptlz\AdobeSign\AdobeSign;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
$factory = new HttpFactory();
$sign = AdobeSign::withIntegrationKey(
(string) getenv('ADOBE_SIGN_INTEGRATION_KEY'),
httpClient: new Client(['timeout' => 30, 'connect_timeout' => 10]),
requestFactory: $factory,
streamFactory: $factory,
);
Composer may ask about a plugin
php-http/discovery contains an optional Composer plugin, so Composer may ask whether to
allow it. Either answer works. The plugin only helps install a missing client at install time;
finding an installed client at runtime works without it.
Set a timeout
The SDK retries rate-limited requests itself, but it never sets a network timeout. That belongs to your HTTP client. Thirty seconds is a sensible ceiling for normal calls; give large uploads and downloads more room.
Get a free developer account
Adobe offers free Acrobat Sign developer accounts for building and testing integrations. They have the full API and send real emails. What you send from them is marked for demonstration only, so you can't use one for real contracts.
-
Sign up
Open Adobe's Acrobat Sign developer guide and follow the link to create a developer account. Use an email address that is not already tied to a paid Acrobat Sign account, because one address can belong to only one account.
-
Confirm your email and set a password
Adobe sends an activation email. Follow it, then log in to the Acrobat Sign web app.
-
Note your data centre (optional)
The address bar shows a host such as
secure.na4.adobesign.com. Thena4part is the data centre (Adobe calls it a shard) your account lives in. You don't need to configure it: the SDK finds it on the first call. It is useful to know when you read logs.
Developer agreements are not for real contracts
Adobe prefixes the name of every agreement sent from a developer account with
[DEMO USE ONLY]. Treat everything signed there as test data. For production you need a
paid Acrobat Sign plan; the code stays the same, only the key changes.
Create an Integration Key
An Integration Key is a long-lived API credential for your own Acrobat Sign account. It is the quickest way to start, and the right choice whenever your application sends from one company account. If your users will connect their own Acrobat Sign accounts, you need OAuth instead; the authentication guide explains the difference.
-
Open the API settings
In the Acrobat Sign web app, open Account, then Acrobat Sign API, then API Information. Adobe rearranges its menus from time to time; if you don't see this, search Adobe's help for "Integration Key".
-
Create the key
Choose Integration Key, give it a name you'll recognise later (for example "Contracts app, staging"), and enable the scopes your code needs. For this page, enable the agreement read and write scopes; enable the webhook scopes too if you plan to use webhooks.
-
Copy it into your environment
Adobe then lists the key under your access tokens, where you can reveal and copy it. Put it in an environment variable or your secrets manager, never in source control.
ADOBE_SIGN_INTEGRATION_KEY=3AAABLblqZhB...your-key...
The key is as powerful as your login
Anyone with the key can send, read and cancel agreements as you, and it never expires on its own. Keep
it out of logs and front-end code. If it leaks, revoke it in the same place you created it and issue
a new one. The SDK helps: it never logs the key, masks it in var_dump(), and replaces it
with Bearer *** in every exception.
Check the connection
Before sending anything, make one harmless call to prove the key, the HTTP client and the network all work:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Conceptlz\AdobeSign\AdobeSign;
$sign = AdobeSign::withIntegrationKey((string) getenv('ADOBE_SIGN_INTEGRATION_KEY'));
$me = $sign->users()->me();
printf("Connected as %s (account admin: %s)\n", $me->getEmail(), $me->getIsAccountAdmin() ? 'yes' : 'no');
ADOBE_SIGN_INTEGRATION_KEY="$(grep '^ADOBE_SIGN_INTEGRATION_KEY=' .env | cut -d= -f2-)" php whoami.php
Behind that one line, the SDK makes two requests. First GET /baseUris
on the shared host api.adobesign.com, which answers with your account's data centre. Then
GET /users/me on that data centre. The data centre is cached, so later
calls skip the first request. With the default in-process cache that lasts for one PHP process; in a web
application, pass a shared PSR-16 cache (see configuration) so every
request benefits.
| If you see | It means |
|---|---|
InvalidArgumentException: Integration key must not be empty. | The environment variable isn't set in this shell. |
AuthenticationException with INVALID_ACCESS_TOKEN | The key is wrong, revoked, or copied with extra characters. |
TransportException | No response at all: DNS, proxy, firewall or TLS. Check that the server can reach api.adobesign.com on port 443. |
An exception from Http\Discovery saying no implementation could be found | No HTTP client is installed. Install one of the pairs above. |
Your first agreement, end to end
An agreement is Acrobat Sign's word for "a set of documents sent to some people to sign". Sending one takes two API calls: upload the file, then create the agreement that points at it. The script below does that, waits for you to sign, and saves the result.
-
Upload the PDF
transientDocuments()->upload()streams the file to Adobe and returns a transient document id. Transient means temporary: the id is valid for seven days, which is plenty of time to use it in the next call. More in Uploading documents. -
Create and send the agreement
AgreementInfois a builder that checks your input before anything is sent: at least one file, at least one participant, valid emails, a valid signing order. The default state isIN_PROCESS, which means "send now". More in Creating agreements. -
Wait for Adobe to process the document
For the first few seconds the status is
DOCUMENTS_NOT_YET_PROCESSEDwhile Adobe converts and prepares the file. Then it becomesOUT_FOR_SIGNATUREand the signing email goes out. -
Sign it
Open the email from Adobe and sign. On a real project, the signers are other people and you would learn about progress through webhooks instead of waiting in a loop.
-
Download the signed PDF
Once the status is
SIGNED,combinedDocument()returns all documents merged into one PDF, optionally with Adobe's audit report appended. It comes back as a stream, so it can go straight to disk.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Conceptlz\AdobeSign\AdobeSign;
use Conceptlz\AdobeSign\Builder\AgreementInfo;
use Conceptlz\AdobeSign\Builder\FileInfo;
use Conceptlz\AdobeSign\Builder\ParticipantSetInfo;
use Conceptlz\AdobeSign\Exception\AdobeSignException;
[$script, $pdfPath, $signerEmail] = $argv + [null, null, null];
if ($pdfPath === null || $signerEmail === null) {
exit("Usage: php first-agreement.php contract.pdf you@example.com\n");
}
$sign = AdobeSign::withIntegrationKey((string) getenv('ADOBE_SIGN_INTEGRATION_KEY'));
try {
// 1. Upload: returns a transientDocumentId, valid for 7 days
$documentId = $sign->transientDocuments()->upload($pdfPath);
echo "Uploaded: $documentId\n";
// 2. Create the agreement; state IN_PROCESS (the default) sends it immediately
$agreementId = $sign->agreements()->create(
AgreementInfo::create('My first agreement')
->file(FileInfo::transientDocument($documentId))
->participantSet(ParticipantSetInfo::signer($signerEmail))
->message('Testing the conceptlz/adobe-sign SDK. Please sign.'),
);
echo "Created: $agreementId\n";
// 3 and 4. Poll until signed (fine for a demo; use webhooks in production)
$deadline = time() + 15 * 60;
do {
$status = $sign->agreements()->status($agreementId);
echo date('H:i:s') . " status: $status\n";
if (in_array($status, ['CANCELLED', 'EXPIRED'], true) || time() > $deadline) {
exit("Stopped waiting.\n");
}
if ($status !== 'SIGNED') {
sleep($status === 'DOCUMENTS_NOT_YET_PROCESSED' ? 2 : 15);
}
} while ($status !== 'SIGNED');
// 5. Download the signed PDF, with the audit report appended
$pdf = $sign->agreements()->combinedDocument($agreementId, attachAuditReport: true);
$out = fopen(__DIR__ . "/signed-$agreementId.pdf", 'wb');
while (!$pdf->eof()) {
fwrite($out, $pdf->read(65536));
}
fclose($out);
echo "Saved signed-$agreementId.pdf\n";
} catch (AdobeSignException $e) {
// Every SDK exception implements this interface; credentials are already redacted
fwrite(STDERR, $e::class . ': ' . $e->getMessage() . "\n");
exit(1);
}
php first-agreement.php contract.pdf you@example.com
Uploaded: CBSCTBABDUAAABACAABAA...
Created: CBJCHBCAABAA...
14:02:11 status: DOCUMENTS_NOT_YET_PROCESSED
14:02:13 status: OUT_FOR_SIGNATURE
14:02:28 status: OUT_FOR_SIGNATURE
14:02:43 status: SIGNED
Saved signed-CBJCHBCAABAA....pdf
In your inbox, the agreement is called [DEMO USE ONLY] My first agreement: that is the
developer-account prefix, not something the SDK adds. Open the saved PDF and you'll find your signature
on the document, Adobe's certificate seal over the whole file, and the audit report on the last pages.
What went over the wire
It helps to know what the SDK sent, because Adobe's own documentation and support talk in terms of HTTP endpoints. This is the whole conversation, with the polling shortened:
GET https://api.adobesign.com/api/rest/v6/baseUris (once, then cached)
POST https://api.na4.adobesign.com/api/rest/v6/transientDocuments multipart: File-Name, File
POST https://api.na4.adobesign.com/api/rest/v6/agreements JSON: AgreementCreationInfo
GET https://api.na4.adobesign.com/api/rest/v6/agreements/{agreementId} (repeated while polling)
GET https://api.na4.adobesign.com/api/rest/v6/agreements/{agreementId}/combinedDocument?attachAuditReport=true
Each request carries Authorization: Bearer <your key> and a
User-Agent naming the SDK and its version. If Adobe answers 429 or 503, the SDK waits as
Adobe asks (Retry-After) and tries again, up to three times by default.
Run the full lifecycle example
The package includes examples/live-lifecycle.php, a longer version of the script above that
exercises more of the SDK against your real account: a signing order with parallel steps, signature levels,
identity checks, an optional webhook, the event history and a signature-evidence report. It is the quickest
way to see what a real account does.
The examples load the package's own Composer autoloader (../vendor/autoload.php), so run them
from a clone of the repository rather than from your project's vendor/ folder:
git clone https://github.com/conceptlz/adobe-sign.git
cd adobe-sign
composer install
ADOBE_SIGN_INTEGRATION_KEY=... \
php examples/live-lifecycle.php /path/to/contract.pdf "alice@example.com,bob@example.com+carol@example.com"
The second argument is the signing order. Commas separate steps; + puts people in the
same step. The example means: Alice signs first, then Bob and Carol in either order. Mixing a
sequential step with a parallel one like this needs "hybrid routing" to be enabled on the account; see
Signing order. A purely sequential list
("a@x.com,b@x.com") always works.
Want to see the order rejected safely first?
Run it with WAIT_MINUTES=0. The script sends the agreement, prints whose turn it is and their
signing links, then exits. If the account doesn't allow the order you asked for, you get the
AccountCapabilityException straight away and nothing is sent.
Optional settings are environment variables:
| Variable | Default | Effect |
|---|---|---|
WAIT_MINUTES | 30 | How long to wait for everyone to sign. 0 sends and exits. |
OUTPUT_DIR | . | Where the signed PDF is saved. |
SIGNATURE_LEVEL | ses | ses, aes or qes. AES and QES make each signer use a certificate-based digital ID (see Signature levels). |
QUALIFIED_ISSUERS | (none) | Comma-separated certificate issuers you accept as qualified, for the QES check at the end. |
VERIFICATION | none | none, email_otp, kba or adobe_account: an identity check before signing (see Identity verification). |
WEBHOOK_URL | (none) | Also registers a webhook for the agreement. Run examples/webhook-receiver.php at that public HTTPS URL. |
A run prints each stage as it happens. Whenever the turn moves on it prints who is up next, with their signing links:
== 1. Upload contract.pdf
transientDocumentId: CBSCTBABDUAAABACAABAA...
== 2. Create with signing order and send
signature level: SES
identity check: NONE
order 1: alice@example.com
order 2: bob@example.com
order 2: carol@example.com
agreementId: CBJCHBCAABAA...
status: OUT_FOR_SIGNATURE
== 4. Who signs now
14:10:02 now waiting for: alice@example.com
sign as alice@example.com: https://secure.na4.adobesign.com/public/apiesign?pid=...
Signers also get an email from Adobe.
== 5. Waiting up to 30 min while people sign (Ctrl+C to stop)
14:11:40 now waiting for: bob@example.com + carol@example.com
...
After the last signature it saves the PDF, lists every audit event (created, emailed, viewed, signed…) and prints, per signer, which signature level they actually reached. Reading that script is a good way to learn the API, because it is short and uses every main part of the SDK.
Real emails go to real people
Both scripts send genuine emails from Adobe. Use addresses you own or that have agreed to take part.
If you stop a run early, cancel the agreement ($sign->agreements()->cancel($id)) so
nobody is left with a stray signing request.
Before you go to production
The scripts above are deliberately simple. A real integration usually changes five things:
- Webhooks, not pollingLet Adobe tell you when an agreement is signed, then download it from a queue job. See Webhooks.
- Store the idsSave the agreement id with your own record (and use
externalId()to put your id on Adobe's side). Ids are the only stable way to find an agreement again. - A shared cachePass a PSR-16 cache (Redis, APCu, files) so the data-centre lookup happens once a day instead of once per PHP request.
- A loggerPass a PSR-3 logger to see method, path, status and duration of every call, and every retry. Headers and bodies are never logged.
- Typed error handlingCatch
RateLimitException,AccountCapabilityExceptionand friends where you can do something useful. See Errors & exceptions.
Where next
Common questions
Do I have to pay Adobe to try this?
No. A free developer account has the full REST API and sends real emails. Its agreements are marked
[DEMO USE ONLY], so you need a paid plan before sending real contracts. Some features,
such as government-ID checks or certificate-based digital signatures, depend on the plan and on admin
settings even on paid accounts.
Which HTTP client should I install?
The one your project already has. If it has none, symfony/http-client with
nyholm/psr7 is small and well tested. Guzzle 7 is equally fine. The SDK only talks to the
PSR-18 interface, so the choice doesn't change any of your SDK code.
Why does the first call make two requests?
Every Acrobat Sign account lives in one data centre, and API calls must go to that data centre. The SDK
asks Adobe's shared host where yours is (GET /baseUris), then caches the answer for a day.
With a shared PSR-16 cache the lookup happens once per day for the whole application. OAuth tokens
already carry the data centre, so OAuth clients skip the lookup entirely.
The agreement was created but no email arrived. What happened?
Check the status first. DOCUMENTS_NOT_YET_PROCESSED lasts a few seconds after creation;
the email goes out after that. If the status is OUT_FOR_SIGNATURE, look in spam and check
the address. If you created the agreement as DRAFT or AUTHORING, nothing is
sent until you move it to IN_PROCESS. Finally, with(['emailOption' => …])
can switch Adobe's emails off entirely; see email options.
I use Laravel. Should I follow this page?
Install the Laravel bridge instead. It installs this SDK, reads the key from your config, uses Laravel's cache and logger, and gives you a facade. Everything you call on the facade is this SDK, so the rest of these docs still apply.
Can my code sign on the signer's behalf to speed up testing?
No, and that is by design: a signature must come from the person signing. The API can hand you a
signing link (signingUrlFor()) to open yourself during testing, but a human still clicks
through Adobe's signing page. Automated test suites usually mock the HTTP layer instead; see
Testing your integration.