Adobe Sign PHP SDKconceptlz/adobe-sign GitHub

Reference

Models & raw data

Data comes back from the SDK as typed objects: 313 models generated from Adobe's own API specification, plus a handful of hand-written ones for the results you use most. This page explains both, how to reach the raw JSON when you need it, what you can pass into methods, and how the generated code is kept in step with Adobe's spec.

Three kinds of objects

  • Generated modelsConceptlz\AdobeSign\Generated\Model\*. One class per schema in Adobe's v6 spec, with getters and setters. Most methods return these.
  • Result modelsConceptlz\AdobeSign\Model\*. Five hand-written, read-only classes for the most common results, with plain properties and helper methods.
  • BuildersConceptlz\AdobeSign\Builder\*. Fluent objects that produce request payloads and validate them before sending. Covered in Creating agreements.
You callYou get
agreements()->get($id)Model\Agreement
agreements()->all()a generator of Model\AgreementSummary
agreements()->sendReminder($id)Model\ReminderCreationResult
agreements()->signatureEvidence($id)Model\SignatureEvidence holding Model\SignerEvidence objects
agreements()->members($id)a raw array (the MembersInfo JSON)
agreements()->events($id), signingUrls($id), documents($id), users()->me(), webhooks()->get($id), …a generated model
create() on any resourcethe new resource's id, a string
combinedDocument(), auditTrail(), formData(), …a PSR-7 StreamInterface

Generated models

src/Generated/Model holds 313 classes, one for each schema in Adobe's published OpenAPI document for REST API v6 (shipped in the repository as spec/adobe-sign-v6.json). They are produced by the Jane code generator and are never edited by hand: a fix to a model is a fix to the spec normaliser followed by a regeneration, and CI fails if the generated code and the spec drift apart.

Every generated model follows the same pattern:

  • One protected property per JSON key, with the same name.
  • A getter and a fluent setter for each: getStatus() / setStatus().
  • Everything is nullable, because Adobe may leave any field out.
  • isInitialized('status') tells you whether a field was set, as opposed to set to null.
  • Status and enum values are plain strings. Adobe adds values without changing the API version, so a PHP enum would break the day a new one appears.
  • No runtime dependencies: plain PHP classes you can construct with new.

Reading them

$me = $sign->users()->me();                          // Generated\Model\DetailedUserInfo
echo $me->getEmail(), ' in account ', $me->getAccountId();

$events = $sign->agreements()->events($agreementId);  // Generated\Model\AgreementEventList
foreach ($events->getEvents() ?? [] as $event) {       // list of Generated\Model\AgreementEvent
    printf("%s %s %s\n", $event->getDate(), $event->getType(), $event->getParticipantEmail());
}

Lists are typed too: getEvents() returns AgreementEvent objects, not arrays. Remember the ?? []: a getter returns null when Adobe left the field out.

Building and changing them

Use generated models when you need a field the builders don't expose, or when updating a resource: fetch it, change it, send it back.

// Update: Adobe expects the full object, so start from what get() returned
$details = $sign->agreements()->get($agreementId)->details();   // FetchedAgreementInfo
$details->setMessage('Please sign by Friday.');
$sign->agreements()->update($agreementId, $details);            // If-Match added automatically

// Create: start from the builder, then set what it doesn't cover
$model = AgreementInfo::create('NDA')->file($documentId)->signers('jane@example.com')->toModel();
$model->setFirstReminderDelay(24);
$sign->agreements()->create($model);

Two classes called AgreementInfo, two called WebhookInfo

Builder\AgreementInfo and Builder\WebhookInfo are the fluent builders. Generated\Model\AgreementInfo (what agreements()->update() accepts besides FetchedAgreementInfo) and Generated\Model\WebhookInfo (what webhooks()->get() returns) are the spec's models. Alias one of each pair in your use statements if a file needs both, as the SDK itself does.

ModelMapper

Conceptlz\AdobeSign\Hydration\ModelMapper converts between decoded JSON arrays and generated models. The SDK uses it for every typed response, and it is public so you can do the same with data you already hold, such as a response you stored, a webhook payload or a fixture.

ModelMapper::hydrate(string $class, array $data): object

Builds a generated model from an array, recursing into nested models and lists of models.

ModelMapper::toArray(object $model): array

Serialises a model back to an API array. Only fields that were set are included, and null values are dropped.

use Conceptlz\AdobeSign\Generated\Model\FetchedAgreementInfo;
use Conceptlz\AdobeSign\Hydration\ModelMapper;

