Adobe Sign PHP SDKconceptlz/adobe-sign GitHub

Help

Example scripts

The package ships four plain-PHP scripts that exercise the SDK end to end, with no framework. One runs offline, one walks a real agreement through signing while you watch, and two are tiny servers for the OAuth and webhook flows. Each section below says what the script shows, how to run it and what it prints.

The scripts at a glance

ScriptShowsNeedsSends real email
send-agreement.phpUpload, create, status, reminder, downloadnothing with --dry-run; otherwise an Integration Keyonly without --dry-run
live-lifecycle.phpThe full flow: signing order, waiting for signatures, saving the signed PDF, events, signature evidenceIntegration Key, a PDF, signer addresses you controlyes
oauth-server.phpOAuth connect, automatic refresh, revokeAn API application's client id and secretno
webhook-receiver.phpA webhook endpoint: verification, echo, typed eventsYour API application's client id; a tunnel for real deliveriesno

Before you start

The scripts load vendor/autoload.php from the package directory, so run them from a checkout of the SDK rather than from your application's vendor/ folder:

git clone https://github.com/conceptlz/adobe-sign.git
cd adobe-sign
composer install          # includes the dev dependencies the dry run needs

Every command below is run from that directory. Pass secrets as environment variables on the command line or from a shell you trust; don't paste them into files you might commit.

Live runs reach real people

Without --dry-run, these scripts create real agreements, and Adobe emails every signer. Use addresses you control, preferably on a free developer account. The scripts don't cancel what they create; cancel leftover agreements in Acrobat Sign or with agreements()->cancel().

send-agreement.php

The shortest complete walkthrough: upload a PDF, create an agreement with one signer, wait until Adobe has processed it, send a reminder, and download the combined PDF to the system temp directory. Any SDK error is printed as its class and message, and the script exits with status 2.

Offline dry run

With --dry-run, the script uses php-http/mock-client and the canned responses in tests/Fixtures, so it needs no account and no network. It ends by listing every request the SDK would have sent, which is a quick way to see the data-centre discovery and the exact endpoints:

php examples/send-agreement.php --dry-run
1. Uploaded transient document: 3AAABLblqZhCx7Q1pLx2Vbzc1sNEW9bhAw6PdCEMUhNjfVn6eVOAZsxmJtAQjXq2nnDqp0RkAHyE
2. Created agreement: CBJCHBCAABAAx1aFQyYF-aGrLsBZtVqaT9gPh6Xw42kK
3. Status: OUT_FOR_SIGNATURE
4. Reminder sent: CBJCHBCAABAArem1
5. Combined PDF saved: /tmp/agreement-CBJCHBCAABAAx1aFQyYF-aGrLsBZtVqaT9gPh6Xw42kK.pdf (125 bytes)

Requests sent:
  1. GET  https://api.adobesign.com/api/rest/v6/baseUris
  2. POST https://api.na4.adobesign.com/api/rest/v6/transientDocuments
  3. POST https://api.na4.adobesign.com/api/rest/v6/agreements
  4. GET  https://api.na4.adobesign.com/api/rest/v6/agreements/CBJCHBCAABAAx1aFQyYF-aGrLsBZtVqaT9gPh6Xw42kK
  5. GET  https://api.na4.adobesign.com/api/rest/v6/agreements/CBJCHBCAABAAx1aFQyYF-aGrLsBZtVqaT9gPh6Xw42kK/members?includeNextParticipantSet=true
  6. POST https://api.na4.adobesign.com/api/rest/v6/agreements/CBJCHBCAABAAx1aFQyYF-aGrLsBZtVqaT9gPh6Xw42kK/reminders
  7. GET  https://api.na4.adobesign.com/api/rest/v6/agreements/CBJCHBCAABAAx1aFQyYF-aGrLsBZtVqaT9gPh6Xw42kK/combinedDocument

