Skip to content
Reference

Electronic Signature contracts

Look up the App contribution, document, participant, request, result, and error contracts for NEXIA Electronic Signature.

Electronic Signature Contracts

Choose the flow before constructing a request: publish a template binding and data source for an App-owned document, or supply an uploaded PDF through the request contract. Resolve SignatureDocumentHost or SignatureHost from the container. Treat returned status and authorized links as the result; creating a document does not mean signatures are complete.

Read an existing request

Use the restored actor and Legal Entity plus your App-owned subject reference. $requestPublicId identifies the request; it does not grant access.

Code example
PHP
use Nexia\Signature\Contracts\SignatureHost;
use Nexia\Signature\SignatureRequestQueryInput;

$query = new SignatureRequestQueryInput(
    legalEntity: $legalEntity,
    actor: $actor,
    subject: $subject,
    requestPublicId: $requestPublicId,
);
$summary = app(SignatureHost::class)->summary($query);

The host returns a SignatureRequestSummary or null. Handle absence without exposing another subject's request. To create a document or submit a request, use the contribution and submission contracts below.

Signature

Apps import only these public SDK interfaces; Core binds their host implementations.

Code example
PHP
interface SignatureDocumentHost
{
    public function createCurrentBound(CurrentBoundSignableDocumentSubmission $submission): CurrentBoundSignableDocumentResult;
    public function replace(SignableDocumentReference $predecessor, CurrentBoundSignableDocumentSubmission $submission): CurrentBoundSignableDocumentResult;
    public function rebuildPreReady(PreReadySignableDocumentReference $predecessor, CurrentBoundSignableDocumentSubmission $submission): CurrentBoundSignableDocumentResult;
    public function retryPreReady(PreReadySignableDocumentReference $document, Actor $actor, string $reasonCode): CurrentBoundSignableDocumentResult;
    public function summary(string $publicId, Actor $actor): ?SignableDocumentSummary;
    public function previewHrefIfAuthorized(string $publicId, Actor $actor): ?string;
    public function sourceDownloadHrefIfAuthorized(string $publicId, Actor $actor): ?string;
    public function cancel(string $publicId, Actor $actor): SignableDocumentSummary;
}

interface SignatureHost
{
    public function submit(SignatureRequestSubmission $submission): SignatureRequestResult;
    public function summary(SignatureRequestQueryInput $query): ?SignatureRequestSummary;
    public function requestDetailHrefIfAuthorized(SignatureRequestQueryInput $query): ?string;
    public function completedDocumentDetailHrefIfAuthorized(SignatureRequestQueryInput $query): ?string;
    public function cancel(SignatureRequestCancelInput $input): SignatureRequestActionResult;
    public function resend(SignatureRequestResendInput $input): SignatureRequestActionResult;
    public function reissue(SignatureRequestReissueInput $input): SignatureRequestResult;
}

Source paths: packages/app-sdk/packages/laravel/src/Signature/Contracts/SignatureDocumentHost.php and packages/app-sdk/packages/laravel/src/Signature/Contracts/SignatureHost.php.

rebuildPreReady() cancels a queued/rendering revision and creates one successor from the current template winner. retryPreReady() instead requeues that exact retryable or attempts-exhausted revision after the owning workflow reauthorizes it. completedDocumentDetailHrefIfAuthorized() returns a protected UI route only for a complete request whose finalized artifact the current actor may read.

Additional source-neutral interfaces cover source plans, credentials, template selection, participant policy, and request-local preparation:

Code example
PHP
interface SignatureDocumentPlanHost
{
    public function submit(SignatureDocumentPlanSubmission $submission): CurrentBoundSignableDocumentResult|PreparedSignableDocumentResult;
}

interface SignatureRequestCredentialHost
{
    public function deriveRequestPasswordVerifier(string $password): SignatureRequestPasswordVerifier;
}

interface SignatureTemplateCatalogHost
{
    public function eligible(SignatureTemplateCatalogQuery $query): SignatureTemplateCatalogResult;
}

interface SignatureTemplateParticipantAssignmentHost
{
    public function resolve(SignatureTemplateParticipantAssignmentQuery $query): SignatureTemplateParticipantAssignmentResult;
}

interface SignatureRequestPreparationHost
{
    public function begin(SignatureRequestPreparationSubmission $submission): SignatureRequestPreparationReference;
    public function submit(SignatureRequestPreparationReference $preparation, SignatureRequestSubmission $submission): SignatureRequestResult;
}