$stored = json_decode($row->payload, true);
$info = ModelMapper::hydrate(FetchedAgreementInfo::class, $stored);
echo $info->getStatus();

$array = ModelMapper::toArray($info);   // back to the shape Adobe sends and expects

It is deliberately forgiving, because Adobe's real responses don't always match the spec:

  • Unknown keys are ignored. A field Adobe adds tomorrow doesn't break hydration.
  • Mistyped values are skipped, not fatal: an object where a string was expected leaves that property unset.
  • Loose scalars are coerced: "42" becomes 42 for an integer field, "true" or "0" become booleans.
  • Only generated models are accepted. Any class outside ModelMapper::MODEL_NAMESPACE throws InvalidArgumentException.

The class metadata is read by reflection once per class and cached for the life of the process, so hydrating thousands of list items stays cheap.

Hand-written result models

Five classes in Conceptlz\AdobeSign\Model are written by hand. They are final, their properties are public readonly, and they exist where a typed property is much nicer to use than a getter on a large generated model, or where the SDK combines several responses.

Agreement

Returned by agreements()->get().

MemberTypeNotes
$id, $namestring"" if missing
$statusstringOUT_FOR_SIGNATURE, SIGNED, CANCELLED, …; compare as a string
$signatureType?stringESIGN or WRITTEN
$senderEmail?string
$createdDate, $expirationTime?stringISO 8601, exactly as Adobe sent it
$etag?stringThe response's ETag; also replayed automatically as If-Match
$rawarrayThe decoded response body, unmodified
details()FetchedAgreementInfoThe full response as the generated model
isSigned(), isOutForSignature()boolStatus shortcuts

AgreementSummary

Yielded by agreements()->all(), one per row of GET /agreements. Properties: $id, $name, $status (relative to the calling user, e.g. WAITING_FOR_MY_SIGNATURE), $type, $groupId, $displayDate, $hidden and $raw. details() returns the generated AgreementAgreement model for the row.

ReminderCreationResult

