conceptlz / adobe-sign
Framework-agnostic PHP SDK for the Adobe Acrobat Sign REST API v6.
Requires
- php: ^8.2
- ext-json: *
- php-http/discovery: ^1.19
- psr/clock: ^1.0
- psr/http-client: ^1.0
- psr/http-client-implementation: ^1.0
- psr/http-factory: ^1.0
- psr/http-factory-implementation: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^2.0 || ^3.0
- psr/simple-cache: ^2.0 || ^3.0
Requires (Dev)
- justinrainbow/json-schema: ^6.0
- nyholm/psr7: ^1.8
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5 || ^11.0
- symfony/http-client: ^6.4 || ^7.0
Suggests
- guzzlehttp/guzzle: PSR-18 client implementation (auto-discovered)
- nyholm/psr7: PSR-17 factories (auto-discovered)
- symfony/http-client: PSR-18 client implementation (auto-discovered)
Provides
None
Conflicts
None
Replaces
None
README
Full documentation: conceptlz.github.io/adobe-sign (guides, reference, FAQ; also in docs/).
A PHP SDK for the Adobe Acrobat Sign REST API v6. Upload documents, send them for e-signature in any signing order, follow progress through webhooks, and download the signed PDF. It works in any PHP 8.2+ project.
- Framework-agnostic. Depends only on PSR interfaces (HTTP client, factories, cache, logger, clock).
Using Laravel? Install
conceptlz/adobe-sign-laravelon top. - Both auth methods. Integration Key, or OAuth 2.0 (authorization code with automatic refresh).
- Production details handled. Data-center routing, retries on rate limits, ETags, streaming uploads/downloads, typed exceptions, and webhook verification.
- Checked against Adobe's spec. Every request in the test suite is validated against Adobe's published OpenAPI schemas, and 313 response models are generated from them.
Contents
- Installation
- The whole flow in one example
- Authentication
- Uploading documents
- Creating agreements and signing order
- Signature levels (SES, AES, QES)
- Managing agreements
- Downloading signed documents
- Webhooks
- Other resources
- Errors
- Configuration
- Generated models
- Testing your integration
- Examples
Installation
composer require conceptlz/adobe-sign
The SDK needs any PSR-18 HTTP client and PSR-17 factories, and finds them automatically. If your project has neither yet:
composer require symfony/http-client nyholm/psr7
Guzzle 7+ works too, and is already installed in every Laravel app.
Requirements: PHP 8.2+, an Acrobat Sign account, and API credentials. A free developer account is enough for development.
The whole flow in one example
use Conceptlz\AdobeSign\AdobeSign; use Conceptlz\AdobeSign\Builder\{AgreementInfo, FileInfo, ParticipantSetInfo, WebhookInfo}; use Conceptlz\AdobeSign\Webhook\EventType; $sign = AdobeSign::withIntegrationKey(getenv('ADOBE_SIGN_INTEGRATION_KEY')); // 1. Upload the PDF $documentId = $sign->transientDocuments()->upload('/path/to/contract.pdf'); // 2. Create the agreement with a signing order, and send it (state IN_PROCESS is the default) $agreementId = $sign->agreements()->create( AgreementInfo::create('Master Services Agreement') ->file(FileInfo::transientDocument($documentId)) ->participantSet(ParticipantSetInfo::signer('client@example.com')->order(1)) ->participantSet(ParticipantSetInfo::approver('legal@example.com')->order(2)) // order 2: these two ->participantSet(ParticipantSetInfo::signer('cfo@example.com')->order(2)) // act in parallel ->participantSet(ParticipantSetInfo::signer('ceo@example.com')->order(3)) ->message('Please review and sign.'), ); // 3. Get notified as people sign (see "Webhooks" for the receiving side) $sign->webhooks()->create( WebhookInfo::forAgreement('Contract sync', 'https://app.example.com/adobe-sign/webhook', $agreementId) ->events(EventType::AGREEMENT_ALL), ); // 4. Once everyone has signed: download the signed PDF and save it if ($sign->agreements()->get($agreementId)->isSigned()) { $pdf = $sign->agreements()->combinedDocument($agreementId, attachAuditReport: true); file_put_contents("storage/agreements/$agreementId.pdf", $pdf->detach()); }
To run this exact flow against your own account, use
examples/live-lifecycle.php.
Authentication
Integration Key
An Integration Key is a non-expiring key for one Acrobat Sign account. Create one in Acrobat Sign under Account → Acrobat Sign API → API Information → Integration Key.
$sign = AdobeSign::withIntegrationKey(getenv('ADOBE_SIGN_INTEGRATION_KEY'));
OAuth 2.0
Use OAuth when users connect their own Acrobat Sign accounts to your application. Create an API application in Acrobat Sign, then configure its redirect URI and scopes.
use Conceptlz\AdobeSign\Auth\{OAuthClient, OAuthConfig, InMemoryTokenStore}; $oauth = new OAuthClient(new OAuthConfig($clientId, $clientSecret, 'https://app.example.com/adobe/callback')); // 1. Send the user to Adobe (keep $state in their session) $state = OAuthClient::generateState(); header('Location: ' . $oauth->authorizationUrl(['user_login:self', 'agreement_read:self', 'agreement_write:self', 'agreement_send:self'], $state)); // 2. On your redirect URI: check the state and exchange the code (valid for 5 minutes) $token = $oauth->handleCallback($_GET, $state); $tokenStore->put($userId, $token); // 3. Any time later. Tokens refresh automatically 60s before expiry, or after a 401. $sign = AdobeSign::withOAuth($oauth, $tokenStore, $userId);
- Token storage.
TokenStorehas two methods,get(string $key)andput(string $key, AccessToken $token).InMemoryTokenStoreships for scripts and tests; implement it on your database for real apps, and store tokens encrypted.AccessToken::toArray()andfromArray()make that easy. - Data center. The token carries the account's
api_access_point, so OAuth clients never callGET /baseUris. - Security checks. An
api_access_pointarriving in the callback query string is only used if it is HTTPS onadobesign.com,echosign.comoradobesigngov.us. This stops a forged callback from receiving your client secret. - Expired or revoked access. Refresh tokens expire after 60 days without use. A dead refresh
token raises
OAuthException, and the user has to authorize again. - Revoking.
$oauth->revoke($token)revokes the access/refresh token pair.
Runnable demo: examples/oauth-server.php (php -S localhost:8080 examples/oauth-server.php).
Acting as another user
An account-admin token can act on behalf of any user in the account:
$sign->asUser('jane@example.com')->agreements()->all(); // sends x-api-user: email:jane@example.com $sign->asUser('CBJCHBCAABAA...')->agreements()->all(); // a user id becomes userid:...
asUser() returns a new client; the original is unchanged.
Uploading documents
$id = $sign->transientDocuments()->upload('/path/contract.pdf'); // from a path $id = $sign->transientDocuments()->upload(new SplFileInfo($path)); // SplFileInfo $id = $sign->transientDocuments()->upload(fopen($path, 'rb'), 'contract.pdf'); // stream resource $id = $sign->transientDocuments()->upload($psr7Stream, 'contract.pdf', 'application/pdf'); // PSR-7 stream
- The upload is streamed, so large PDFs aren't loaded into memory.
- Transient documents expire 7 days after upload. Use the id in an agreement before then.
- To reuse a document many times, store it as a library template (
libraryDocuments()->create()), then reference it withFileInfo::libraryDocument($id). - Adobe can also fetch a document itself:
FileInfo::url('https://…/contract.pdf', 'contract.pdf').
Creating agreements and signing order
AgreementInfo builds the POST /agreements payload. Its defaults are e-signature (ESIGN) and
send immediately (IN_PROCESS).
use Conceptlz\AdobeSign\Builder\{AgreementInfo, FileInfo, ParticipantInfo, ParticipantSetInfo}; use Conceptlz\AdobeSign\Enum\{AgreementState, ParticipantRole, ReminderFrequency}; $info = AgreementInfo::create('NDA') ->file($documentId) // a transientDocumentId ->participantSet(ParticipantSetInfo::signer(ParticipantInfo::email('jane@example.com', 'Jane Doe'))) ->participantSet(ParticipantSetInfo::role(ParticipantRole::APPROVER, 'boss@example.com')) ->cc('legal@example.com') ->message('Please sign by Friday.') ->externalId('crm-deal-42') // your own reference ->reminderFrequency(ReminderFrequency::DAILY_UNTIL_SIGNED) ->expiresAt(new DateTimeImmutable('+14 days')); $agreementId = $sign->agreements()->create($info);
Signing order
Each ParticipantSetInfo is one step in the signing order. Sets with the same order number act in
parallel. Leave order() out and sets are numbered in the order you add them.
| You want | Do |
|---|---|
| A, then B, then C | ->signers('a@x.com', 'b@x.com', 'c@x.com') |
| A, then B and C (any order), then D | ->order(1) for A, ->order(2) for B and C, ->order(3) for D |
| Anyone in a group can sign for it | ParticipantSetInfo::signer('a@x.com', 'b@x.com'): one set, two members |
| Approve before signing | ParticipantSetInfo::approver(...)->order(1), then signers from order(2) |
Adobe requires the order numbers to run from 1 with no gaps (1, 1, 2 is fine; 1, 3 is not). The
builder checks this locally and throws InvalidArgumentException, so a bad order never reaches the API.
Account setting for mixed orders. Purely sequential (1, 2, 3) and purely parallel (1, 1, 1)
orders always work. Mixing them, e.g. 1 then 2 + 2 then 3, is called hybrid routing and is
an Acrobat Sign account setting. When it's off, Adobe answers 403 HYBRID_ROUTING_NOT_ALLOWED
(an AccountCapabilityException); an account admin can enable it in the account's send settings.
Available roles (ParticipantRole): SIGNER, APPROVER, ACCEPTOR, CERTIFIED_RECIPIENT, FORM_FILLER,
DELEGATE_TO_*, NOTARY_SIGNER, ELECTRONIC_SEALER, WITNESS, HOST, IN_PERSON_SIGNER.
Sending: now, or draft first
$info->state(AgreementState::IN_PROCESS); // default: sent to the first signer(s) on create $info->state(AgreementState::AUTHORING); // create without sending, e.g. to place fields in Adobe's UI $sign->agreements()->updateState($agreementId, 'IN_PROCESS'); // send a draft later
Anything the builder doesn't cover
with() merges raw fields from the spec's AgreementCreationInfo into the payload. For full typing,
start from the generated model:
$info->with(['emailOption' => ['sendOptions' => ['initEmails' => 'NONE']]]); $model = $info->toModel(); // Conceptlz\AdobeSign\Generated\Model\AgreementCreationInfo $model->setFirstReminderDelay(24); $sign->agreements()->create($model); // create() accepts the builder, the model, or a raw array
Signature levels (SES, AES, QES)
The EU's eIDAS regulation defines three levels of electronic signature. Other countries organise electronic signatures in their own ways (some in similar tiers, some by licensed certificates, some with no levels at all); see the jurisdiction table below. A project chooses the level a document needs. The package defaults to SES, Acrobat Sign's normal e-signature, and lets you require more per project, per agreement or per signer.
| Level | What the signer does | How Acrobat Sign does it | Legal weight (EU) |
|---|---|---|---|
| SES (simple) | types, draws or clicks | e-signature (the default) | admissible; the party relying on it must prove who signed |
| AES (advanced) | signs with a personal digital certificate | isDigitalSignatureRequired: a cloud-based digital signature |
linked to the signer; later changes are detectable |
| QES (qualified) | signs with a qualified certificate from a QTSP | same request as AES; the account's signature provider must be a QTSP | equivalent to a handwritten signature in every EU member state |
Requiring a level
use Conceptlz\AdobeSign\ClientOptions; use Conceptlz\AdobeSign\Enum\SignatureLevel; // Project-wide default (SES unless set) $sign = AdobeSign::withIntegrationKey($key, options: new ClientOptions(defaultSignatureLevel: SignatureLevel::AES)); // Per agreement: every signer AgreementInfo::create('Employment contract')->file($doc)->signers('employee@example.com') ->signatureLevel(SignatureLevel::QES); // Per signer, overriding the agreement in either direction AgreementInfo::create('Loan agreement')->file($doc) ->participantSet(ParticipantSetInfo::signer('borrower@example.com')->signatureLevel(SignatureLevel::QES)) ->participantSet(ParticipantSetInfo::signer('witness@example.com')->signatureLevel(SignatureLevel::SES));
- AES and QES apply to signers only. Approvers, acceptors and form fillers don't sign, so asking for
them throws
InvalidArgumentException. - AES/QES with
signatureType(WRITTEN)is rejected locally, before anything is sent. - The default applies to agreements built with
AgreementInfo. Raw arrays and generated models are sent as you wrote them.
Identity checks
Independently of the level, you can require each signer to prove who they are before they can open the document:
use Conceptlz\AdobeSign\Builder\{ParticipantInfo, Verification}; ParticipantInfo::email('jane@example.com')->verifyWith(Verification::emailOtp()); ParticipantInfo::email('ali@example.ae')->verifyWith(Verification::phone('501234567', '971', 'AE')); ParticipantInfo::email('max@example.de')->verifyWith(Verification::digitalIdentity(providerId: '…')); // eID / bank ID gateway ParticipantInfo::email('kim@example.com')->verifyWith(Verification::governmentId()); // ID document scan
Also available: password(), whatsApp(), knowledgeBased() (US only), adobeAccount() and none().
An Acrobat Sign admin decides which methods are enabled, and government ID uses a paid quota. A method
the account doesn't allow, or a feature it doesn't have (such as mixed signing orders), throws
AccountCapabilityException. That error is fixed in Acrobat Sign admin, not in code.
What makes a signature qualified
The API call for QES is the same as for AES. Whether the certificate is qualified depends on the trust provider configured in your Acrobat Sign account. To get real QES:
- Use an Acrobat Sign plan that includes digital signatures.
- In Acrobat Sign admin, enable digital signatures and add a qualified trust service provider (QTSP) as a cloud signature provider. Make it the default for the regions that need QES.
- Each signer needs an identity with that provider. Verifying it is part of the provider's onboarding (video ident, eID, in person, ...).
Where to look by jurisdiction. These are brief pointers to the general position, not legal advice; confirm the specifics with Adobe, the provider and your legal team. The signature levels guide explains each one.
| Jurisdiction | Model | Framework | Where stronger signatures come from |
|---|---|---|---|
| EU / EEA | Tiered | eIDAS (EU No 910/2014) | QTSPs on the EU Trusted List; several integrate with Acrobat Sign (e.g. DigiCert, TrustPro, A-Trust, Bundesdruckerei sign-me) |
| United Kingdom | Tiered | UK eIDAS and the Electronic Communications Act 2000 | Providers qualified under the UK regime |
| Switzerland | Tiered | ZertES | Providers recognised under Swiss rules (e.g. DigiCert for ZertES) |
| Brazil | Tiered, certificate-based | MP 2.200-2/2001 (ICP-Brasil), Law 14.063/2020 | ICP-Brasil certificates |
| UAE | Tiered | Federal Decree-Law No. 46 of 2021 | TDRA-licensed providers; UAE PASS |
| India | Certificate-based | Information Technology Act 2000 | Certifying Authorities licensed by the CCA; Aadhaar eSign |
| Saudi Arabia | Certificate-based | Electronic Transactions Law | Licensed certification service providers |
| United States | Technology-neutral | ESIGN Act, UETA | No tiers; evidence and identity checks carry the weight |
| Canada | Technology-neutral (federal "secure" tier) | Provincial e-commerce acts; PIPEDA | Certificates only for specific federal purposes |
| Singapore | Technology-neutral (with a "secure" tier) | Electronic Transactions Act | Secure electronic signatures that meet the Act's conditions |
| Australia | Technology-neutral | Electronic Transactions Act 1999 and state laws | No tiers; deeds and witnessing follow state rules |
This package requests the level and reports what was used. It can't decide whether a signature meets a legal requirement in your jurisdiction; that is for your legal or compliance team.
Proving the level after signing
$evidence = $sign->agreements()->signatureEvidence($agreementId); foreach ($evidence->signers as $signer) { // email, order, signed, signedAt, signatureInputType (TYPED, SCRIBBLE, ...), digitallySigned, // certificateIssuer, signingProvider, verification (EMAIL_OTP, GOV_ID, ...) } $qualified = ['DigiCert', 'TrustPro QTSP']; // certificate issuers you accept as qualified if (!$evidence->meets(SignatureLevel::QES, $qualified)) { $problems = $evidence->shortfalls(SignatureLevel::QES, $qualified); // ["jane@example.com signed with an electronic signature (TYPED), not a digital one: SES, QES required."] }
- How levels are inferred. A signature counts as AES when Adobe recorded a digital signature for it, and as QES when that signature's certificate issuer matches your list.
- Formal verification. For an authoritative check, validate the signed PDF's signatures against the relevant trusted list, e.g. with the EU's DSS validation service.
Managing agreements
$agreement = $sign->agreements()->get($agreementId); // Model\Agreement: id, name, status, etag, raw... $agreement->status; // OUT_FOR_SIGNATURE, SIGNED, CANCELLED, ... $agreement->details(); // full generated FetchedAgreementInfo $sign->agreements()->status($agreementId); // shortcut foreach ($sign->agreements()->all() as $summary) {} // every agreement, pages fetched lazily $sign->agreements()->sendReminder($agreementId); // remind whoever's turn it is $sign->agreements()->cancel($agreementId, 'Sent to the wrong person'); $copyId = $sign->agreements()->copy($agreementId); // new DRAFT, same documents and people // Embedded signing: a link for one signer, when it's their turn $url = $sign->agreements()->signingUrlFor($agreementId, 'jane@example.com');
Updating. Fetch the agreement, change it, and send it back:
$details = $sign->agreements()->get($agreementId)->details(); $details->setMessage('Updated instructions'); $sign->agreements()->update($agreementId, $details); // If-Match is sent automatically
If someone else changed the agreement in between, the update throws PreconditionFailedException.
Fetch it again and retry. This can also happen in the first few seconds after sending: Adobe keeps
updating the agreement in the background while it delivers emails, so an ETag read in that window can
already be stale.
Whose turn is it? members($id, includeNextParticipantSet: true) adds nextParticipantSets.
Adobe leaves that key out unless asked, even though the spec lists it. Every participant set also has a
status such as WAITING_FOR_MY_SIGNATURE or NOT_YET_VISIBLE.
Developer accounts. On a free developer account, Adobe prefixes every agreement name with
[DEMO USE ONLY].
Not signable yet. Right after create(), Adobe processes the document asynchronously. Until it
finishes, status is DOCUMENTS_NOT_YET_PROCESSED and signingUrls() throws NotFoundException
(AGREEMENT_NOT_EXPOSED or AGREEMENT_NOT_SIGNABLE). Even once the status reads OUT_FOR_SIGNATURE,
the participant data can lag for a moment. Retry after a few seconds.
All agreement methods: create, get, status, all, update, updateState, cancel, copy, members, signatureEvidence,
sendReminder, reminders, signingUrls, signingUrlFor, views, documents, document, combinedDocument,
combinedDocumentUrl, auditTrail, formData, formFields, events.
Downloading signed documents
Binary endpoints return a PSR-7 stream, so files of any size can go straight to disk, cloud storage, or the browser without being loaded into memory:
$pdf = $sign->agreements()->combinedDocument($agreementId, attachAuditReport: true); // To a file, in chunks $out = fopen("/var/app/agreements/$agreementId.pdf", 'wb'); while (!$pdf->eof()) { fwrite($out, $pdf->read(65536)); } fclose($out); // Or hand the underlying resource to anything that accepts one (Flysystem, Laravel Storage, S3 SDK...) $filesystem->writeStream("agreements/$agreementId.pdf", $pdf->detach());
| Method | Returns |
|---|---|
combinedDocument($id, attachAuditReport: true) |
every document merged into one PDF, optionally with the audit report |
auditTrail($id) |
the audit report PDF on its own |
document($id, $documentId) |
one original document (ids from documents($id)) |
combinedDocumentUrl($id) |
a short-lived download URL instead of the bytes |
formData($id) |
CSV of the values signers entered |
Webhooks
Webhooks let Adobe tell you when something happens (sent, viewed, signed, declined, completed) instead of you polling.
Subscribe
use Conceptlz\AdobeSign\Builder\WebhookInfo; use Conceptlz\AdobeSign\Webhook\EventType; $sign->webhooks()->create( WebhookInfo::forAgreement('Contract sync', 'https://app.example.com/adobe-sign/webhook', $agreementId) ->events(EventType::AGREEMENT_ALL) ->agreementPayload(participants: true), // what the payload should include );
There are also forAccount(), forGroup(), forUser() and forResource() variants.
Adobe recommends one webhook per agreement over one account-wide webhook. If deliveries to a webhook keep failing, Adobe stops it, so with one webhook per agreement a failure only affects that agreement.
Receive
Adobe's rules (the handler implements all of them):
- Verification. On creation, Adobe sends a
GETwith anX-AdobeSign-ClientIdheader. You must answer 2xx and echo that value, otherwise the webhook isn't created. - Every notification. Each
POSTneeds the same echo within 5 seconds. Without it, Adobe retries with growing intervals for 72 hours, then disables the webhook. - No signature. Payloads aren't signed. The handler only accepts allowlisted client ids. Treat payload content as a hint and confirm with the API before acting on it.
use Conceptlz\AdobeSign\Webhook\{WebhookHandler, AgreementEvent}; $handler = new WebhookHandler([$yourClientId]); // PSR-7 (Slim, Mezzio, Symfony with the PSR bridge, ...) $result = $handler->handle($serverRequest, $responseFactory, $streamFactory); if ($result->event instanceof AgreementEvent && $result->event->isWorkflowCompleted()) { $queue->push(['download', $result->event->agreementId()]); // do slow work later } return $result->response; // carries the echo // Plain PHP $clientId = $handler->verifyClientId($_SERVER['HTTP_X_ADOBESIGN_CLIENTID'] ?? null); foreach ($handler->acknowledgementHeaders($clientId) as $name => $value) { header("$name: $value"); } echo $handler->acknowledgementBody($clientId); $event = $_SERVER['REQUEST_METHOD'] === 'POST' ? $handler->parse(file_get_contents('php://input')) : null;
- Event classes. Events come back as
AgreementEvent,MegaSignEvent,WidgetEvent,LibraryDocumentEvent, orUnknownEventfor event types Adobe adds later. They're returned to you, never dispatched. - Event fields. Useful ones include
$event->event(compare withEventType::*),notificationId(use it to drop retried duplicates),actingUserEmail,participantUserEmailandeventDate.resource()holds the payload's agreement, web form, etc. - Testing locally. Adobe only calls public HTTPS URLs, so use a tunnel such as
ngrokorcloudflared. A complete receiver is inexamples/webhook-receiver.php.
Other resources
| Accessor | What it is | Operations |
|---|---|---|
libraryDocuments() |
reusable templates | all, get, create, update, updateState, documents, document, combinedDocument, auditTrail, formFields, members, events |
widgets() |
web forms (public signing links) | all, get, create, update, updateState, agreements, documents, members, formFields, events, formData, combinedDocument, auditTrail |
megaSigns() |
Send in Bulk | all, get, create, updateState, cancel, agreements, sendReminder, members, events, formData, combinedDocument, errorReport |
users() |
account users | all, get, me, create, update, groups, updateState, events |
webhooks() |
webhook subscriptions | all, get, create, update, updateState, delete |
- Lists (
all(),agreements(),events()on users) are generators: pages are fetched only as you iterate. - Return values are typed generated models.
create()andupdate()accept a generated model or a raw array.
Errors
Every exception implements Conceptlz\AdobeSign\Exception\AdobeSignException. API errors carry
$statusCode, Adobe's $errorCode (e.g. INVALID_AGREEMENT_ID), and the response.
| Exception | When |
|---|---|
AuthenticationException |
401: bad or expired key/token, invalid x-api-user |
OAuthException (extends the above) |
OAuth denied, state mismatch, dead refresh token: re-authorize |
NotFoundException |
404: unknown id, deleted resource, AGREEMENT_NOT_SIGNABLE |
ValidationException |
400/415: invalid payload or parameters |
RateLimitException |
429 after retries; $retryAfter says how long to wait |
PreconditionFailedException |
412: stale ETag; fetch the resource again and retry |
AccountCapabilityException |
the account doesn't allow it: hybrid routing off, identity method not enabled, no government-ID quota |
ApiException |
any other API error (403 PERMISSION_DENIED, 5xx, ...), or a reply the SDK can't read (invalid JSON) |
TransportException |
no response at all: network, DNS, TLS |
InvalidArgumentException |
rejected locally before sending: bad email, signing-order gap, missing file |
WebhookException |
inbound webhook rejected (unknown client id, unreadable body) |
use Conceptlz\AdobeSign\Exception\{AdobeSignException, NotFoundException, RateLimitException}; try { $sign->agreements()->get($id); } catch (NotFoundException $e) { // $e->errorCode: INVALID_AGREEMENT_ID, AGREEMENT_DELETED, ... } catch (RateLimitException $e) { // retry in $e->retryAfter seconds } catch (AdobeSignException $e) { // anything else from the SDK }
Exceptions never contain your token: the stored request's Authorization header is replaced with
Bearer ***.
Configuration
Every collaborator is optional and can be injected:
use Conceptlz\AdobeSign\{AdobeSign, ClientOptions}; $sign = new AdobeSign( $tokenProvider, // IntegrationKeyTokenProvider, OAuthTokenProvider, or your own TokenProvider httpClient: $psr18Client, // default: auto-discovered requestFactory: $psr17, streamFactory: $psr17, cache: $psr16Cache, // caches the account's data center; default: in-process only logger: $psr3Logger, // method, path, status, duration; never headers or bodies options: new ClientOptions(maxRetries: 5), );
ClientOptions |
Default | Meaning |
|---|---|---|
bootstrapUrl |
https://api.adobesign.com |
host used only for GET /baseUris; change for Gov Cloud |
baseUriTtl |
86400 |
seconds to cache the discovered data center |
maxRetries |
3 |
retries for 429/503 before RateLimitException |
retryBaseDelay |
1.0 |
base seconds for exponential backoff when Adobe gives no Retry-After |
maxRetryDelay |
60.0 |
never wait longer than this per retry; a longer Retry-After fails fast |
autoEtag |
true |
send If-Match on updates using the ETag from the last get() |
defaultSignatureLevel |
SignatureLevel::SES |
level for AgreementInfo agreements that don't set one |
userAgent |
conceptlz-adobe-sign-php/<version> |
How the SDK behaves:
- Data centers. Acrobat Sign accounts live on different data centers ("shards"). The SDK looks
yours up with
GET /baseUrisand caches it (use a shared PSR-16 cache in production). If the account moves, the API answers403 INVALID_API_ACCESS_POINT; the SDK then looks the data center up again, once. - Rate limits. Retries follow
Retry-After(in seconds or as an HTTP date), or the "try again in N seconds" text in Adobe's error message, or else exponential backoff with jitter.
Generated models
Conceptlz\AdobeSign\Generated\Model holds 313 classes generated from Adobe's spec (spec/adobe-sign-v6.json).
They're plain PHP with getters and setters and no runtime dependencies. Resources return them
directly, and Hydration\ModelMapper converts between them and arrays:
use Conceptlz\AdobeSign\Hydration\ModelMapper; use Conceptlz\AdobeSign\Generated\Model\FetchedAgreementInfo; $model = ModelMapper::hydrate(FetchedAgreementInfo::class, $array); $array = ModelMapper::toArray($model); // only fields that were set
Fields Adobe adds later are ignored, not fatal. Status and enum values are plain strings, because Adobe adds new values without a version change.
Testing your integration
Inject a mock PSR-18 client, for example
php-http/mock-client, and queue the responses:
$http = new Http\Mock\Client(); $http->addResponse(new Nyholm\Psr7\Response(200, ['Content-Type' => 'application/json'], '{"apiAccessPoint":"https://api.na1.adobesign.com/","webAccessPoint":"https://secure.na1.adobesign.com/"}')); $http->addResponse(new Nyholm\Psr7\Response(200, ['Content-Type' => 'application/json'], '{"id":"agr-1"}')); $sign = AdobeSign::withIntegrationKey('test', httpClient: $http); $sign->agreements()->create($info); $sent = $http->getRequests()[1]; // assert on what was sent
The first request a fresh client sends is always GET /baseUris. To skip it, share a PSR-16 cache
that already holds the data center, or use an OAuth token, which carries its own.
Examples
| File | What it shows |
|---|---|
examples/send-agreement.php |
upload, create, status, reminder, download (--dry-run works offline) |
examples/live-lifecycle.php |
the full flow against a real account: signing order, wait for signatures, save the signed PDF |
examples/webhook-receiver.php |
a webhook endpoint in plain PHP |
examples/oauth-server.php |
OAuth connect, refresh and revoke with PHP's built-in server |
License
MIT. See LICENSE.