PreparedSignableDocumentHost is the lower-level uploaded-PDF bridge used by the plan host. Prefer SignatureDocumentPlanHost when App code intentionally supports both published-template and uploaded-PDF sources. SignatureRequestPreparationHost starts a request-local editable draft from an App-authorized canonical snapshot, then accepts the exact ready document with the ordinary request idempotency contract. Business values remain App-owned; accept an edited business value through the owning App command and begin a new preparation. Catalog and participant-assignment queries always carry the actor, Legal Entity, exact subject, binding version, locale, and effective date.

Template-binding publication

An App publishes each reusable Signature template family as a SignatureTemplateBindingDescriptor in the immutable set returned by AppDescriptorContribution::appDescriptors(). The contributor class must live under a namespace and directory declared by the App Manifest's contributionLocations(). Core discovers the interface from that location and filters the set by descriptor class; there is no SignatureTemplateBindingContribution interface and no manual container or service-provider registration.

The descriptor's resourceKey links the family to an existing canonical Resource. It does not publish that Resource. An App-owned subject also needs its normal Resource contribution, create permission, and ResourceReferenceResolutionContribution, because Core rechecks both the permission and exact ResourceRef when it creates or reads a document.

Source paths: packages/app-sdk/packages/laravel/src/AppDescriptors/AppDescriptorContribution.php, packages/app-sdk/packages/laravel/src/AppDescriptors/AppDescriptorSet.php, and packages/app-sdk/packages/laravel/src/AppDescriptors/SignatureTemplateBindingDescriptor.php.

Data-source publication

An App that permits template authors to pull protected values publishes one paired contribution through the same App discovery location:

Code example
PHP
interface SignatureDocumentDataSourceContribution extends AppDescriptorContribution
{
    public function signatureDocumentDataSourceProviders(): array;
}

interface SignatureDocumentDataSourceProvider
{
    public function appKey(): string;
    public function sourceKey(): string;
    public function sourceVersion(): int;
    public function resolve(SignatureDocumentDataQuery $query): SignatureDocumentDataResult;
}

appDescriptors() returns the static SignatureDocumentDataSourceDescriptor; signatureDocumentDataSourceProviders() returns its runtime provider. Core joins the pair only when App, source key, version, contribution class, and App owner all match.

For a newly authored model-backed source, set the optional descriptor resourceKey. The registry then requires a labeled ResourceDescriptor with that exact key and the same discovered App owner, and the catalog serializes the Resource label for the authoring UI. This is separate from supportedSubjectResourceKeys, which limits the document subjects compatible with the source. The descriptor and provider do not make a missing Resource or subject Resource exist.

packages/people/src/Descriptors/PeopleSignatureDocumentDataSources.php is the complete in-repository example. PeopleCoreAppManifest::contributionLocations() covers its Descriptors directory; the class returns four data-source descriptors and four matching providers for employment-contract, worker, employment, and worker-assignment Resources.

Bulk-binding publication

An App that supports one exact Group Bulk binding implements SignatureBulkBindingContribution. Its appDescriptors() publishes SignatureBulkBindingDescriptor values and signatureBulkBindingProviders() publishes the matching runtime providers. Core joins App key, binding key, binding version, and capability contract version exactly; a descriptor without its provider is unavailable. The provider must authorize its bounded preview and reauthorize() the final execution context rather than trusting preview output.

SignatureBulkBindingDescriptor fieldContract
appKey, bindingKey, bindingVersion, capabilityContractVersionExact provider identity; the binding key is owned by the App
supportedSubjectResourceKeysOne to twenty unique canonical Resource keys
roleAssignmentPoliciesOne to eight uniquely keyed role policies and their allowed assignment sources
supportedInvitationChannels, supportedAuthenticationMethodsNon-empty capability ceilings enforced by Core
groupSelectorResourceKey, groupSelectorLabelKey, groupSelectorRequiredOptional paired selector metadata; a required selector must name one supported subject Resource
statusDescriptor lifecycle; inactive contributions are not available

Preview receives Core-restored tenant, Legal Entity, actor, template, group selection, and exact participant slots. The returned execution plan keeps its document variables, classifications, and signatory-role values out of ordinary serialization; only protectedPayload() may hand them to encrypted Core persistence. Final execution uses the provider's fresh reauthorization result, not the earlier preview snapshot.

Minimal example