Returned by sendReminder() on agreements and bulk sends: $id (the reminder's id, nullable) and $disabledRecipients, the participants who won't receive it, for example because they turned reminder emails off.

SignatureEvidence and SignerEvidence

Built by agreements()->signatureEvidence(), which combines the agreement's members and events into a per-signer record of what was actually reached. SignatureEvidence has $agreementId, $status and $signers, plus allSigned(), meets($level, $qualifiedIssuers), shortfalls($level, $qualifiedIssuers) and lowestLevel($qualifiedIssuers).

SignerEvidence memberTypeMeaning
$email, $namestring, ?stringWho signed
$orderintStep in the signing order
$signed, $signedAtbool, ?stringWhether and when (ISO 8601)
$signatureInputType?stringAs Adobe reports it: TYPED, SCRIBBLE (drawn), …
$digitalSignatureRequiredboolAES or QES was requested for this signer
$digitallySignedboolA certificate-based signature was actually applied
$certificateIssuer, $signingProvider?stringThe certificate's issuer and the cloud signature provider, for digital signatures
$verificationstringIdentity check before signing: NONE, EMAIL_OTP, PHONE, KBA, GOV_ID, DIG_ID, …
achievedLevel($qualifiedIssuers)?SignatureLevelSES, AES, or QES when the issuer is in your list; null if not signed
issuedByAnyOf($issuers)boolCase-insensitive substring match against the issuer

How these are used to prove a signature level is covered in Signature levels.

The $raw escape hatch

Agreement and AgreementSummary keep the complete decoded JSON in $raw, untouched. Use it for a field the typed properties don't cover, or one Adobe added after this release, without waiting for an SDK update:

$agreement = $sign->agreements()->get($agreementId);

$agreement->status;                                // typed, the common case
$agreement->raw['locale'] ?? null;                 // anything else in the response
$agreement->raw['participantSetsInfo'][0]['memberInfos'][0]['email'] ?? null;

$agreement->details()->getVaultingInfo();          // or the full generated model

The other three result models are derived values with no single response behind them, so they have no $raw. Webhook events have one too: $event->raw holds the notification exactly as Adobe posted it (see Webhooks).

Methods that return arrays

agreements()->members() returns the decoded MembersInfo array rather than a model. The live API's member data differs from the spec in places (it omits nextParticipantSets unless asked, for example), and an array lets you read what actually arrived. Hydrate it yourself if you prefer a model:

use Conceptlz\AdobeSign\Generated\Model\MembersInfo;

$members = ModelMapper::hydrate(MembersInfo::class, $sign->agreements()->members($agreementId, includeNextParticipantSet: true));

Passing data in: arrays, models or builders

Methods that send a payload accept a generated model or a plain array, and some also accept a builder. The rule is the same everywhere:

You passWhat is sent
An arrayExactly that array, JSON-encoded
A generated modelModelMapper::toArray($model): only the fields you set
A builder (AgreementInfo, WebhookInfo)Its toArray(), after local validation
Any other objectNothing: InvalidArgumentException before sending
MethodAccepts
agreements()->create()AgreementInfo|AgreementCreationInfo|array
agreements()->update()Generated\Model\AgreementInfo|FetchedAgreementInfo|array
webhooks()->create(), update()Builder\WebhookInfo|Generated\Model\WebhookInfo|array
libraryDocuments()->create()LibraryDocumentCreationInfoV6|array
widgets()->create()WidgetCreationInfoV6|array
megaSigns()->create()MegaSignInfo|array
users()->create(), update()DetailedUserInfo|array

Which to choose:

  • Builders for agreements and webhooks. They check signing orders, emails and signature levels before anything is sent, and only they apply ClientOptions::$defaultSignatureLevel.
  • Generated models for updates (start from what get() returned) and for fields the builders don't cover.
  • Arrays when you already have the JSON, for example from a template stored in your database. They are sent as written, with no checks and no default signature level.

The builders also have a with() method that merges raw spec fields into what they produce, which is often simpler than switching to a model:

AgreementInfo::create('NDA')
    ->file($documentId)
    ->signers('jane@example.com')
    ->with(['emailOption' => ['sendOptions' => ['initEmails' => 'NONE']]]);

Why there is no symfony/serializer

Jane normally generates normalizer classes alongside the models, and those depend on symfony/serializer and Jane's runtime package. The SDK deletes them after generation and uses its own small ModelMapper instead. The reasons:

  • The core depends only on PSR interfaces. A Serializer dependency would pin a Symfony major version on every project that installs the SDK, including projects that already use a different one. An architecture test fails the build if a non-PSR runtime dependency creeps in.
  • Leniency is a feature. The real API sends fields and values the spec doesn't list. ModelMapper skips what doesn't fit instead of throwing, which is what a client of a changing API needs.
  • It is small and pinned down by tests. GeneratedModelsContractTest checks that every generated model follows the accessor convention the mapper relies on, and that generated code has no runtime dependencies.

Regenerating the models

You never need to do this to use the SDK. It is for contributors updating to a newer copy of Adobe's spec, or fixing a spec problem. Everything runs from the root of the development monorepo:

  1. Install the generator

    Jane lives in an isolated tool directory, so it is never a dependency of any package.

    composer --working-dir=tools/jane install
  2. Update or fix the spec

    Replace packages/adobe-sign/spec/adobe-sign-v6.json with Adobe's newer version, or fix a quirk in packages/adobe-sign/spec/normalize.php. The same normaliser feeds the generator and the test suite's spec validator, so both read the spec identically.

  3. Generate

    This normalises the spec, runs Jane, and deletes the normalizer and runtime classes so only plain models remain.

    composer generate
  4. Run the suites

    Both test suites and PHPStan must pass. CI also fails if src/Generated is out of sync with the spec.

    composer test
    composer analyse

Never patch a generated file

A hand edit in src/Generated is lost on the next run and breaks the sync check. If a generated model is wrong, the fix belongs in spec/normalize.php; if the live API disagrees with the spec, the SDK accepts both shapes in hand-written code and the difference is recorded with a regression test.

Common questions

Why does a getter return null when the agreement clearly has that value?

Either the endpoint you called doesn't include that field, or the value had a type the spec doesn't allow and ModelMapper skipped it. Check $raw on the result model, or the response body in a logger middleware, to see what Adobe actually sent.

How do I store a model as JSON?

json_encode(ModelMapper::toArray($model)), and ModelMapper::hydrate() to read it back. For Agreement, store $agreement->raw and rebuild with Agreement::fromArray().

Why are statuses strings and not enums?

Adobe's spec warns that new values can appear without a new API version. A string keeps working when that happens; an enum would throw. Compare with string literals or your own constants.

How do I find which generated model a method uses?

Look at the method's return type in your IDE, or its docblock, whose first line names the endpoint. The model names match the schema names in Adobe's API reference, so FetchedAgreementInfo there is FetchedAgreementInfo here.

Can I extend a result model with my own methods?

The hand-written models are final. Wrap them in your own class, or work from $raw or details(). Keeping them final lets the SDK add properties in minor releases without breaking subclasses.