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
| Script | Shows | Needs | Sends real email |
|---|---|---|---|
send-agreement.php | Upload, create, status, reminder, download | nothing with --dry-run; otherwise an Integration Key | only without --dry-run |
live-lifecycle.php | The full flow: signing order, waiting for signatures, saving the signed PDF, events, signature evidence | Integration Key, a PDF, signer addresses you control | yes |
oauth-server.php | OAuth connect, automatic refresh, revoke | An API application's client id and secret | no |
webhook-receiver.php | A webhook endpoint: verification, echo, typed events | Your API application's client id; a tunnel for real deliveries | no |
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 passesincludeNextParticipantSet=truebecause 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.
| Argument | Meaning | Orders sent |
|---|---|---|
"a@x.com" | one signer | 1 |
"a@x.com,b@x.com" | a, then b | 1, 2 |
"a@x.com+b@x.com" | a and b in parallel | 1, 1 |
"a@x.com,b@x.com+c@x.com,d@x.com" | a, then b and c together, then d | 1, 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:
| Variable | Values | Default | Effect |
|---|---|---|---|
ADOBE_SIGN_INTEGRATION_KEY | your key | required | The account to use. |
SIGNATURE_LEVEL | ses, aes, qes | ses | Applied with AgreementInfo::signatureLevel(). aes and qes make every signer sign with a digital ID from a cloud signature provider. |
QUALIFIED_ISSUERS | comma-separated issuer names | empty | Certificate issuers you accept as qualified, used in the final QES check. Matched case-insensitively as substrings. |
VERIFICATION | none, email_otp, kba, adobe_account | none | Identity check every signer must pass before opening the document. Independent of the signature level. |
WEBHOOK_URL | an HTTPS URL | none | Also registers an agreement webhook for AGREEMENT_ALL events at that URL (run webhook-receiver.php there). |
WAIT_MINUTES | whole minutes | 30 | How long to wait for everyone to sign. 0 stops after printing the first signing links. |
OUTPUT_DIR | a 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_URLwas 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
sesthe lines readTYPED, not digital, verification NONE -> SES. Withqesand noQUALIFIED_ISSUERS, the last line is(set QUALIFIED_ISSUERS to check QES); with issuers that don't match, it readsfalls short:followed by the reasons fromshortfalls().
| Exit status | Meaning |
|---|---|
0 | Everyone signed and the PDF was saved, or WAIT_MINUTES=0 ended the run after step 4 |
1 | Usage error (no key, no readable PDF, no signers) or an SDK exception, printed with its class |
2 | Stopped 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.
- Configure the API application
In Acrobat Sign, on your API application's "Configure OAuth" page, add
http://localhost:8080/callbackas a redirect URI and enable theuser_loginandagreement_readscopes. - 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 - Connect
Open
http://localhost:8080/login. You are sent to Adobe, sign in and approve, and Adobe redirects back to/callback, which checks thestate, exchanges the code and stores the token. - Use and revoke
http://localhost:8080/lists up to ten agreements./logoutrevokes the token pair at Adobe and deletes the file.
| Path | What it does | SDK calls |
|---|---|---|
/login | Redirects to Adobe's consent page | OAuthClient::generateState(), authorizationUrl() |
/callback | Verifies the state, exchanges the code, stores the token | handleCallback(), TokenStore::put() |
/ | Lists agreements, refreshing the token if needed | AdobeSign::withOAuth(), agreements()->all(pageSize: 20) |
/logout | Revokes the token pair | OAuthClient::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 noGET /baseUriscall. - 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 anOAuthException.
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.
- Start it
ADOBE_SIGN_CLIENT_IDis 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 - 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.
- 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.phpdo it withWEBHOOK_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.