The People App declares a discoverable template family in packages/people/src/Contribution/PeopleSignatureDescriptors.php:

Code example
PHP
namespace Amuzcorp\Nexia\PeopleCore\Contribution;

use Nexia\AppDescriptors\Contracts\AppDescriptorContribution;
use Nexia\AppDescriptors\AppDescriptorSet;
use Nexia\AppDescriptors\DescriptorStatus;
use Nexia\AppDescriptors\SignatureTemplateBindingDescriptor;
use Nexia\AppDescriptors\SignatureTemplateVariableDescriptor;
use Nexia\AppDescriptors\SignatureSignatoryRoleDescriptor;
use Nexia\Signature\SignatureVariableType;
use Nexia\Signature\SignatureDataClassification;

final class PeopleSignatureDescriptors implements AppDescriptorContribution
{
    public static function appDescriptors(): AppDescriptorSet
    {
        return AppDescriptorSet::of(new SignatureTemplateBindingDescriptor(
            appKey: 'people',
            bindingKey: 'people.employment_contract',
            resourceKey: 'people.employment_contract',
            labelKey: 'people.signature_bindings.employment_contract.label',
            descriptionKey: 'people.signature_bindings.employment_contract.description',
            createPermissionKey: 'people.employment_contract.create_document',
            variables: [
                new SignatureTemplateVariableDescriptor('worker.full_name', 'people.signature_bindings.employment_contract.variables.worker_full_name', SignatureVariableType::String, true, SignatureDataClassification::Restricted),
                new SignatureTemplateVariableDescriptor('contract.starts_on', 'people.signature_bindings.employment_contract.variables.contract_starts_on', SignatureVariableType::Date, true, SignatureDataClassification::Internal),
            ],
            signatoryRoles: [
                new SignatureSignatoryRoleDescriptor('worker', 'people.signature_bindings.employment_contract.roles.worker', 1, 1),
            ],
            syntheticSample: [
                'worker.full_name' => 'Alex Kim',
                'contract.starts_on' => '2026-09-01',
            ],
            templateKeys: ['people.employment_contract.new_hire'],
            version: '1.0',
            status: DescriptorStatus::Active,
        ));
    }
}

This focused form is constructor-valid and shows the minimum relationship. The real People binding adds the optional employer representative and the remaining employment-contract variables. The class is discovered only because PeopleCoreAppManifest::contributionLocations() covers its namespace and directory. No service provider registers it.

This descriptor registers a Signature template family, not the business Resource itself. resourceKey must name an existing canonical Resource, and Core must be able to resolve that exact subject through ResourceReferenceResolutionContribution before document creation is authorized. For People, those separate contributions are Contribution/Resources/EmploymentContractModule.php and Contribution/ReferenceResolution/PeopleEmploymentContractReferenceResolver.php.

After contribution discovery and tenant template publication, create a document from current App facts:

Handler excerpt: $legalEntity, $actor, and $subject are restored/authorized context; $workerPartyPublicId, $contractPublicId, and $correlationId come from the App record and operation. $workerPartyPublicId is the worker’s person Party public ID, not the Worker Resource ID.

Code example
PHP
use Nexia\Signature\Contracts\SignatureDocumentHost;
use Nexia\Signature\CurrentBoundSignableDocumentSubmission;

$documents = app(SignatureDocumentHost::class);
$result = $documents->createCurrentBound(new CurrentBoundSignableDocumentSubmission(
    legalEntity: $legalEntity,
    actor: $actor,
    subject: $subject,
    bindingKey: 'people.employment_contract',
    bindingVersion: '1.0',
    templateKey: 'people.employment_contract.new_hire',
    locale: 'ko',
    effectiveOn: '2026-09-01',
    variables: [
        'worker.full_name' => 'Alex Kim',
        'contract.starts_on' => '2026-09-01',
    ],
    signatoryRoles: [
        'worker' => [['party_public_id' => $workerPartyPublicId]],
    ],
    idempotencyKey: 'people.employment-contract.document:'.$contractPublicId,
    correlationId: $correlationId,
));

CurrentBoundSignableDocumentResult is an acceptance result, not a ready reference. Consume signature.document.outcome.v1, call summary(), and build SignableDocumentReference only when status is ready and the stored subject, revision, and checksum agree.

The published template also freezes its parallel or sequential routing mode and signer order into that ready document. The App does not add those fields to SignatureRequestSubmission; Core derives them from expectedDocument and rejects request-level overrides.

