Adobe Sign PHP SDKconceptlz/adobe-sign GitHub

Start here

How Acrobat Sign works

Acrobat Sign has its own vocabulary: transient documents, participant sets, states that aren't statuses, shards. This page explains each idea in plain words and shows where it appears in the SDK, so the rest of the documentation (and Adobe's error messages) make sense.

The big picture

You upload one or more documents. You create an agreement that bundles those documents with a list of participant sets, each with a role (sign, approve, fill in a form…) and a place in the signing order. Adobe emails whoever is first, records every step in an audit trail, moves the agreement through a series of statuses, and when the last person has acted, seals the final PDF. You follow along by polling or through webhooks, then download the signed file.

  • DocumentA file to be signed. Uploaded temporarily (transient), kept as a reusable template (library) or fetched from a URL.
  • AgreementOne signing transaction: documents + participants + options. Has an id, a status and a history.
  • Participant setOne step of the workflow: a role, an order number and one or more members.
  • MemberA person (email address) in a participant set. Any one member can act for the whole set.
  • StatusWhere the agreement is now, as Adobe reports it: OUT_FOR_SIGNATURE, SIGNED, CANCELLED…
  • EventOne entry in the audit trail: created, emailed, viewed, signed, with who, when and from where.
  • Data centreThe regional cluster (shard) your account lives on, such as na4 or eu1. Every API call must go there.

Documents: transient, library and URL

Acrobat Sign never signs a file "in place" on your server. It needs its own copy, and there are three ways to give it one. All three end up as an entry in the agreement's list of files (fileInfos in the API, FileInfo in the SDK).

KindLifetimeHow you get oneUse it for
Transient document 7 days transientDocuments()->upload($file) → FileInfo::transientDocument($id) The normal case: a contract generated or chosen for this agreement.
Library document Until deleted Created in the web app or with libraryDocuments()->create() → FileInfo::libraryDocument($id) A template sent many times (a standard NDA), often with form fields placed once in Adobe's editor.
URL document Fetched at creation FileInfo::url($url, $name) A file already on a public HTTPS address. Adobe downloads it itself.

"Transient" catches people out. The upload is a staging area, not storage: the id stops working after seven days, and you can't list or download transient documents. Once an agreement has been created from it, the agreement holds its own copy, so the seven-day limit no longer matters. Details and edge cases are in Uploading documents.

Agreements

An agreement is the unit of work in Acrobat Sign: "these documents, for these people, in this order". It has a name (shown in emails and the web app), an id that looks like CBJCHBCAABAA…, a sender, a status and an event history. Once sent, its documents are fixed: you can change some details (the message, the expiry, participants in some cases) but not the file being signed. To send a corrected document, cancel and send a new agreement, or use agreements()->copy() to start a new draft with the same set-up.

Adobe merges the documents in the order you added them. At the end you can download each one separately or all of them as one combined PDF, with or without the audit report.

Developer accounts rename your agreements