Two things worth noticing in that list:

  • Request 1 goes to the shard-agnostic host; everything after it goes to na4, the data centre the discovery returned.
  • Request 5 is made by sendReminder(): without explicit participant ids it asks who is next, and passes includeNextParticipantSet=true because the live API leaves that data out otherwise.

Against your account

ADOBE_SIGN_INTEGRATION_KEY=your-integration-key \
  php examples/send-agreement.php contract.pdf signer@example.com

The live run attaches a small logger that writes the SDK's request log to stderr, so you see each call with its status and duration alongside the numbered steps on stdout:

[debug] Adobe Sign request {"method":"POST","path":"/api/rest/v6/transientDocuments","status":201,"duration_ms":934}
1. Uploaded transient document: 3AAABLblqZhB2k9xTq…
[debug] Adobe Sign request {"method":"POST","path":"/api/rest/v6/agreements","status":201,"duration_ms":1611}
2. Created agreement: CBJCHBCAABAA8sQ1fN0r…
[debug] Adobe Sign request {"method":"GET","path":"/api/rest/v6/agreements/CBJCHBCAABAA8sQ1fN0r…","status":200,"duration_ms":288}
[debug] Adobe Sign request {"method":"GET","path":"/api/rest/v6/agreements/CBJCHBCAABAA8sQ1fN0r…","status":200,"duration_ms":251}
3. Status: OUT_FOR_SIGNATURE
[debug] Adobe Sign request {"method":"GET","path":"/api/rest/v6/agreements/CBJCHBCAABAA8sQ1fN0r…/members","status":200,"duration_ms":305}
[debug] Adobe Sign request {"method":"POST","path":"/api/rest/v6/agreements/CBJCHBCAABAA8sQ1fN0r…/reminders","status":201,"duration_ms":412}
4. Reminder sent: CBJCHBCAABAAqk2W…
[debug] Adobe Sign request {"method":"GET","path":"/api/rest/v6/agreements/CBJCHBCAABAA8sQ1fN0r…/combinedDocument","status":200,"duration_ms":702}
5. Combined PDF saved: /tmp/agreement-CBJCHBCAABAA8sQ1fN0r….pdf (48211 bytes)
  • The data-centre lookup doesn't appear in the log: discovery bypasses the request log (see PSR-3 logging).
  • Step 3 may take a few polls: right after creation the status is DOCUMENTS_NOT_YET_PROCESSED, and the script checks every 2 seconds, up to 10 times.
  • The downloaded PDF is the document as it stands, unsigned so far. The signer receives the original invitation and the reminder, so two emails.

Without a key or arguments it prints its usage line and exits with status 1.

live-lifecycle.php

The real thing, end to end: upload a PDF, send it in the signing order you describe, print whose turn it is and their signing links, wait while people sign, save the signed PDF with its audit report, list the agreement's events, and finish with the signature evidence for each signer. It is how the SDK is checked by hand against a real account.

ADOBE_SIGN_INTEGRATION_KEY=your-integration-key \
  php examples/live-lifecycle.php contract.pdf "alice@example.com,bob@example.com+carol@example.com,dan@example.com"

Signer syntax: "a,b+c,d"

The second argument describes the signing order. Commas separate steps; a plus sign puts people in the same step, where they sign in parallel, in any order. Each person becomes a signer with the order number of their step.

ArgumentMeaningOrders sent
"a@x.com"one signer1
"a@x.com,b@x.com"a, then b1, 2
"a@x.com+b@x.com"a and b in parallel1, 1
"a@x.com,b@x.com+c@x.com,d@x.com"a, then b and c together, then d1, 2, 2, 3

Purely sequential and purely parallel orders work on any account. Mixing the two, as in the last row, is "hybrid routing", an account setting; when it's off, creation fails with an AccountCapabilityException (HYBRID_ROUTING_NOT_ALLOWED). See Signing order.

Options

Everything else is set through environment variables:

VariableValuesDefaultEffect
ADOBE_SIGN_INTEGRATION_KEYyour keyrequiredThe account to use.
SIGNATURE_LEVELses, aes, qessesApplied with AgreementInfo::signatureLevel(). aes and qes make every signer sign with a digital ID from a cloud signature provider.
QUALIFIED_ISSUERScomma-separated issuer namesemptyCertificate issuers you accept as qualified, used in the final QES check. Matched case-insensitively as substrings.
VERIFICATIONnone, email_otp, kba, adobe_accountnoneIdentity check every signer must pass before opening the document. Independent of the signature level.
WEBHOOK_URLan HTTPS URLnoneAlso registers an agreement webhook for AGREEMENT_ALL events at that URL (run webhook-receiver.php there).
WAIT_MINUTESwhole minutes30How long to wait for everyone to sign. 0 stops after printing the first signing links.
OUTPUT_DIRa directory.Where to save signed-<agreementId>.pdf.

An invalid SIGNATURE_LEVEL or VERIFICATION value stops the script with a message naming the allowed values. kba (knowledge-based questions) is US-only, and every method must be enabled on the account; if it isn't, creation fails with an AccountCapabilityException.

What it prints

A run with an AES requirement and an email code, where three people sign over about ten minutes:

SIGNATURE_LEVEL=aes VERIFICATION=email_otp WAIT_MINUTES=30 OUTPUT_DIR=/tmp/out \
  php examples/live-lifecycle.php contract.pdf "alice@example.com,bob@example.com+carol@example.com"
== 1. Upload contract.pdf
transientDocumentId: 3AAABLblqZhC0nTq7mXw…

== 2. Create with signing order and send
  signature level: AES (each signer signs with a digital ID)
  identity check: EMAIL_OTP
  order 1: alice@example.com
  order 2: bob@example.com
  order 2: carol@example.com
agreementId: CBJCHBCAABAAu3ZpQ8LkR2vW…
status: OUT_FOR_SIGNATURE

== 4. Who signs now
  14:02:11 now waiting for: alice@example.com
    sign as alice@example.com: https://secure.na4.adobesign.com/public/apiesign?pid=CBFCIBAA3AAABLblqZh…
Signers also get an email from Adobe.

== 5. Waiting up to 30 min while people sign (Ctrl+C to stop)
  14:06:41 now waiting for: bob@example.com + carol@example.com
    sign as bob@example.com: https://secure.na4.adobesign.com/public/apiesign?pid=CBFCIBAA3AAABLblqZh…
    sign as carol@example.com: https://secure.na4.adobesign.com/public/apiesign?pid=CBFCIBAA3AAABLblqZh…
  14:09:58 now waiting for: carol@example.com
    sign as carol@example.com: https://secure.na4.adobesign.com/public/apiesign?pid=CBFCIBAA3AAABLblqZh…
  14:12:30 everyone has signed: SIGNED

== 6. Download the signed PDF (with audit report)
saved /tmp/out/signed-CBJCHBCAABAAu3ZpQ8LkR2vW….pdf (412398 bytes)

== 7. What happened (agreement events)
  2026-11-03T14:02:05Z  CREATED                  sender@example.com
  2026-11-03T14:02:06Z  ACTION_REQUESTED         alice@example.com
  2026-11-03T14:04:12Z  EMAIL_OTP_AUTHENTICATED  alice@example.com
  2026-11-03T14:06:30Z  DIGSIGNED                alice@example.com
  2026-11-03T14:06:31Z  ACTION_REQUESTED         bob@example.com
  2026-11-03T14:06:31Z  ACTION_REQUESTED         carol@example.com
  2026-11-03T14:09:47Z  DIGSIGNED                bob@example.com
  2026-11-03T14:12:19Z  DIGSIGNED                carol@example.com
  2026-11-03T14:12:20Z  SIGNED                   sender@example.com