Generate a fail-closed starting pair with a dry run first:

Code example
Shell
task artisan -- nexia-apps:make-package-signature-data-source assets AssignedAssetLabelsV2 \
  --subject-resource-key=people.employment_contract \
  --source-resource-key=assets.asset_assignment --cardinality=many \
  --min-items=0 --max-items=50 --field-key=asset_display_name \
  --field-type=string --field-classification=internal --field-formatter=plain

Add --write only after reviewing the two target paths. The command creates a descriptor/contribution and a provider that returns unavailable until every authorization and provenance TODO is implemented; it never reflects a model, fillable list, cast, column, or relation. Existing targets are refused unless the explicit --write --force combination is used.

Parameters

SignatureTemplateBindingDescriptor

ParameterTypeRequiredDefaultBehavior
appKeystringyes—Owning App key
bindingKeystringyes—App-owned family key; also exposed as key
resourceKeystringyes—Canonical Resource kind the family binds
labelKey, descriptionKeystringyes—Localized catalog text keys
createPermissionKeystringyes—App-owned authority Core rechecks for document creation
variableslist of SignatureTemplateVariableDescriptoryes—Unique typed variables and their data classification
signatoryRolesnon-empty list of SignatureSignatoryRoleDescriptoryes—Unique lower-snake-case roles, min/max cardinality, and optional assignment policy
syntheticSamplekeyed arrayyes—Non-production preview values; every required variable must appear
templateKeysnon-empty list of stringyes—Unique App-owned template families allowed by the binding
versionstringno1.0Exact contribution version frozen into documents
statusDescriptorStatusnoActiveContribution lifecycle
requestNavigationIdstring or nullnonullOptional Shell navigation identity for the App-owned request entry point
requestNavigationRoutelocal absolute route or nullnonullOptional local route; requires requestNavigationId and rejects //, whitespace, and control characters

Variable types are string, integer, decimal, boolean, date, datetime, and money. Classification is public, internal, confidential, or restricted; only public may appear in logs. Money values have numeric amount and a three-letter uppercase currency.

Each role's optional SignatureParticipantAssignmentPolicy declares one or more permitted authorities: binding_resolved, request_supplied, or template_fixed. It limits how a participant may be supplied; it does not itself resolve or authorize that participant.

SignatureDocumentDataSourceDescriptor

ParameterRequired behavior
appKey, sourceKey, sourceVersionExact App-owned keys and positive integer source version; the provider must return the same tuple
resourceKeyOptional canonical Resource key for the same App. When supplied, it identifies the labeled ResourceDescriptor used in authoring.
supportedSubjectResourceKeys, lookupModeExplicit subjects and one of direct_subject, derived_ref, explicit_source_ref, owner_scoped_query
defaultSourceRefAnchorRequired canonical owner-projected anchor for derived_ref and explicit_source_ref; forbidden for the other modes. Core uses it only to initialize authoring and never guesses a record
cardinality, stableSortKeyone or bounded many; many requires a declared required deterministic scalar sort field
providedDataContractsOptional versioned semantic contracts whose declared field schemas must agree across providers
fieldsExplicit typed, required/optional, classified fields and compatible formatter allowlists
syntheticSample, label keysComplete authoring-only sample and App-local locale keys; never live values

SignatureDocumentDataQuery

Core restores tenant, Legal Entity, actor, purpose, exact subject ResourceRef, derived anchors, optional explicit source ref, asOf, requested fields, and selected refs. The provider must independently reauthorize every protected field and record, prove the subject relationship, apply the App-owned as-of/state rule, bound the query, and re-resolve selected refs exactly. A ResourceRef or party_id identifies a candidate; neither grants access.

CurrentBoundSignableDocumentSubmission

ParameterTypeRequiredDefaultBehavior
legalEntity, actorSDK contractsyes—Restored current execution context
subjectResourceRefyes—App-owned business record Core authorizes
bindingKey, bindingVersionstringyes—Exact installed descriptor identity
templateKeystringyes—Current published template family to resolve
localestringyes—BCP-47-like locale such as ko or en-US
effectiveOnY-m-d stringyes—Selects the effective published template revision
variableskeyed arrayyes—Current App values checked against the descriptor
signatoryRoleskeyed listsyes—Scalar-only business identity snapshots by role
idempotencyKeystringyes—Same key and payload return the existing command result
correlationIdUUID stringyes—Trace identity
causationIdUUID or nullnonullTriggering event identity
trustedAssetslistno[]Explicit host-governed assets such as an official seal placement