On a free developer account, Adobe prefixes every agreement name with [DEMO USE ONLY]. If your code compares names (it shouldn't; compare ids or your externalId), the prefix will surprise it.

Participant sets and members

The most important idea in the Acrobat Sign data model is that you don't add people to an agreement; you add participant sets. A set is one step of the workflow. It has:

  • a role: what this step does (sign, approve, accept, fill in fields…);
  • an order: when this step happens (1, 2, 3…; sets with the same number happen in parallel);
  • one or more members: the people who can perform it.

The rule that matters: any one member can complete the set for everyone. A set with two members is "either of these people signs", not "both of these people sign". If you need both signatures, use two sets.

One set with two members versus two sets
use Conceptlz\AdobeSign\Builder\ParticipantSetInfo;

// Either partner may sign for the firm: ONE signature is collected
ParticipantSetInfo::signer('alice@firm.example', 'bob@firm.example');

// Both must sign, at the same time: TWO signatures are collected
ParticipantSetInfo::signer('alice@firm.example')->order(1);
ParticipantSetInfo::signer('bob@firm.example')->order(1);

The "group" form is useful for shared mailboxes and teams ("anyone in accounts payable can approve"). Every member receives the email; the first to act completes the step and the others are released.

Roles

The role decides what Adobe asks the person to do and which emails they get. The SDK has a ParticipantRole enum with every role the v6 API accepts, plus shortcuts for the two common ones (ParticipantSetInfo::signer() and ::approver()). Any other role goes through ParticipantSetInfo::role().

RoleWhat the person does
SIGNERReviews and signs. The only role that produces a signature, and the only one a signature level (SES/AES/QES) can apply to.
APPROVERReviews and approves without signing. Typical for internal sign-off ("legal approves before the CEO signs").
ACCEPTORAccepts the document, e.g. acknowledging a policy, without a signature field.
FORM_FILLERFills in form fields (prices, dates, addresses) before others sign.
CERTIFIED_RECIPIENTConfirms delivery: Adobe records that they received the document.
DELEGATE_TO_SIGNER, DELEGATE_TO_APPROVER, …Doesn't act themselves but chooses who will (an assistant routing to the right signatory).
WITNESS, NOTARY_SIGNER, IN_PERSON_SIGNER, HOST, ELECTRONIC_SEALERSpecialised workflows (witnessed signing, notarisation, in-person signing, organisation seals). They depend on account features.

Responses can contain two more roles you never send: SENDER and SHARE. They describe the agreement's owner and people it was shared with. Treat roles (and statuses) as open-ended strings: Adobe adds values without changing the API version, which is why the SDK's models keep them as plain strings.

CCs and the sender

A CC receives copies of the emails and the final PDF but takes no action; add CCs with AgreementInfo::cc(). The sender is the Acrobat Sign user who created the agreement. They own it, can cancel or remind, and receive status emails. The sender doesn't sign unless you also add them as a participant.

State versus status

The API uses two similar words for two different things, and mixing them up is the most common source of confusion.

  • StateWhat you ask for: the stage you want the agreement to be in. You set it when creating (AgreementInfo::state()) and change it with agreements()->updateState(). Adobe never returns it in GET /agreements/{id}.
  • StatusWhat Adobe reports: where the agreement actually is. Read it with agreements()->status($id) or get($id)->status.
STATE · WHAT YOU ASK FOR DRAFT saved, not sent AUTHORING fields placed in Adobe's editor IN_PROCESS send now (the default) STATUS · WHAT ADOBE REPORTS DOCUMENTS_NOT_YET_PROCESSED a few seconds after create() OUT_FOR_SIGNATURE or OUT_FOR_APPROVAL, … SIGNED everyone has acted repeats for each step of the order CANCELLED cancel() or declined EXPIRED past expiresAt()
The top row is what you request; the bottom rows are what status() returns. SIGNED, CANCELLED and EXPIRED are final: nothing moves an agreement out of them.

The three creation states

State (AgreementState)What happensWhen to use it
IN_PROCESSSent immediately to the first step of the order. The SDK's default.Almost always.
AUTHORINGCreated but not sent, ready for someone to place signature and form fields in Adobe's editor.Your documents have no field tags and a person should position fields by hand before sending.
DRAFTSaved as an incomplete draft.You build the agreement over several steps before it is ready.

Adobe's allowed path is DRAFT → AUTHORING → IN_PROCESS → CANCELLED, changed with agreements()->updateState($id, 'IN_PROCESS') and friends. Draft and authoring agreements don't expire: the expiry countdown starts when the agreement is sent.

Statuses you will see

StatusMeaning
DOCUMENTS_NOT_YET_PROCESSEDAdobe is still converting and preparing the files. Lasts a few seconds after creation. Some calls (signing links, for example) fail until it passes.
OUT_FOR_SIGNATUREWaiting for one or more signers. The usual "in progress" status.
OUT_FOR_APPROVAL, OUT_FOR_ACCEPTANCE, OUT_FOR_FORM_FILLING, OUT_FOR_DELIVERYSame idea, when the current step's role isn't a signer.
SIGNEDEveryone has acted. The final PDF is sealed and ready to download.
APPROVED, ACCEPTED, DELIVERED, FORM_FILLEDCompleted agreements whose participants approved, accepted, received or filled rather than signed.
CANCELLEDStopped early: the sender cancelled it, or a participant declined. The events tell you which.
EXPIREDThe expiry date passed before everyone acted.
DRAFT, AUTHORINGNot yet sent.
WAITING_FOR_VERIFICATION, WAITING_FOR_REVISION, PAUSED, …Less common workflow pauses. The spec lists more, and Adobe may add new ones.

The SDK's Agreement model adds two shortcuts for the statuses most code cares about: isSigned() and isOutForSignature(). For anything else compare $agreement->status as a string, and give unknown values a sensible default.

Each step has a status too

agreements()->members($id) returns every participant set with its own status. That is how you tell whose turn it is:

Participant set statusMeaning
WAITING_FOR_MY_SIGNATURE (or _APPROVAL, _ACCEPTANCE, _FORM_FILLING, …)It is this step's turn now.
NOT_YET_VISIBLEA later step; these people haven't been emailed yet.
WAITING_FOR_OTHERSThis step is done, and others still have to act.
COMPLETEDThis step is done and the agreement is finished.
CANCELLED, EXPIRED, REMOVEDThe step will never happen.

Pass includeNextParticipantSet: true to also get nextParticipantSets, the steps whose turn it is. Adobe leaves that key out unless you ask, even though the spec lists it. The signing order guide shows both techniques.

The first few seconds

Creating an agreement returns its id immediately, but Adobe processes the documents in the background. For a few seconds the status is DOCUMENTS_NOT_YET_PROCESSED, some calls answer AGREEMENT_NOT_EXPOSED or AGREEMENT_NOT_SIGNABLE (both surface as NotFoundException), and participant data can lag behind the status. Code that creates and then immediately inspects an agreement should poll briefly:

Waiting out processing
$status = $sign->agreements()->status($agreementId);
for ($i = 0; $status === 'DOCUMENTS_NOT_YET_PROCESSED' && $i < 15; $i++) {
    sleep(2);
    $status = $sign->agreements()->status($agreementId);
}

If a conversion fails (a corrupt or password-protected file, for example), Adobe cancels the agreement itself and records an AUTO_CANCELLED_CONVERSION_PROBLEM event.

Events and the audit trail

Acrobat Sign records everything that happens to an agreement as an event: created, emailed (ACTION_REQUESTED), email opened (EMAIL_VIEWED), documents viewed, identity checked (EMAIL_OTP_AUTHENTICATED…), signed (ACTION_COMPLETED; DIGSIGNED for a certificate-based signature), delegated, declined (REJECTED), cancelled (RECALLED), expired. Each event carries a date, the participant's email, the acting user and, for signatures, details such as the input type (typed, drawn, image) and the IP address.

The same history is available in three forms:

FormSDKGood for
Event list (JSON)agreements()->events($id)Showing progress in your UI, analytics, checking what happened.
Audit report (PDF)agreements()->auditTrail($id), or combinedDocument($id, attachAuditReport: true)Evidence: the human-readable record you keep with the contract.
Push notificationswebhooks()->create(…)Reacting as things happen, without polling.
Printing the history
foreach ($sign->agreements()->events($agreementId)->getEvents() ?? [] as $event) {
    printf("%s  %-22s %s\n", $event->getDate(), $event->getType(), $event->getParticipantEmail());
}
2026-11-03T14:02:11Z  CREATED                sender@example.com
2026-11-03T14:02:13Z  ACTION_REQUESTED       jane@example.com
2026-11-03T14:05:40Z  EMAIL_VIEWED           jane@example.com
2026-11-03T14:06:02Z  ACTION_COMPLETED       jane@example.com

Data centres (shards) and base URIs

Acrobat Sign runs in several regional data centres. Adobe calls them shards, and their names appear in host names: na1, na4, eu1, eu2, jp1, au1 and so on. An account lives in exactly one, chosen when it was created (an EU customer is usually on an EU shard, which matters for data residency).

Every account has two addresses on its shard:

Name in the APIExampleWhat lives there
apiAccessPointhttps://api.na4.adobesign.com/The REST API. The SDK appends api/rest/v6.
webAccessPointhttps://secure.na4.adobesign.com/The web app, and the pages people sign on. Signing links look like https://secure.na4.adobesign.com/public/apiesign?pid=….

Calls sent to the wrong shard fail with 403 INVALID_API_ACCESS_POINT. Adobe provides a shard-neutral host, api.adobesign.com, whose GET /baseUris tells you where an account lives. The SDK handles all of this:

  1. Discover once

    On the first call it asks GET /baseUris with your credentials.

  2. Cache

    It stores the answer in the PSR-16 cache you passed (in memory by default) for 24 hours, keyed by a hash of the credential, never the credential itself.

  3. Recover

    If Adobe ever answers INVALID_API_ACCESS_POINT (the account was moved), it forgets the cached shard, rediscovers it and retries the request once.

OAuth is simpler still: Adobe returns the shard (api_access_point) with the token, so OAuth clients never need GET /baseUris. US government accounts live on a separate domain (adobesigngov.us); for those, change ClientOptions::$bootstrapUrl as described in Configuration.

Users: sender, acting user and participant

Three different "who" questions come up, and the API answers each in a different place.

WhoMeaningWhere you see it
The API callerThe Acrobat Sign user your credentials act as. An Integration Key acts as the user who created it; an OAuth token acts as the user who authorised your app.users()->me()
The senderThe user who created an agreement. Usually the API caller at the time of creation.get($id)->senderEmail; senderInfo in members()
A participantSomeone in a participant set. They need no Acrobat Sign account.members($id); participantEmail on events
The acting user of an eventWho actually did it, which differs from the participant when someone delegated or a sender acted on a participant's behalf.actingUserEmail on events

An account administrator's credentials can act as another user in the same account, so that agreements are sent from the right person's name and appear in their list. The SDK exposes this as asUser(), which adds Adobe's x-api-user header:

Sending as a colleague
// Same account, different user: the agreement shows maria@company.example as the sender
$asMaria = $sign->asUser('maria@company.example');
$agreementId = $asMaria->agreements()->create($agreement);

// $sign itself is unchanged and still acts as the key's owner
$sign->agreements()->all();

Users also belong to groups, which carry their own settings (email templates, signing options, which identity checks are allowed). An agreement belongs to the sender's primary group unless you set another. Many "why does this work for one user but not another?" questions come down to group settings. Authentication covers asUser() in depth.

Where next

Common questions

I set the state to IN_PROCESS but the status says OUT_FOR_SIGNATURE. Is that wrong?

No, that is exactly right. IN_PROCESS is the state you asked for ("send it"), and OUT_FOR_SIGNATURE is the status Adobe reports once it has. States are requests; statuses are facts. Adobe never returns the state in GET /agreements/{id}.

I put two people in one participant set and only one signature was collected. Why?

A participant set is completed by any one of its members. To collect both signatures, give each person their own set. Give them the same order() if they may sign at the same time, or different orders if one must sign first.

Do signers need an Adobe account?

No. Participants sign through a link in the email (or an embedded signing link you give them). Only senders and API callers need Acrobat Sign accounts. You can require signers to log in to an Adobe account as an identity check (Verification::adobeAccount()), but that is optional.

Can I change the document after sending?

Not the content being signed: changing it would undermine the signatures already collected. Cancel the agreement and send a new one. agreements()->copy($id) creates a draft with the same documents and participants as a starting point. You can update some details in flight, such as the message or expiry; see Managing agreements.

How do I find out which data centre my account is on?

Look at the web app's address bar after logging in: secure.na4.adobesign.com means na4. You never need to configure it in the SDK, which discovers and caches it. If you log with a PSR-3 logger, the request URLs show it too.

How do I tell a declined agreement from one I cancelled?

Both end with status CANCELLED. Read the events: a participant declining is recorded as REJECTED (with their comment), the sender cancelling as RECALLED. Webhooks report the same distinction as separate event types.

My transient document expired. Is the agreement I created from it affected?

No. The seven-day limit applies to the upload id only. An agreement keeps its own copy of the file from the moment it is created, for as long as the agreement exists in your account.