Guides
Uploading documents
Every agreement starts with a file. This page covers getting files to Adobe (from disk, from a stream, from a PDF you just generated), what Adobe needs to know about them, how long an upload lives, and the three ways an agreement can reference a document, including several files in one agreement.
Why upload first?
The POST /agreements request is JSON, and JSON is a poor way to carry a 10 MB PDF. So Acrobat Sign splits sending into two calls: a multipart upload that returns an id, and the agreement request that refers to the file by that id. The uploaded file is called a transient document because it is a staging copy: Adobe keeps it for seven days, and it becomes permanent only when an agreement (or a library template) is created from it.
use Conceptlz\AdobeSign\Builder\{AgreementInfo, FileInfo, ParticipantSetInfo};
$documentId = $sign->transientDocuments()->upload('/srv/contracts/msa-2026.pdf');
$agreementId = $sign->agreements()->create(
AgreementInfo::create('Master Services Agreement')
->file(FileInfo::transientDocument($documentId))
->participantSet(ParticipantSetInfo::signer('client@example.com')),
);
TransientDocuments::upload()
transientDocuments()->upload(mixed $file, ?string $fileName = null, ?string $mimeType = null, ?string $groupId = null): stringPOST /transientDocuments
Uploads one file and returns its transientDocumentId. $file may be a path (string),
an SplFileInfo, an open stream resource, or a PSR-7 StreamInterface.
$fileName is the name Adobe shows (max 255 characters); it defaults to the file's base name and is
required when uploading from a stream with no file behind it. $mimeType is optional.
$groupId files the upload under a specific Acrobat Sign group.
Throws InvalidArgumentException before sending when the path doesn't exist or isn't readable, when
$file is some other type, or when the file name is missing or longer than 255 characters.
The request body is built as it streams: the SDK copies your file into php://temp in 64 KB chunks
(PHP keeps the first 2 MB in memory and spills the rest to a temporary file), then sends that. A 200 MB scan never
sits in PHP's memory, though it does need the same amount of free temporary disk space. Seekable streams are rewound
first, so a stream you have already read from is uploaded from the start.
From a path or SplFileInfo
$id = $sign->transientDocuments()->upload('/srv/contracts/nda.pdf'); // name: nda.pdf
$id = $sign->transientDocuments()->upload(new SplFileInfo('/srv/contracts/nda.pdf'));
$id = $sign->transientDocuments()->upload('/tmp/php8a7Fq2', 'NDA - Acme Ltd.pdf'); // upload temp file, nicer name
The name matters more than it looks: it is what signers see in the email and on the signing page, and it is the name of the file in Adobe's web app. The last example shows the common case of an uploaded file in PHP's temp directory, where the base name is meaningless.
From a stream
Streams let you upload from anywhere without a local copy: cloud storage, another HTTP response, a database blob.
A stream resource opened on a real file still gets its name automatically; anything else needs $fileName.
// League\Flysystem: readStream() returns a PHP stream resource
$stream = $filesystem->readStream("tenants/$tenantId/contracts/$contractId.pdf");
$id = $sign->transientDocuments()->upload($stream, "Contract $contractId.pdf", 'application/pdf');
$response = $httpClient->sendRequest($requestFactory->createRequest('GET', $documentServiceUrl));
$id = $sign->transientDocuments()->upload(
$response->getBody(), // StreamInterface
'Quote Q-1042.pdf',
$response->getHeaderLine('Content-Type') ?: 'application/pdf',
);
From a string you generated
A string passed to upload() is always treated as a path. If you have the PDF's bytes in
a variable (from dompdf, TCPDF, a template engine or an API), wrap them in a stream first. Passing the bytes directly
fails with "File … does not exist or is not readable".
$dompdf = new Dompdf\Dompdf();
$dompdf->loadHtml($html);
$dompdf->render();
$pdfBytes = $dompdf->output(); // a string of PDF bytes, NOT a path
$stream = fopen('php://temp', 'w+b');
fwrite($stream, $pdfBytes);
rewind($stream);
$id = $sign->transientDocuments()->upload($stream, "Offer letter - {$candidate->name}.pdf", 'application/pdf');
If you already have a PSR-17 stream factory (Nyholm, Guzzle, Symfony), $factory->createStream($pdfBytes)
does the same in one line.
File names and mime types
Adobe works out the file type in this order: the Mime-Type you pass, then the content type of the file part,
then the file name's extension. When you leave $mimeType out, the SDK sends the file part as
application/octet-stream, so the extension decides. Two habits avoid surprises:
- Always give file names a real extension (
.pdf,.docx), even when they come from user input. - Pass
$mimeTypewhenever you upload from a stream or the name might lack an extension.
| File | Mime type | Notes |
|---|---|---|
application/pdf | The safest choice. Layout is exactly what you sent. | |
| Word (.docx) | application/vnd.openxmlformats-officedocument.wordprocessingml.document | Adobe converts it to PDF. Fonts and pagination may differ slightly from Word's. |
| Word (.doc) | application/msword | Converted to PDF. |
| Images (.png, .jpg) | image/png, image/jpeg | Converted to a one-page PDF; handy for scanned forms. |
| Plain text, HTML | text/plain, text/html | Converted to PDF. |
Every format is converted to PDF before signing, and conversion happens after the agreement is created. That is why
a new agreement spends a few seconds as DOCUMENTS_NOT_YET_PROCESSED. If conversion fails (a corrupt file,
a password-protected PDF, an unsupported format), Adobe cancels the agreement itself and records an
AUTO_CANCELLED_CONVERSION_PROBLEM event, which you can also receive as a webhook. Generate PDFs yourself when
you can: you then control exactly what people sign.
Size and other limits
The SDK imposes no size limit of its own, and because uploads stream, memory isn't the constraint. Adobe does enforce
limits on file size and page count per document, and they can depend on your plan; check Adobe's current documentation
if you handle large scans. Upload size limits on your side matter too: a reverse proxy or PHP's
upload_max_filesize will stop a large file long before Adobe sees it.
The seven-day lifetime
A transient document id is valid for seven days after upload. After that, Adobe rejects an agreement that refers to it, and the SDK raises the error as an ApiException subclass with Adobe's error code. A few practical consequences:
- Upload lateUpload just before creating the agreement, in the same job. Don't upload when a user drafts a contract and send next week.
- Don't store the id long-termStore the agreement id instead. A transient id is useless once the agreement exists, because the agreement has its own copy.
- One upload, many agreementsWithin the seven days, the same transient id can be used in several agreements, for example to send the same document to several customers separately.
- Can't be listedThere is no API to list, download or delete transient documents. They just expire.
Sending the same template for months?
Make it a library document instead: it never expires, and its form fields only need placing once. See library documents below.
Referencing documents: FileInfo
An agreement's documents are a list of FileInfo objects. Each one points at exactly one source. The builder has
a named constructor per source:
FileInfo::transientDocument(string $transientDocumentId): FileInfo
A file you uploaded with upload() in the last seven days. This is what you will use most. As a shortcut,
AgreementInfo::file() also accepts the id as a plain string.
FileInfo::libraryDocument(string $libraryDocumentId): FileInfo
A template from your Acrobat Sign library, including any form fields placed on it.
FileInfo::url(string $url, string $name, string $mimeType = 'application/pdf'): FileInfo
A file Adobe downloads itself. $name is the name shown to participants. The URL must be reachable from the
public internet when the agreement is created; a pre-signed S3 or Azure link with a short expiry works well.
->label(string $label): FileInfo
Names the file inside the agreement so other parts of the request can refer to it: form field templates, limited document visibility, and custom workflows (where it is required, mapping the file to the workflow's file element).
All four throw InvalidArgumentException for a blank id, URL or name, so a missing value fails in your code, not
as a puzzling 400 from Adobe.
Which source should I use?
| Transient | Library | URL | |
|---|---|---|---|
| Extra call | One upload per file | None (created once) | None |
| Lifetime of the reference | 7 days | Until deleted | Fetched at creation |
| Form fields | Text tags in the document, or authoring | Placed once, reused | Text tags in the document |
| Your server must be public | No | No | The URL must be |
| Typical use | Generated or per-deal documents | Standard forms and policies | Files already in public cloud storage |
Library documents
A library document is a reusable template stored in Acrobat Sign. You can create one in the web app (where you can also place
fields visually) or through the API from a transient upload. The SDK has no builder for library documents, so pass the
payload as an array; Adobe requires name, fileInfos, sharingMode,
state and templateTypes.
use Conceptlz\AdobeSign\Builder\FileInfo;
$transientId = $sign->transientDocuments()->upload('/srv/templates/standard-nda-v3.pdf');
$libraryId = $sign->libraryDocuments()->create([
'name' => 'Standard NDA v3',
'fileInfos' => [FileInfo::transientDocument($transientId)->toArray()],
'sharingMode' => 'ACCOUNT', // USER, GROUP, ACCOUNT, GLOBAL
'state' => 'ACTIVE', // or AUTHORING to place fields in the web app first
'templateTypes' => ['DOCUMENT'], // DOCUMENT and/or FORM_FIELD_LAYER
]);
// From now on, every NDA is one call:
$sign->agreements()->create(
AgreementInfo::create("NDA - {$counterparty}")
->file(FileInfo::libraryDocument($libraryId))
->participantSet(ParticipantSetInfo::signer($counterpartyEmail)),
);
Store the library id in your configuration. Library, web forms, users, bulk covers the other library operations (listing, updating, downloading).
Several files in one agreement
An agreement can hold several documents: a contract and its schedules, an offer letter and a policy acknowledgement. Each is
uploaded separately (one file per upload call), then added with one file() call each.
They appear in the order you add them, and participants sign them all in one session.
$uploads = $sign->transientDocuments();
$agreement = AgreementInfo::create('Supply agreement with schedules')
->file(FileInfo::transientDocument($uploads->upload('/deals/1042/contract.pdf')))
->file(FileInfo::transientDocument($uploads->upload('/deals/1042/schedule-a-pricing.xlsx',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')))
->file(FileInfo::libraryDocument($standardTermsLibraryId))
->participantSet(ParticipantSetInfo::signer('buyer@example.com'));
$agreementId = $sign->agreements()->create($agreement);
Afterwards, agreements()->documents($id) lists them with their document ids;
document($id, $documentId) downloads one, and combinedDocument($id) returns all of them merged in the same
order. See Downloading signed files.
Uploading many files? Do it before building
If an upload fails halfway through a list, nothing has been sent yet: transient documents that were never used simply expire. So it is safe to upload all files first, then build and create the agreement. There is no clean-up to do.
FileInfo fields the builder doesn't cover
Adobe's AgreementFileInfo has a few rarer options, such as notarize for notarised documents.
FileInfo doesn't model them and file() only accepts a FileInfo or an id, so replace the whole
list with AgreementInfo::with(), which is merged last and overrides what the builder produced:
$agreement = AgreementInfo::create('Notarised deed')
->file($transientId) // still needed: the builder checks there is at least one file
->participantSet(ParticipantSetInfo::signer('owner@example.com'))
->with(['fileInfos' => [
['transientDocumentId' => $transientId, 'notarize' => true],
]]);
Or skip the builder entirely: agreements()->create() also accepts a raw array or the generated
AgreementCreationInfo model. Creating agreements explains the trade-offs.
Signature and form fields
Where does the signature go? There are three options, from least to most work:
-
Let Adobe add a signature block
If a document has no fields, Adobe adds signature fields for each signer itself, typically on an extra page at the end. Fine for simple acknowledgements.
-
Text tags in the document
Put Adobe's text tags in the document text, for example
{{Sig_es_:signer1:signature}}, and Adobe turns them into fields during processing. This works for generated documents: add the tags in your template. Adobe's text-tag guide lists the syntax for signatures, initials, dates and data fields. -
Place fields by hand
Create the agreement in the
AUTHORINGstate, open Adobe's authoring page (see states), drag fields onto the page, then send. Or place them once on a library document.
Where next
Common questions
I passed the PDF contents to upload() and got "does not exist or is not readable". Why?
A string argument is always a path. Wrap the bytes in a stream (php://temp or a PSR-17
createStream()) and pass a file name, as shown in From a string you generated.
Can I upload several files in one call?
No. Adobe's upload endpoint takes exactly one file per request. Call upload() once per file and add each id with
its own file() call; the agreement shows them in that order.
Can I reuse a transient document id for several agreements?
Yes, within seven days of the upload. For anything you send repeatedly over a longer period, use a library document.
Can I send Word documents, or only PDFs?
Adobe converts Word, images, text and several other formats to PDF before signing. It works, but the converted layout can differ slightly from what you see in Word, and a failed conversion cancels the agreement. If you produce documents programmatically, render them to PDF yourself.
Can FileInfo::url() point at a file behind our login?
No. Adobe downloads the file from its own servers without your cookies or headers, so it needs a public URL. Use a short-lived
pre-signed link from your storage provider, or upload the file with upload() instead.
How do I delete a transient document I uploaded by mistake?
You can't, and you don't need to: unused transient documents expire after seven days and are never visible to anyone. Just don't create an agreement from it.