== 8. Signature evidence (required: AES)
  alice@example.com: TYPED, digital (Intesi Group Cloud Signature; issuer: Intesi Group Advanced Cloud Signature CA), verification EMAIL_OTP -> AES
  bob@example.com: TYPED, digital (Intesi Group Cloud Signature; issuer: Intesi Group Advanced Cloud Signature CA), verification EMAIL_OTP -> AES
  carol@example.com: SCRIBBLE, digital (Intesi Group Cloud Signature; issuer: Intesi Group Advanced Cloud Signature CA), verification EMAIL_OTP -> AES
  meets AES

Reading it:

  • Step 3 is missing because no WEBHOOK_URL was set.
  • "now waiting for" is printed only when the turn moves on. The script polls every 10 seconds; while Adobe is still processing it says (Adobe is still processing) and shows the links on the next poll.
  • Events come straight from agreements()->events(); the exact list depends on the account and on what each person did.
  • Evidence: the provider and issuer depend on the trust provider your account offers. With the default ses the lines read TYPED, not digital, verification NONE -> SES. With qes and no QUALIFIED_ISSUERS, the last line is (set QUALIFIED_ISSUERS to check QES); with issuers that don't match, it reads falls short: followed by the reasons from shortfalls().
Exit statusMeaning
0Everyone signed and the PDF was saved, or WAIT_MINUTES=0 ended the run after step 4
1Usage error (no key, no readable PDF, no signers) or an SDK exception, printed with its class
2Stopped waiting: the agreement was cancelled, expired or archived, or the time ran out

Run it in the background

A run can take as long as your signers do. Start it in a second terminal or with output redirected to a file, sign from the emails or the printed links, and read the result when it finishes. With WAIT_MINUTES=0 the agreement stays out for signature; cancel it when you're done.

oauth-server.php

A complete OAuth authorization-code flow on PHP's built-in web server: connect an Acrobat Sign account, list its agreements with a token that refreshes itself, and revoke access. It stores tokens in a JSON file in the system temp directory (mode 600) so you can restart the server and watch refresh-on-expiry work; a real application stores them encrypted in its database.

  1. Configure the API application

    In Acrobat Sign, on your API application's "Configure OAuth" page, add http://localhost:8080/callback as a redirect URI and enable the user_login and agreement_read scopes.

  2. Start the server
    ADOBE_SIGN_CLIENT_ID=your-client-id ADOBE_SIGN_CLIENT_SECRET=your-client-secret \
      php -S localhost:8080 examples/oauth-server.php
  3. Connect

    Open http://localhost:8080/login. You are sent to Adobe, sign in and approve, and Adobe redirects back to /callback, which checks the state, exchanges the code and stores the token.

  4. Use and revoke

    http://localhost:8080/ lists up to ten agreements. /logout revokes the token pair at Adobe and deletes the file.

PathWhat it doesSDK calls
/loginRedirects to Adobe's consent pageOAuthClient::generateState(), authorizationUrl()
/callbackVerifies the state, exchanges the code, stores the tokenhandleCallback(), TokenStore::put()
/Lists agreements, refreshing the token if neededAdobeSign::withOAuth(), agreements()->all(pageSize: 20)
/logoutRevokes the token pairOAuthClient::revoke()

After connecting, the home page shows something like:

Connected to https://api.na4.adobesign.com/ (token expires 2026-11-03T15:04:05+00:00)

SIGNED       CBJCHBCAABAAu3ZpQ8LkR2vW Supplier agreement 2026
OUT_FOR_SIGNATURE CBJCHBCAABAA8sQ1fN0rTb5c Mutual NDA
WAITING_FOR_MY_SIGNATURE CBJCHBCAABAAp0Lx2Vbzc1sN Office lease renewal
CANCELLED    CBJCHBCAABAAk3mQ7wRt9YhZ [DEMO USE ONLY] SDK live check: contract.pdf