SignatureRequestSubmission

ParameterTypeRequiredDefaultBehavior
legalEntity, actor, subjectSDK contractsyes—Must match current tenant, scope, and App resource authority
expectedDocumentSignableDocumentReferenceyes—Frozen public id, positive revision, and lowercase 64-character checksum
participantsnon-empty listyes—Immutable SignatureParticipantSnapshot entries whose unique positive sequences match the ready document's frozen signer order
consentPolicySignatureConsentPolicyReferenceyes—Exact host policy key/version; baseline is standard-consent@1.0
expiryPolicySignatureExpiryPolicyyes—Explicit-zone ISO-8601 deadline; current action is expire
idempotencyKeystringyes—Maximum 191 characters
correlationIdUUID stringyes—Trace identity
causationIdUUID or nullnonullTriggering event identity

A participant declares roleKey, positive sequence, unique assignedFieldKeys, displayName, locale, invitation channel and address, authentication profile, required methods, and optional partyPublicId for an internal person Party. That value is an identity assertion, not authority: Core re-resolves an active person Party in the current tenant and requires its current contact email to match the invitation address before it creates a personal-inbox binding. Leave it absent for external recipients; Core never infers a Party from an email address. Supply the email OTP address when email_otp is selected. Use SignatureRequestCredentialHost to derive the opaque requestPasswordVerifier; never hash a request password in the App. There is deliberately no request routingMode or signingOrder input.

partyPublicId is included in the transport snapshot but excluded from toLogSafeArray(). Entering through an authenticated Party binding does not satisfy or remove any frozen email_otp or request_password method.

Options

Document source

SourceContractUse when
Published templateSignatureDocumentHost::createCurrentBound()The App contributes a stable template family; preferred path
Composable planSignatureDocumentPlanHost::submit() with PublishedTemplateSignatureDocumentSourceOne action deliberately selects among source kinds
Uploaded PDFPlan host with UploadedPdfSignatureDocumentSourceA one-off PDF and explicit normalized field rectangles are required

An uploaded PDF uses a private uploadIntentId, participant snapshots, and SignatureFieldDefinition entries. Rectangles use a one-based page and normalized x, y, width, and height within 0..1. Available field kinds are text, checkbox, date, select, signer_name, signature, and signed_at. Optional datePart is presentation-only: date accepts year, month, or day, while signed_at also accepts date and time; other field kinds reject it.

Authentication and delivery

ProfileRequired methodsCurrent availability
standard_password@1.0request_passwordOperational when local password derivation is ready
standard_email_otp@1.0email_otpOperational when mail and queues are ready
standard_password_email_otp@1.0request_password and email_otpOperational when both dependencies are ready
standard_sms_otp@1.0sms_otpUnavailable

Email is the supported invitation channel. Do not accept sms or sms_otp merely because they exist in the enums; Core rejects unavailable capabilities.

Data-source versioning

A breaking field, meaning, cardinality, lookup, or provenance change gets a new positive source version. Keep the exact old descriptor/provider operational while a published template revision still names it, or publish a successor template and migrate new preparations explicitly. Validate the assembled contract and all App-local locale keys with php artisan nexia-apps:validate-app-descriptors <app-key>.

Routing and progress

ContractValues or fieldsMeaning
SignatureRoutingModeparallel, sequentialImmutable policy copied from the published template through the document into the request
SignatureRequestResult::routingModeSignatureRoutingModeAccepted request's frozen mode; legacy serialized results default to parallel
SignatureRequestSummary::routingModeSignatureRoutingModeAuthorized, log-safe request mode
SignatureParticipantProgressroleKey, roleSlot, sequence, status, routingState, presentedRevision, completedAtLog-safe participant identity, execution order, routing eligibility, and request-local PDF revision ordinal
SignatureParticipantRoutingStatewaiting, revision_pending, active, completed, closed, failedWhether a participant is waiting for a predecessor, waiting for a verified cumulative PDF, eligible now, or terminal

In parallel, every participant is active against request-local revision 0. In sequential, participant N becomes active only after verified revision N - 1 exists. presentedRevision reports the exact ordinal that participant saw; it does not grant artifact access or expose PDF content or a checksum.

Output or return

