Guides
Library, web forms, users and bulk sends
Beyond one-off agreements, Acrobat Sign has reusable templates, public web forms anyone can sign,
bulk sends that create one agreement per recipient, and the users who own all of it. This page shows
the main methods of libraryDocuments(), widgets(), megaSigns()
and users(), with the paging pattern they share.
Overview
| Accessor | Adobe calls it | What it is | Methods |
|---|---|---|---|
libraryDocuments() | library documents | reusable templates | all, get, create, update, updateState, documents, document, combinedDocument, auditTrail, formFields, members, events |
widgets() | web forms | a public signing link; each signature creates an agreement | all, get, create, update, updateState, agreements, documents, members, formFields, events, formData, combinedDocument, auditTrail |
megaSigns() | Send in Bulk | one document sent as a separate agreement to each recipient | all, get, create, updateState, cancel, agreements, sendReminder, members, events, formData, combinedDocument, errorReport |
users() | users | people in the account | all, get, me, create, update, groups, updateState, events |
Common to all four:
- Return values are generated models from
Conceptlz\AdobeSign\Generated\Model, with getters for every field in Adobe's spec (see Models & raw data). create()andupdate()accept the generated model or a raw array. There are no fluent builders for these resources, so the examples use arrays shaped like the spec.- Updates replace the whole object, so fetch with
get(), change it and send it back; the fetch also makes the client sendIf-Match, and a conflict throwsPreconditionFailedException(see ETags in practice). - Files (
combinedDocument(),auditTrail(),formData(),errorReport()) come back as PSR-7 streams, handled exactly like agreement downloads.
The paging pattern
Every method that lists things (all() on each resource, widgets()->agreements(),
megaSigns()->agreements(), users()->events(), and
agreements()->all()) returns a PHP \Generator that walks Adobe's cursor
pagination for you:
- Nothing is requested until you start iterating.
- The next page is fetched only when you reach the end of the current one.
$pageSize(default 100) is the number of rows per request, not a cap on the total.- Paging stops when Adobe returns no next cursor, or returns the same cursor twice.
// Stream through everything, one page in memory at a time
foreach ($sign->libraryDocuments()->all() as $template) {
echo $template->getId(), ' ', $template->getName(), "\n";
}
// Stop early: no further pages are requested after break
foreach ($sign->users()->all() as $user) {
if (strcasecmp($user->getEmail(), 'finance-lead@example.com') === 0) {
$userId = $user->getId();
break;
}
}
// The first 10 only
foreach (new LimitIterator($sign->widgets()->all(pageSize: 10), 0, 10) as $form) {
// ...
}
// Everything into an array: fine for small accounts, downloads every page
$megaSigns = iterator_to_array($sign->megaSigns()->all(), preserve_keys: false);
A generator can be iterated only once. To loop twice, call the method again, or collect the results
into an array the first time. Most list methods also take filters such as groupId and a
showHidden… flag; named arguments keep those calls readable.
Library documents (templates)
A library document is a document stored in the account for reuse: a standard NDA, an onboarding pack, or a form field layer that can be laid over other documents. Upload once, then reference it from any number of agreements without re-uploading. Transient documents expire after seven days; library documents stay until you remove them.
Creating a template
$transientId = $sign->transientDocuments()->upload('/templates/mutual-nda.pdf');
$templateId = $sign->libraryDocuments()->create([
'name' => 'Mutual NDA (2026)',
'fileInfos' => [['transientDocumentId' => $transientId]],
'sharingMode' => 'ACCOUNT', // USER, GROUP, ACCOUNT or GLOBAL
'templateTypes' => ['DOCUMENT'], // and/or FORM_FIELD_LAYER
'state' => 'ACTIVE', // or AUTHORING, to place fields in Adobe's editor first
]);
libraryDocuments()->create(LibraryDocumentCreationInfoV6|array $info): stringPOST /libraryDocuments
Required fields: fileInfos, name, sharingMode, state and templateTypes. Returns the new library document's id.
Using a template in an agreement
use Conceptlz\AdobeSign\Builder\{AgreementInfo, FileInfo};
$agreementId = $sign->agreements()->create(
AgreementInfo::create('NDA with Example Ltd')
->file(FileInfo::libraryDocument($templateId))
->signers('counterparty@example.com'),
);
Fields placed on the template (signature boxes, dates, text fields) come with it, which is the main reason to use templates: the layout is designed once, in Adobe's editor, by someone who isn't a developer.
Listing, updating and removing
// List (generator): filters groupId, showHiddenLibraryDocuments, includeSharees
foreach ($sign->libraryDocuments()->all(groupId: $salesGroupId) as $template) {
echo $template->getName(), ' (', implode(', ', $template->getTemplateTypes() ?? []), ")\n";
}
// Rename: fetch, modify, put (If-Match is automatic)
$template = $sign->libraryDocuments()->get($templateId);
$template->setName('Mutual NDA (2026, v2)');
$sign->libraryDocuments()->update($templateId, $template);
// Retire it: AUTHORING, ACTIVE or REMOVED
$sign->libraryDocuments()->updateState($templateId, 'REMOVED');
// Its fields, files and history
$fields = $sign->libraryDocuments()->formFields($templateId)->getFields();
$pdf = $sign->libraryDocuments()->combinedDocument($templateId);
$events = $sign->libraryDocuments()->events($templateId);
Web forms (widgets)
A web form is a public URL (or embeddable snippet) that anyone can open and sign, without you sending anything: a sign-up form, a waiver, a consent form. Each time someone signs it, Acrobat Sign creates a new agreement. The API calls web forms widgets.
Creating a web form
$transientId = $sign->transientDocuments()->upload('/forms/event-waiver.pdf');
$widgetId = $sign->widgets()->create([
'name' => 'Event waiver',
'fileInfos' => [['transientDocumentId' => $transientId]],
'state' => 'ACTIVE', // or DRAFT / AUTHORING
'widgetParticipantSetInfo' => [
'role' => 'SIGNER',
'memberInfos' => [['email' => '']], // the public signer: unknown until they sign
],
]);
widgets()->create(WidgetCreationInfoV6|array $info): stringPOST /widgets
Required fields: fileInfos, name, state and
widgetParticipantSetInfo. The web form's signer is unknown when the form is created,
so Adobe expects their email to be left empty. To have someone from your side counter-sign every
submission, add them in additionalParticipantSetsInfo.
The public link isn't on get()'s result; it's on the list rows, which carry
getUrl() and getJavascript() (the embed snippet):
foreach ($sign->widgets()->all() as $form) {
if ($form->getId() === $widgetId) {
$publicUrl = $form->getUrl();
$embed = $form->getJavascript();
break;
}
}
Following submissions
// Every agreement created by signing this form (generator)
foreach ($sign->widgets()->agreements($widgetId) as $submission) {
echo $submission->getDisplayDate(), ' ', $submission->getStatus(), ' ', $submission->getId(), "\n";
}
// All entered data as one CSV, one row per signer
$csv = $sign->widgets()->formData($widgetId)->detach();
Each submission is an ordinary agreement, so you download its signed PDF with
agreements()->combinedDocument($submission->getId()). To be told about new submissions,
subscribe a webhook to the web form with
WebhookInfo::forResource($name, $url, 'WIDGET', $widgetId) (see
Webhooks).
Switching a form off
widgets()->updateState(string $widgetId, string $state, ?array $inactiveInfo = null): voidPUT /widgets/{widgetId}/state
$state is ACTIVE, INACTIVE, AUTHORING or
CANCELLED. For a deactivated form, $inactiveInfo tells visitors what
happened: give exactly one of message or redirectUrl.
$sign->widgets()->updateState($widgetId, 'INACTIVE', ['message' => 'Registration for this event has closed.']);
// or send visitors somewhere useful
$sign->widgets()->updateState($widgetId, 'INACTIVE', ['redirectUrl' => 'https://example.com/events']);
Send in Bulk (megaSigns)
Send in Bulk sends the same document to many people, creating a separate agreement for each one: the annual policy acknowledgement for every employee, or a price-change notice to every customer. Each recipient sees only their own agreement. The API calls it a megaSign; the agreements it creates are its children.
Creating a bulk send
The recipients go in a CSV file, uploaded as a transient document. Adobe defines the CSV columns (one row per child agreement); its Send in Bulk CSV guide has the format.
$documentId = $sign->transientDocuments()->upload('/docs/policy-2026.pdf');
$recipients = $sign->transientDocuments()->upload('/tmp/recipients.csv', 'recipients.csv', 'text/csv');
$megaSignId = $sign->megaSigns()->create([
'name' => 'Code of conduct 2026',
'fileInfos' => [['transientDocumentId' => $documentId]],
'signatureType' => 'ESIGN',
'state' => 'IN_PROCESS', // or AUTHORING
'childAgreementsInfo' => [
'fileInfo' => ['transientDocumentId' => $recipients, 'fileType' => 'CSV'],
],
'message' => 'Please read and sign the 2026 code of conduct.',
]);
megaSigns()->create(MegaSignInfo|array $info): stringPOST /megaSigns
Required fields: childAgreementsInfo, fileInfos, name, signatureType and state. Returns the megaSign's id.
The project's default signature level doesn't apply here
ClientOptions::$defaultSignatureLevel is applied by the AgreementInfo builder.
Bulk sends (and web forms and library documents) are sent as raw payloads, exactly as you wrote them. If
you need digital signatures in a bulk send, set the corresponding Adobe fields yourself.
Following, reminding and cancelling
// The child agreements, one per recipient (generator)
$pending = 0;
foreach ($sign->megaSigns()->agreements($megaSignId) as $child) {
if ($child->getStatus() !== 'SIGNED') {
$pending++;
}
}
// Remind everyone who hasn't signed yet (allUnsigned defaults to true)
$sign->megaSigns()->sendReminder($megaSignId, note: 'Reminder: please sign by the 31st.');
// Rows that failed to send (bad addresses in the CSV, ...)
$errors = $sign->megaSigns()->errorReport($megaSignId)->detach();
// Stop: cancels every child agreement still in flight
$sign->megaSigns()->cancel($megaSignId, 'Superseded by the corrected version.');
megaSigns()->sendReminder(string $megaSignId, array $participantIds = [], ?string $note = null, bool $allUnsigned = true): ReminderCreationResultPOST /megaSigns/{megaSignId}/reminders
With $allUnsigned true, every participant (other than CCs and sharees) who still has to sign
is reminded. $participantIds is for sharees or CCs. Returns the generated
ReminderCreationResult (getId(), getDisabledRecipients()).
cancel($megaSignId, $comment, $notifyOthers) is a shortcut for
updateState($megaSignId, 'CANCELLED', [...]). formData() returns one CSV with the
form data of every child, and combinedDocument() the merged PDF. Each child is also an
ordinary agreement you can manage through agreements().
Users
users() manages the people in the account. Apart from me(), most calls need an
account-admin token (OAuth scopes user_read / user_write with the
:account modifier, or an admin's integration key).
// Who does this token belong to?
$me = $sign->users()->me(); // same as get('me')
echo $me->getEmail(), ' admin: ', var_export($me->getIsAccountAdmin(), true), "\n";
// Everyone, or only the admins (generator)
foreach ($sign->users()->all(accountAdminFilter: true) as $admin) {
echo $admin->getEmail(), "\n";
}
// Add a user; pass a raw array so only the fields you mean are sent
$userId = $sign->users()->create([
'email' => 'new.starter@example.com',
'firstName' => 'New',
'lastName' => 'Starter',
]);
// Their groups, and their history (generator)
foreach ($sign->users()->groups($userId)->getGroupInfoList() ?? [] as $group) {
echo $group->getName(), $group->getIsPrimaryGroup() ? ' (primary)' : '', "\n";
}
foreach ($sign->users()->events($userId) as $event) {
echo $event->getDate(), ' ', $event->getEvent(), "\n";
}
// Deactivate someone who left, with a reason
$result = $sign->users()->updateState($userId, 'INACTIVE', 'Left the company');
echo $result->getState();
users()->create(DetailedUserInfo|array $user): stringPOST /users
The spec reuses DetailedUserInfo for this request, whose list of required fields includes
values the server sets (such as id). In practice the email is what Adobe needs, so a raw
array is the cleaner choice.
To act on agreements as one of these users, rather than manage them, use
$sign->asUser('email:someone@example.com') (see Authentication).
Common questions
Transient document or library document?
A transient document is a one-off upload that expires after seven days; use it for documents you generate per agreement. A library document is stored in the account indefinitely; use it for standard documents you send again and again, especially ones with fields placed in Adobe's editor.
Where do I find a web form's public URL?
On the list rows: iterate widgets()->all(), find the form by id and call
getUrl() (or getJavascript() for the embed snippet). get()
returns the form's configuration, which doesn't include the link.
Does my default signature level apply to bulk sends and web forms?
No. The default is applied by the AgreementInfo builder, and these resources are created
from raw payloads, which the SDK sends unchanged. See Signature
levels for what the level does on agreements.
users()->all() fails with a permission error. Why?
Listing and managing users needs account-admin rights. Use an admin's integration key, or an OAuth
token with the :account scope modifier granted by an admin. users()->me()
works for anyone.
My second foreach over all() is empty. What happened?
A generator can only be iterated once. Call all() again for a fresh pass (it re-requests
the pages), or collect the first pass into an array with iterator_to_array().
Some recipients of my bulk send never got an agreement. How do I find out who?
megaSigns()->errorReport($id) returns a CSV of the rows that failed to send, typically
because of invalid addresses or missing columns. Fix those rows and send them in a new bulk send.