/logout revokes the token.
  • The data centre comes from the token's api_access_point, so there is no GET /baseUris call.
  • Statuses longer than 12 characters push the columns out; the script keeps formatting minimal.
  • Without the environment variables the server answers Set ADOBE_SIGN_CLIENT_ID and ADOBE_SIGN_CLIENT_SECRET. with status 500. Any SDK error is shown as its class and message with status 400; a denied consent, for example, appears as an OAuthException.

The full OAuth guide, including token storage and security checks, is in Authentication.

webhook-receiver.php

A minimal webhook endpoint in plain PHP. It checks the X-AdobeSign-ClientId header against an allowlist, acknowledges first (Adobe allows five seconds), and then logs the event, noting when an agreement has completed. Rejected requests get 403 (unknown client) or 400 (unreadable body) and no echo.

  1. Start it

    ADOBE_SIGN_CLIENT_ID is the client id (application id) of the API application that creates your webhooks; Adobe sends it with every delivery.

    ADOBE_SIGN_CLIENT_ID=your-client-id php -S localhost:8081 examples/webhook-receiver.php
  2. Make it reachable

    Adobe only calls public HTTPS URLs. Put a tunnel such as ngrok or cloudflared in front of port 8081 and note its HTTPS address.

  3. Register a webhook

    Point a webhook at the tunnel. Adobe first sends a verification GET, which the script answers.

    $sign->webhooks()->create(
        WebhookInfo::forAgreement('Sync', 'https://your-tunnel.example.com/', $agreementId)
            ->events(EventType::AGREEMENT_ALL),
    );

    Or let live-lifecycle.php do it with WEBHOOK_URL.

Trying it without Adobe

You can post a sample notification yourself, using a fixture from the test suite:

curl -i http://localhost:8081/ \
  -H 'X-AdobeSign-ClientId: your-client-id' \
  -H 'Content-Type: application/json' \
  --data @tests/Fixtures/webhook_agreement_workflow_completed.json

The response (headers abridged) carries the echo Adobe requires, in a header and in the body:

HTTP/1.1 200 OK
X-AdobeSign-ClientId: your-client-id
Content-Type: application/json

{"xAdobeSignClientId":"your-client-id"}

The server's console shows the acknowledged event (PHP's built-in server adds its own timestamp and request lines around it):

[2026-11-03T14:22:10Z] AGREEMENT_WORKFLOW_COMPLETED CBJCHBCAABAAx1aFQyYF-aGrLsBZtVqaT9gPh6Xw42kK -> completed, fetch the signed PDF with agreements()->combinedDocument()

The same request with a different client id gets HTTP/1.1 403 Forbidden and no echo, which is what makes Adobe treat a delivery not meant for you as failed. The comments in the script point out what a real application adds: put the event on a queue, drop duplicates by notificationId, and re-fetch the agreement through the API before acting, because payloads are not signed. See Webhooks.

Common questions

Can I run the examples from my project's vendor directory?

Not as they are: they require vendor/autoload.php relative to the package directory, and the dry run needs the SDK's dev dependencies and test fixtures. Clone the repository and run composer install there (Before you start).

How do I clean up agreements the examples created?

Cancel them in Acrobat Sign's Manage page, or with $sign->agreements()->cancel($id, 'Test run', notifyOthers: false). On a developer account they are clearly marked: Adobe prefixes their names with [DEMO USE ONLY].

Can the script sign for the test signers?

No, and it shouldn't: a signature is a person's act. The script prints the signing links so the people you listed can sign from the terminal output as well as from their email.

How do I check a QES run?

Run with SIGNATURE_LEVEL=qes and set QUALIFIED_ISSUERS to the certificate issuer names of your qualified provider. The last step then reports meets QES or why not. The account must have a qualified trust service provider configured; see Signature levels.

Can I run the OAuth example on a different port?

The redirect URI http://localhost:8080/callback is written into the script and must match the one registered with Adobe exactly. To use another port, change both.