Adobe Sign PHP SDKconceptlz/adobe-sign GitHub

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.

The two-call pattern
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.

From Flysystem, S3 or any library that hands out resources
// 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');
From a PSR-7 stream, e.g. a document service's HTTP response
$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".

A PDF rendered with dompdf
$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 $mimeType whenever you upload from a stream or the name might lack an extension.
FileMime typeNotes
PDFapplication/pdfThe safest choice. Layout is exactly what you sent.
Word (.docx)application/vnd.openxmlformats-officedocument.wordprocessingml.documentAdobe converts it to PDF. Fonts and pagination may differ slightly from Word's.
Word (.doc)application/mswordConverted to PDF.
Images (.png, .jpg)image/png, image/jpegConverted to a one-page PDF; handy for scanned forms.
Plain text, HTMLtext/plain, text/htmlConverted 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?

TransientLibraryURL
Extra callOne upload per fileNone (created once)None
Lifetime of the reference7 daysUntil deletedFetched at creation
Form fieldsText tags in the document, or authoringPlaced once, reusedText tags in the document
Your server must be publicNoNoThe URL must be
Typical useGenerated or per-deal documentsStandard forms and policiesFiles 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.

Turning an upload into a reusable template
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.

A contract, a schedule and a template in one agreement
$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:

  1. 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.

  2. 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.

  3. Place fields by hand

    Create the agreement in the AUTHORING state, 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.