Guides
Signing order
"The client signs first, then our CFO and legal team in parallel, then the CEO countersigns." This page shows how
to express any order like that with one order() call per step, which orders Adobe accepts and which need
an account setting, how to find out whose turn it is, and how to hand the current signer a link for embedded signing.
The rule in one sentence
Each participant set gets an order number starting at 1; sets with the same number act at the same time, and a number only starts when every set with a lower number has finished. Adobe enforces this on its side: people in a later step aren't emailed, can't see the document and get no signing link until their turn comes.
use Conceptlz\AdobeSign\Builder\{AgreementInfo, ParticipantSetInfo};
$agreement = AgreementInfo::create('Master Services Agreement')
->file($documentId)
->participantSet(ParticipantSetInfo::signer('client@customer.example')->order(1))
->participantSet(ParticipantSetInfo::signer('cfo@company.example')->order(2))
->participantSet(ParticipantSetInfo::approver('legal@company.example')->order(2))
->participantSet(ParticipantSetInfo::signer('ceo@company.example')->order(3));
Common patterns
| You want | Orders | How | Account setting |
|---|---|---|---|
| A, then B, then C | 1, 2, 3 | ->signers('a@x.com', 'b@x.com', 'c@x.com') | None |
| A, B and C at the same time | 1, 1, 1 | ->order(1) on every set | None |
| A first, then B and C in any order, then D | 1, 2, 2, 3 | Explicit order() per set | Hybrid routing |
| Approval before signature | 1, 2 | approver(...)->order(1), signer(...)->order(2) | None |
| Anyone in a team signs, once | 1 | One set, several members: signer('a@x.com', 'b@x.com') | None |
Sequential
The default. Each set waits for the one before. When the order expresses authority (an employee signs, then a manager countersigns), sequential is usually what the business means.
// Three equivalent ways to write 1, 2, 3
$agreement->signers('employee@example.com', 'manager@company.example', 'hr@company.example');
$agreement->participantSet(ParticipantSetInfo::signer('employee@example.com'))
->participantSet(ParticipantSetInfo::signer('manager@company.example'))
->participantSet(ParticipantSetInfo::signer('hr@company.example'));
$agreement->participantSet(ParticipantSetInfo::signer('employee@example.com')->order(1))
->participantSet(ParticipantSetInfo::signer('manager@company.example')->order(2))
->participantSet(ParticipantSetInfo::signer('hr@company.example')->order(3));
Parallel
Everyone is emailed at once and can act in any order; the agreement completes when the last one has acted. Good for co-signers of equal standing (two founders, joint tenants) and the fastest way to collect signatures.
$agreement->participantSet(ParticipantSetInfo::signer('tenant1@example.com')->order(1))
->participantSet(ParticipantSetInfo::signer('tenant2@example.com')->order(1))
->participantSet(ParticipantSetInfo::signer('landlord@example.com')->order(1));
Mixed ("hybrid")
Some steps sequential, some parallel, as in the diagram above. This is the most natural fit for many real processes and the one that needs an account setting, covered in hybrid routing.
"Any one of these people"
Not an ordering feature, but often confused with parallel signing: a single set with several members is completed by whichever member acts first. Use it for "any director may sign for the company". Use parallel sets when you need everyone's signature.
// ONE signature from any director, then the customer
$agreement->participantSet(ParticipantSetInfo::signer('dir1@company.example', 'dir2@company.example')->order(1))
->participantSet(ParticipantSetInfo::signer('customer@example.com')->order(2));
How sets without order() are numbered
You can leave order() out. The builder then numbers each unnumbered set one more than the set added
just before it (the first set gets 1). Explicit numbers are kept as they are. That gives intuitive results when
you number everything or nothing, and surprising ones when you mix:
| Sets added | Orders sent | Meaning |
|---|---|---|
signer(a), signer(b), signer(c) | 1, 2, 3 | Sequential, as expected |
signer(a), signer(b)->order(1), signer(c) | 1, 1, 2 | a and b together, then c |
signers(a, b), approver(c)->order(2) | 1, 2, 2 | c approves in parallel with b, which may not be what you meant |
signer(a)->order(2), signer(b) | 2, 3 | Rejected: there is no step 1 |
Number all or nothing
For anything beyond a simple sequence, call order() on every set. Reviewers can then read the workflow straight off
the code, and moving a line doesn't silently change who signs when. $set->getOrder() returns the explicit number
(or null), and $agreement->toArray()['participantSetsInfo'] shows the final numbers.
The consecutive-order rule
Adobe's spec requires the distinct order numbers to form "a valid consecutive increasing sequence": exactly 1, 2, …, n with no gaps, repeats allowed. Adobe rejects anything else, but with an error that doesn't say which number is wrong. The builder checks the rule locally, so a bad order never reaches the API:
| Orders | Valid? | Why |
|---|---|---|
| 1, 2, 3 | Valid | Sequential |
| 1, 1, 2 | Valid | Repeats are parallel steps |
| 2, 1, 2 | Valid | The order you add sets in doesn't matter, only the numbers |
| 1, 3 | Invalid | Gap: there is no 2 |
| 2, 2 | Invalid | Must start at 1 |
| 0, 1 | Invalid | order(0) throws immediately: orders start at 1 |
Conceptlz\AdobeSign\Exception\InvalidArgumentException:
Signing orders must run 1..n without gaps (parallel sets share a number); got 1, 3.
The check runs when the agreement is built (toArray(), and so create()). It applies to builder agreements
only; if you send a raw array or a generated model, Adobe does the checking.
Gaps usually appear when steps are optional: "legal reviews at step 2, but only for contracts over $100k". Build the list of steps first and number them afterwards, rather than hard-coding numbers:
$steps = [[ParticipantSetInfo::signer($deal->customerEmail)]];
if ($deal->value > 100_000) {
$steps[] = [ParticipantSetInfo::approver('legal@company.example')];
}
$steps[] = [
ParticipantSetInfo::signer('cfo@company.example'), // these two share a step
ParticipantSetInfo::signer('sales-director@company.example'),
];
foreach ($steps as $i => $sets) {
foreach ($sets as $set) {
$agreement->participantSet($set->order($i + 1)); // always 1..n
}
}
Mixed orders need hybrid routing
Purely sequential orders (1, 2, 3) and purely parallel ones (1, 1, 1) always work. Mixing them (1, 2, 2, 3) is what Acrobat Sign calls hybrid routing, and it is an account setting that is off on many accounts, including new developer accounts. When it is off, Adobe refuses the agreement:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"code":"HYBRID_ROUTING_NOT_ALLOWED","message":"Hybrid routing is not enabled for this user"}
The SDK maps that code to AccountCapabilityException (a subclass of ApiException). The name says what to do:
the request is valid, but the account doesn't allow it, so the fix is in Acrobat Sign's admin settings, not in your code. Nothing was
created, and nobody was emailed.
use Conceptlz\AdobeSign\Exception\AccountCapabilityException;
try {
$agreementId = $sign->agreements()->create($mixedOrderAgreement);
} catch (AccountCapabilityException $e) {
if ($e->errorCode !== 'HYBRID_ROUTING_NOT_ALLOWED') {
throw $e;
}
// Same people, strictly one after another: always allowed
$logger->notice('Hybrid routing is off; sending sequentially. Ask an Acrobat Sign admin to enable it.');
$agreementId = $sign->agreements()->create($sequentialAgreement);
}
Enabling it
-
Find an account administrator
Only an Acrobat Sign account (or group) admin can change send settings. On a developer account, that is you.
-
Open the send settings
In the Acrobat Sign web app, go to the account settings and open the Send Settings page. Look for the signing-order options and allow hybrid (mixed sequential and parallel) routing. Adobe's labels and menu layout change over time; Adobe's help pages on "hybrid signing order" describe the current location.
-
Check the group, too
Settings can be overridden per group, and the error is about the sending user. If the account allows it but one user still gets the error, check the settings of that user's group.
-
Try again
Re-run the request; no change in your code is needed. The package's live-lifecycle example (
"a@x.com,b@x.com+c@x.com") is a quick way to confirm.
Whose turn is it?
Your UI will want to say "waiting for the CFO and legal", and your reminder job needs to know who to nudge. The answer comes from the participants endpoint.
agreements()->members(string $agreementId, bool $includeNextParticipantSet = false): arrayGET /agreements/{agreementId}/members
Returns the raw MembersInfo array: participantSets (every step, each with order,
role, status and memberInfos), senderInfo, ccsInfo and
sharesInfo. With includeNextParticipantSet: true it also contains nextParticipantSets, the steps
whose turn it is now. Adobe omits that key unless you ask, even though its spec lists it.
$members = $sign->agreements()->members($agreementId, includeNextParticipantSet: true);
// 1. Ask Adobe directly
$waitingFor = [];
foreach ($members['nextParticipantSets'] ?? [] as $set) {
foreach ($set['memberInfos'] ?? [] as $member) {
$waitingFor[] = $member['email'];
}
}
// 2. Or read each step's status (works without the flag)
foreach ($members['participantSets'] ?? [] as $set) {
$emails = implode(', ', array_column($set['memberInfos'] ?? [], 'email'));
printf("%d %-9s %-26s %s\n", $set['order'], $set['role'], $set['status'], $emails);
}
1 SIGNER WAITING_FOR_OTHERS client@customer.example
2 SIGNER WAITING_FOR_MY_SIGNATURE cfo@company.example
2 APPROVER WAITING_FOR_MY_APPROVAL legal@company.example
3 SIGNER NOT_YET_VISIBLE ceo@company.example
A step whose status starts with WAITING_FOR_MY_ is up now; NOT_YET_VISIBLE is a later step whose members
haven't been told about the agreement yet. Match people by email or by member id, not by the order field of
nextParticipantSets entries, which doesn't always mirror the step number.
Give it a few seconds after creating
Right after create(), the status is DOCUMENTS_NOT_YET_PROCESSED, and even once it reads
OUT_FOR_SIGNATURE, participant data can lag for a moment, so nextParticipantSets may briefly be empty. Code that
shows "whose turn" right after sending should retry for a few seconds, or wait for the first webhook.
The SDK uses the same information for reminders: agreements()->sendReminder($id) with no participant ids reminds whoever's
turn it is, falling back to WAITING_FOR_MY_* statuses if Adobe leaves out nextParticipantSets. With webhooks,
AGREEMENT_ACTION_COMPLETED tells you a step finished and AGREEMENT_ACTION_REQUESTED that the next one was asked to
act; see Webhooks.
Signing URLs for embedded signing
By default, Adobe emails each person when their turn comes. For embedded signing, you show the signing page inside your own application instead: the customer finishes checkout and signs on the next screen, or a field agent hands a tablet to a client. Adobe gives you a signing URL for each person whose turn it currently is.
agreements()->signingUrls(string $agreementId, ?int $expiry = null, ?string $frameParent = null): SigningUrlResponseGET /agreements/{agreementId}/signingUrls
Returns the generated SigningUrlResponse: getSigningUrlSetInfos() gives one entry per current step, each with
getSigningUrls() (objects with getEmail() and getEsignUrl()). $expiry is the link lifetime
in seconds, from 30 to 315,360,000 (ten years); outside that range throws InvalidArgumentException before the call. Leave it out
to use the account's setting. $frameParent is the domain that will host the page in an iframe.
Throws NotFoundException (AGREEMENT_NOT_SIGNABLE, or AGREEMENT_NOT_EXPOSED) when nobody can sign yet,
typically in the first seconds after creation; retry shortly.
agreements()->signingUrlFor(string $agreementId, string $email, ?int $expiry = null): ?string
The signing link for one person, comparing emails case-insensitively, or null if it isn't their turn (or they aren't a signer
on this agreement). The usual call in a web app, where you know who is logged in.
Signing links point at your account's data centre, for example https://secure.na4.adobesign.com/public/apiesign?pid=….
-
Create the agreement
Optionally switch Adobe's initial emails off and set a post-sign redirect back to your app (see email options).
-
Wait until it is signable
Retry
signingUrlFor()onNotFoundExceptionfor a few seconds while Adobe processes the documents. -
Authenticate your user, then fetch the link
Request the link when the person opens the page, not in advance, and only after your own login has confirmed who they are.
-
Redirect or embed
Redirect the browser to the link, or load it in an iframe on the
$frameParentdomain. After signing, Adobe sends them to your redirect URL.
use Conceptlz\AdobeSign\Exception\NotFoundException;
public function sign(string $contractId): Response
{
$contract = $this->contracts->findForUser($contractId, $this->auth->user()); // your access check
for ($attempt = 1; $attempt <= 5; $attempt++) {
try {
$url = $this->sign->agreements()->signingUrlFor(
$contract->adobeAgreementId,
$this->auth->user()->email,
expiry: 600, // 10 minutes is plenty to start
);
break;
} catch (NotFoundException) {
sleep(2); // still processing; try again shortly
$url = null;
}
}
if ($url === null) {
return $this->view('contracts/not-your-turn', ['contract' => $contract]);
}
return $this->redirect($url);
}
A signing URL is a key to someone's signature
Whoever opens the link can sign as that person, unless an identity check is configured. Don't log signing URLs, don't put them in
analytics, and don't send them over channels you wouldn't trust with a password. Generate them on demand, after your own
authentication, with a short $expiry. For higher assurance, add an identity check such as
Verification::emailOtp() or phone() to the participant, so the link alone isn't enough.
Embedded signing and evidence
When Adobe emails the signer, the audit trail shows that the link was delivered to their address, which is part of the evidence linking the signature to them. With embedded signing that link comes from your login instead. Keep your own record of how the user authenticated (time, method, IP) alongside the agreement id, so that together with Adobe's audit report you can show who signed.
Where next
Common questions
Why do I get HYBRID_ROUTING_NOT_ALLOWED?
Your order mixes sequential and parallel steps (for example 1, 2, 2, 3), and the sending user's account or group doesn't allow that.
An Acrobat Sign admin can enable hybrid routing in the send settings; until then, use a purely sequential or purely parallel order.
The SDK raises it as AccountCapabilityException, and nothing was sent. See Hybrid routing.
Can I skip an order number, say 1 then 3?
No. Adobe requires the numbers to run 1 to n without gaps, and the builder rejects gaps before sending. Build the list of steps first and number them afterwards, as in the optional-steps example.
Can I change the order after sending?
Not reliably through this SDK. Adobe allows some participant changes in flight, but reordering steps that may already have acted isn't
something to depend on. Cancel and send a new agreement; agreements()->copy($id) gives you a draft with the same documents
and participants to start from.
nextParticipantSets is missing or empty. Why?
Either you didn't pass includeNextParticipantSet: true (Adobe only includes the key on request), the agreement is still
processing (wait a few seconds after creation), or it is finished, cancelled or expired, so nobody is up. Each step's
status in participantSets tells you which.
signingUrlFor() returns null for a signer. What's wrong?
It isn't that person's turn yet (an earlier step hasn't finished), they have already signed, or the email doesn't match a signer on the
agreement. Signing URLs exist only for people who can act right now. Check members() to see where the agreement is.
How do I make our company always sign last?
Give your signatory the highest order number. A common reason is legal: the counterparty's signature is their offer, and your signature last is the acceptance, so the contract is only binding once you have checked their side. The audit trail records the sequence.
What's the difference between two sets with order 1 and one set with two members?
Two sets with order 1 collect two signatures, in any order. One set with two members collects one signature, from whichever member acts first. Use the first for co-signers, the second for "any authorised person".