CallReturnMeaning
createCurrentBound, replace, rebuildPreReadyCurrentBoundSignableDocumentResultPublic id, accepted|existing, document status, frozen binding/template versions, revision
SignatureDocumentHost::summary?SignableDocumentSummaryAuthorized non-content state, resolved fields, page count, checksum/failure, attempts, trusted assets
SignatureHost::submit, reissueSignatureRequestResultPublic id, accepted|existing, lifecycle status, subject, exact document, optional predecessor, frozen routing mode
SignatureHost::summary?SignatureRequestSummaryFrozen routing mode, log-safe participant routing/progress and presented revision, expiry, failure code, and completed artifact reference
cancel, resendSignatureRequestActionResultAction, accepted|existing, and resulting request status
authorized href methods?stringProtected Core route, or null when current authority does not hold

Document statuses are queued, rendering, ready, retryable_failed, terminal_failed, cancelled, and superseded. Request terminal statuses are completed, declined, expired, cancelled, and failed; preceding states are draft, preparing, ready, dispatching, in_progress, and finalizing.

SignatureRequestSummary::toLogSafeArray() excludes participant names, invitation and OTP addresses, credential material, and document content. Do not replace it with SignatureParticipantSnapshot::toArray() in diagnostics.

Errors

Host failures use SignatureException; branch on errorCode, not message text.

SignatureErrorCodeCauseResolution
unauthorized, request_scope_mismatchActor, Legal Entity, subject, or App authority is staleRestore current context and reauthorize the business record
binding_unavailable, template_unavailableDescriptor/version is absent or no eligible published template existsVerify contribution discovery, exact version, locale, and effective date
document_not_ready, document_reference_mismatchRequest used an unfinished or different revision/checksumConsume the document outcome and rebuild the reference from an authorized summary
payload_drift, request_conflictAn idempotency identity was reused with different frozen inputKeep one canonical payload per key; create an explicit successor for change
participant_invalid, participant_assignment_invalidRole cardinality, sequence, or field ownership does not matchRebuild participants from the binding and ready document assignments
authentication_profile_unavailable, authentication_methods_invalidProfile version and required methods differSubmit the exact profile method set
unsupported_capabilitySMS or a runtime dependency is unavailableSelect an operational capability; do not bypass the host gate
consent_policy_unavailable, expiry_policy_invalidPolicy version is missing or deadline/action is invalidUse a published policy and explicit-zone future timestamp
request_terminalMutation targeted a terminal requestStart reissue() when a valid successor is needed
execution_unavailable, rate_limitedBounded runtime or delivery failurePreserve the handoff and retry through an authorized idempotent path
artifact_verification_failedCompleted PDF or manifest identity failed verificationStop business application and escalate as an integrity incident

DTO constructors throw InvalidArgumentException before a host call for shape errors such as non-UUID correlation ids, list/keyed-array confusion, duplicate sequences, invalid locale/date/timestamp formats, or out-of-page rectangles.

Real usage

packages/people/src/Actions/SubmitEmploymentContractDocumentHandoff.php restores a delegated actor, rechecks people.employment_contract.create_document, and calls createCurrentBound() only after its Inbox reservation commits. It compares the returned binding, version, and template before accepting the handoff.

packages/people/src/Actions/SubmitEmploymentContractSignatureHandoff.php then rebuilds SignatureRequestSubmission from the durable App handoff. It checks the current SignableDocumentReference, calls submit() or reissue(), and rejects a result whose subject, document, checksum, or predecessor differs. It submits the worker as sequence 1 and employer representative as sequence 2; it does not choose a routing mode. The published template remains the routing authority.

Finally, packages/people/src/Actions/ApplyEmploymentContractSignatureOutcome.php consumes signature.request.accepted.v1 and signature.request.outcome.v1, calls both authorized summaries before the App Inbox transaction, and applies one projection only after current actor, permission, subject, expiry, document, and artifact facts agree.

For a data-source implementation, compare packages/people/src/Descriptors/PeopleSignatureDocumentDataSources.php with packages/people/src/Signature/WorkerAssignmentsSignatureDocumentDataSourceProvider.php. The descriptor declares the exact employment-contract subject, derived Worker anchor, bounded many-cardinality fields, and stable sort key. The provider then rechecks the tenant, Legal Entity, actor, subject and selected references, applies the as-of window, bounds at 50, and sorts by effective date plus public ID.

Verify this integration with:

Code example
Shell
php artisan test packages/people/tests/Feature/EmploymentContractSignatureBridgeTest.php
Source of truth: docs/developers/content/en/app-sdk/electronic-signature-contracts.md