Adobe Sign PHP SDKconceptlz/adobe-sign GitHub

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:

OptionInstallPick it when
Symfony HttpClient + Nyholm PSR-7composer require symfony/http-client nyholm/psr7You have no preference, or you use Symfony. It is lightweight and what the SDK's own tests use.
Guzzle 7composer require guzzlehttp/guzzleGuzzle 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-18Your choiceBuzz, 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():

Injecting Symfony HttpClient
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,
);
Injecting Guzzle
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.

  1. 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.

  2. Confirm your email and set a password

    Adobe sends an activation email. Follow it, then log in to the Acrobat Sign web app.

  3. Note your data centre (optional)

    The address bar shows a host such as secure.na4.adobesign.com. The na4 part 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.

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.

  1. 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".

  2. 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.

  3. 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.

.env
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:

whoami.php
<?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 seeIt means
InvalidArgumentException: Integration key must not be empty.The environment variable isn't set in this shell.
AuthenticationException with INVALID_ACCESS_TOKENThe key is wrong, revoked, or copied with extra characters.
TransportExceptionNo 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 foundNo 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.

  1. 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.

  2. Create and send the agreement

    AgreementInfo is 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 is IN_PROCESS, which means "send now". More in Creating agreements.

  3. Wait for Adobe to process the document

    For the first few seconds the status is DOCUMENTS_NOT_YET_PROCESSED while Adobe converts and prepares the file. Then it becomes OUT_FOR_SIGNATURE and the signing email goes out.

  4. 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.

  5. 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.

first-agreement.php
<?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:

VariableDefaultEffect
WAIT_MINUTES30How long to wait for everyone to sign. 0 sends and exits.
OUTPUT_DIR.Where the signed PDF is saved.
SIGNATURE_LEVELsesses, 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.
VERIFICATIONnonenone, 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, AccountCapabilityException and 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.