REFRACT eSign
REFRACT eSign provides native electronic signing for documents rendered by REFRACT PDF. Signers access a REFRACT-hosted signing portal to verify their identity, review the document, and apply an electronic signature.
REFRACT eSign manages the complete electronic signature lifecycle for REFRACT PDF-generated documents.
Document rendering and audit storage occur in your Azure subscription via the REFRACT Dispatcher and Processor. Signers interact with a signing portal hosted on REFRACT Shared Services by Refract Logic. Signer invitations are routed through Azure Communication Services.
The document itself is generated and stored in your infrastructure. Signers view and download it directly from your Dispatcher.
Enterprise Feature
REFRACT eSign requires the vaultESign feature gate on your licence.
Needs Azure Blob Storage
REFRACT eSign keeps its records, its tamper-evident audit chain and its documents in your Azure Blob Storage account, and cannot run without it. The installer deploys that account on every install, whatever your storage routes say, so routing your documents to SharePoint does not turn REFRACT eSign off. If a deployment is missing the account's settings, REFRACT eSign refuses every request with BLOB_STORAGE_NOT_CONFIGURED, naming the missing settings, and the Dispatcher's health check reports blobStorage as missing. See Storage Providers.
Documents stored in your Azure subscription
Documents are generated and stored in your Azure subscription. REFRACT eSign signers view and download documents directly from your deployment. Signature images and field values pass through the REFRACT-hosted portal on their way back to your subscription - the portal does not write them to its own storage. The portal keeps signer contact details, one-time-code hashes, and session state in its own database.
Notification Flow:
The eSign portal composes every eSign email: invitations (initial, sequential, and resend), automatic reminders and signing-link expiry warnings, OTP access codes, completion notifications, documents-ready notifications, and the requester's notices. Where each one is sent from follows EMAIL_PROVIDER on the Dispatcher that created the envelope, which is recorded on the envelope when it is created:
vault-shared(default): sent through REFRACT Shared Services' Azure Communication Services.customer-acs: the portal hands the finished email to your Dispatcher, which checks that the recipient is that envelope's signer or requester and queues it, and your Processor sends it from your own Azure Communication Services. It never falls back to the shared service.
In both modes every eSign email is recorded on the envelope's audit trail as queued or not sent. See REFRACT eSign emails for the full breakdown, including what happens when your Dispatcher cannot be reached.
Audit Flow: Your Processor writes the signing session's audit trail and evidence certificate to your Azure Blob Storage and Table Storage.
How It Works
-
A document is rendered by your REFRACT Dispatcher (direct on-demand render) or REFRACT Processor (workflow seal, batch distribution). The resulting PDF is stored in your Azure Blob Storage.
-
An initiator (SPFx command set or API caller) creates a signing session via
POST /api/esign/session(calls your customer-hosted Dispatcher). -
Your Dispatcher calls the eSign portal to create the per-signer portal sessions and links. The portal composes the invitation emails with those links, which are sent through Azure Communication Services: the shared service by default, or your own with
customer-acs(see Notification Flow above). -
Each signer visits their portal link (hosted on the eSign portal), verifies via OTP, reviews the document, completes any manifest fields (initials, date, text, checkboxes), and applies their signature.
-
The signer's completed submission is posted as a callback to your Dispatcher (
POST /api/esign/callback). The Dispatcher validates the HMAC signature, updates slot state, and enqueues a seal message to your Azure Service Bus. Your Processor picks up the seal message for sealing to a final PDF, with its SHA-256 hash recorded in the chained audit trail, and evidence certificate generation. -
The completed signed PDF and evidence certificate are stored in your Azure Blob Storage and (optionally) written back to SharePoint.
Infrastructure Locations
| Component | Hosted On | Purpose |
|---|---|---|
| REFRACT Dispatcher | Your Azure Subscription | API endpoint, session creation, callback receiver, token generation |
| REFRACT Processor | Your Azure Subscription | Signature sealing, evidence certificate generation, audit writing |
| eSign Portal | REFRACT Shared Services by Refract Logic | Signing UI, OTP verification, cross-device transfer. Keeps encrypted signer contact details, OTP hashes, and session state in its own storage. |
| Notification Service | REFRACT Shared Services by Refract Logic | eSign emails via Azure Communication Services, in vault-shared mode (the default). In customer-acs mode your own Processor and ACS send them instead. |
| Licensing API | REFRACT Shared Services by Refract Logic | Session validation, metering, and platform health |
Signing Modes
| Mode | Description |
|---|---|
single | One signer. Session completes when the single slot is signed. |
sequential | Multiple signers in a defined order. Each signer is notified only after the previous signer completes. |
parallel | Multiple signers notified simultaneously. Session completes when all slots are signed. |
Signer limit - up to 10 per session
REFRACT eSign supports up to 10 signers per session. For documents requiring more than 10 signatories, consider splitting into multiple sessions per document section or using REFRACT Workflow's governance approval routing.
Session Expiry
expiresInMinutes is the time each signer has to sign. A session expires when a signer's window ends before all slots are signed.
| Where the window is set | Unit | Allowed | Default when omitted |
|---|---|---|---|
POST /api/esign/session (direct API) | minutes | 60 to 10080 | 1440 (1 day) |
templateSettings.esign.expiresInMinutes (render auto-trigger, including the Power Platform connector's Generate Document) | minutes | 60 to 10080 | 10080 (7 days) |
| Send for Signature dialog in REFRACT Lifecycle | days | 1 to 7 | 1 day |
The value must be a whole number of minutes. A value outside the range is refused, not shortened to fit, with code ESIGN_WINDOW_INVALID and the message "expiresInMinutes must be a whole number of minutes from 60 (the minimum) to 10080 (7 days); received ...". On the direct API this is an HTTP 400. On a render it is a setup rejection: the render returns an error report and no session is created. On the render paths, a value that is not a JSON number (for example the string "240") is ignored and the 7-day default applies, so send it as a number. There is no expiresInDays setting for eSign.
Sequential signers each get their own window. In a sequential session with two or more signers, each signer's window starts when that signer is invited, not when the session is created. The whole session also has a ceiling: the window multiplied by the number of signers, at most 30 days. No signer's deadline goes past the ceiling, and a resent invitation does not extend a deadline. single and parallel sessions have one deadline shared by every signer.
What happens when a session expires:
- The session state transitions to
expired, and the requester is emailed when the request has a requester address. The expiry check runs every 15 minutes. - A signer who had not signed sees Signing request expired when opening their link: "This signing request expired on date and time." and "It can no longer be signed. Contact the sender if it still needs your signature." The document is no longer available to them.
- A signer who had already signed still sees their signed card, which says: "The request expired before everyone signed, so no sealed document will be produced." See After Signing or Declining.
- Signatures from the expired session are not carried forward to a new one.
There is no extend or revive endpoint. To recover from an expired session, create a new session via POST /api/esign/session. All signers - including those who had already signed - must sign again in the new session.
POST /api/esign/session/{correlationId}/resend-invite does not rescue expired sessions. It only resends the invitation email for slots already in activated, viewed, or otp_verified state within a live session. See Resend for what the response reports.
Reminders
REFRACT eSign reminds signers automatically. Reminders are on by default for every customer; you can tune the schedule or switch them off per request or per template.
Who is reminded and when. A signer is reminded once they can sign now: they have been invited and have not yet signed, declined or expired. Each signer's schedule is counted from when that signer was invited. In a sequential session a later signer is reminded only once they have been invited, counted from their own invitation. The first reminder goes out after firstAfterDays, then one every repeatEveryDays, up to maxReminders. In addition, one expiry warning is sent expiryWarningDays before that signer's link expires, but only when the link lives at least 3 days, so a session on the direct API's 1-day default window never gets one.
What the signer receives. The reminder email uses the invitation's layout and the signer's same link, worded as a reminder ("Reminder: please sign ...", "Your signature is still needed"). The expiry warning is worded "Your signing link expires soon": it says when the link stops working and that the sender will need to send a new request.
What stops them. Reminders stop for a signer when that signer signs or declines, or when the envelope is revoked, cancelled or expired.
Settings.
| Setting | Type | Default | Range | Description |
|---|---|---|---|---|
enabled | boolean | true | Set to false to turn reminders off. | |
firstAfterDays | number | 2 | 1 to 30 | Days after the signer is invited before the first reminder. |
repeatEveryDays | number | 2 | 1 to 30 | Days between reminders after the first. |
maxReminders | number | 3 | 1 to 10 | Most reminders sent to one signer. |
expiryWarningDays | number | 1 | 0 to 14 | Days before the signer's link expires to send the expiry warning. 0 turns the warning off. |
All values are whole numbers. Anything else is refused with HTTP 400 and a sentence naming the setting; on a render, it is refused before the render runs. The settings can be supplied in three layers, and the narrowest wins field by field: the built-in default, then the template's templateSettings.esign.reminders block, then the request's own reminders object. For Send for Signature (the Activity Center dialog) and for POST /api/esign/session, the dialog or the body's reminders object sits over the default. The dialog has a Reminders section with a checkbox ("Remind each signer automatically until they sign") and four number fields (first reminder, repeat every, most reminders, expiry warning), starting at the defaults.
Sent the same way as every other eSign email. The eSign portal sends the reminder through the envelope's own email provider: with EMAIL_PROVIDER=customer-acs it is sent from your own ACS (via your Dispatcher and Processor), otherwise through the shared service. See REFRACT eSign emails.
Recorded on the audit trail. Each reminder is claimed with a conditional write before it is sent, so it goes out exactly once; a failed send is recorded and is not retried at that time. The chain records a SIGN_REMINDER_SENT event with the signer, the kind (reminder or expiry-warning), the number, and the outcome (sent, kept for retry, or failed). It never records an address. The Lifecycle timeline shows it as a Signing Reminder sub-entry, for example "Reminder 2 to signer 1".
Timing rules that apply to every reminder
- A reminder is not sent within 2 hours of expiry.
- A reminder waits while the signer was emailed in the last 12 hours, a manual Resend included.
- After downtime only the latest due reminder is sent.
- There are no quiet hours: a reminder goes out at the first sweep after it is due. The sweep runs every 15 minutes.
- Reminders are email only, and do not count as a new session for licensing or metering.
- The resolved schedule is stored on the envelope when it is created. An envelope created before reminders were introduced has none and is never reminded.
- Setting
REFRACT_REMINDERS_DISABLED=trueon the Dispatcher Function App stops every reminder, for every product, across the deployment.
Resend
The requester's Resend button (POST /api/esign/session/{correlationId}/resend-invite) now waits for the email and reports the real outcome: the response carries sent, keptForRetry and failed counts. keptForRetry applies with customer-acs when your Dispatcher is unreachable and the email is queued for retry. A failed send does not use up one of the 10 resends allowed per slot. Each Resend is recorded on the audit chain as SIGN_INVITE_RESENT ("Signing Invitation Re-sent"), and the next automatic reminder waits 12 hours after it. The Lifecycle timeline shows who resent it by name (or sign-in name), or "Requester (identity not recorded)".
A Resend clears a code lock. When the signer is locked out of codes (10 wrong codes, or all 5 codes sent; see OTP Verification), a Resend that is sent or kept for retry also clears the lock: every earlier code stops working, and the signer's code counts start again (3 per code, 10 wrong codes and 5 codes in total). The same link works again. A Resend to a signer who is not locked leaves their codes and counts unchanged. The response lists the cleared slots in codeLocksCleared, and the audit chain records SIGN_OTP_LOCK_CLEARED ("Signing Code Lock Cleared") before the SIGN_INVITE_RESENT entry, with the slot, which limit it was, the counts cleared, and who resent. A Resend that clears a lock counts as one of the 10 resends per slot and keeps the 2-minute gap, so once all 10 are used a lock can no longer be cleared.
Session States
| State | Description |
|---|---|
pending | Session created; no signer has accessed their portal link yet. |
in_progress | At least one signer has activated their slot. |
sealing | All slots complete; processor is generating the final signed PDF and evidence certificate. |
complete | Signed PDF and evidence certificate produced and stored in your subscription. |
cancelled | Session cancelled by the initiator before completion. |
expired | Session TTL elapsed before all slots were completed. No seal occurs; new session required. |
Slot States (Per Signer)
| State | Description |
|---|---|
pending | Invitation sent; signer has not yet opened the portal. |
activated | Signer has opened the portal link. |
viewed | Signer has opened and scrolled through the document. |
otp_verified | Signer has passed the OTP challenge. |
signed | Signer has completed all fields and submitted their signature. |
declined | Signer declined to sign. |
expired | Slot TTL elapsed. |
OTP Verification
Every signer must pass an OTP identity challenge before they can access the document. The portal enforces this unconditionally; it is not a per-slot or per-session setting.
A link alone verifies nothing. The signing link carries only identifiers. Anyone who opens it, including someone it was forwarded to, sees only the identity step: a Full Name field (pre-filled from namePrefill) and a button to send a code. The code goes to the signer's address on the request, and the page has no field for entering a different address. Until the code is verified, the browser is not shown the document, the signer's email address, the requester, or the fields to complete. The name typed on the identity step is saved only once the code is verified.
The code. A 6-digit code, sent by email only (no SMS), valid for 10 minutes. Requesting a new code replaces the previous one.
Each device verifies separately. Verification belongs to the browser it was done in. Opening the link on another device or browser asks for a new code, and so does a cross-device transfer. Only the most recently verified device can sign: verifying on a new device ends the verification on the previous one. A verified device stays verified while it is in use, and needs a new code after 1 hour without activity or 12 hours after the code was entered, whichever comes first.
Failure limits. Every guess is counted before it is checked.
| Limit | What the signer sees | How to recover |
|---|---|---|
| 3 wrong guesses on one code | "Too many incorrect attempts for this code. Request a new code." Verify is disabled. | Send a new code. A new code starts a fresh count of 3. |
| 10 wrong guesses in total for this signer on this request | The identity step is replaced by a Verification locked card: "Too many incorrect verification codes were entered on date and time." and "For your security, no more codes can be sent for this request. Ask requester to resend your invitation; this link will work again once they do." No code can be sent or verified, and the lock stays after a page reload. | The requester resends the invitation (Resend in REFRACT Lifecycle, or POST /api/esign/session/{correlationId}/resend-invite). The Resend clears the lock and the same link works again. See Resend. |
| 5 codes sent for this signer on this request | "The maximum number of codes for this signing request has been sent. Ask the sender to send it again." | The requester resends the invitation. The Resend clears the limit and the counts start again. See Resend. |
After each wrong guess the page shows how many attempts remain. The totals of 10 wrong guesses and 5 codes last for the whole request and are not reset by a successful verification, so verifying again later (after the device verification ends, or on another device) draws on the same totals. Only the requester's Resend resets them, and only while the signer is locked. In the locked card, requester is the requester's name, or the organization name, or "the sender" when neither is on the request; the card also shows a Requested by row when either is known. A page left open on the locked card notices a Resend within 30 seconds while it is visible, or when it regains focus. The lock is recorded on the audit chain as SIGN_OTP_LOCKED ("Signer Locked Out of Codes") with the slot, the number of wrong codes, and when, never a code. Reaching the 5-code limit is not recorded as its own event.
Cross-Device Signing
Signers can transfer their signing session from one device to another, for example opening the portal link on desktop but completing the signature on a phone, without restarting the session or re-sending invitations.
The transfer uses a one-time QR code displayed automatically on the OTP page (desktop) and available via a button inside the signing session. Scanning the QR code on the new device triggers the same OTP identity challenge. Only after passing OTP verification does the new device gain access to the signing session.
Key security properties:
- Token is 256-bit random and single-use
- Minted and stored by the eSign portal itself, in its own Azure Table Storage - not in your Azure subscription
- New device must complete full OTP re-verification. Transfer cannot bypass identity checks.
- ETag conditional write on the completion route prevents two devices from submitting simultaneously
Convenience does not reduce security
Cross-device transfer was designed so that convenience does not reduce security. Every device that participates in a session independently proves the signer's identity via OTP. The one-time token cannot be reused after redemption or after it expires.
See Cross-Device Signing for the full security architecture and constraints.
After Signing or Declining
A signer can open their link again after they have signed or declined. They see a status page, not the signing form.
| The signer | Opens the link | Sees |
|---|---|---|
| Signed | On the device where they verified, while that verification lasts | Signing Complete: "You signed this document on date and time.", then what happens next (below). When the signed copies are ready, links to download them, which work until the envelope's download links expire (7 days from when the documents are ready unless the template sets download.expiresInDays; see Signed Document Downloads). |
| Signed | On any other browser, or after the device verification has ended | Already signed: "You signed this document on date and time.", then what happens next, with no document, downloads, or code prompt. |
| Declined | On any browser | Signing declined: "You declined to sign this document on date and time." and "Requester has been notified. No further action is needed from you." when the request has a requester email, otherwise "No further action is needed from you." |
| Signed or did not sign; another signer declined | On any browser | Request declined: "This signing request was declined by another signer, who declined to sign the document on date and time." (for a decline at the consent step: "who refused to conduct business electronically (policy id version)") and "No signed document will be produced, and no further action is needed from you." |
| Did not sign; the sender cancelled the request | On any browser | Signing cancelled: "This signing request was cancelled on date and time." and "It can no longer be signed, and no further action is needed from you." |
| Did not sign; their window or the request expired | On any browser | Signing request expired: "This signing request expired on date and time." and "It can no longer be signed. Contact the sender if it still needs your signature." |
For a signed signer, what happens next depends on the request: "Other parties still need to sign. You will receive an email with the signed document when everyone has signed.", or "Everyone has signed. The sealed document and its certificate are sent to you by email.", or "The request expired before everyone signed, so no sealed document will be produced. Contact requester if it still needs your signature."
Every one of these pages is the same card: a heading, what happened and when (in the signer's own local time, left out when it is not on record), what happens next, and, on the signed, declined, and locked cards, who sent the request. None shows the session countdown. These pages appear even after the signer's window has passed, so a signer who has signed or declined is never shown the expired card. Two cases still show something else: if the sender revokes the request, a signed signer sees Signing cancelled; and once the request has been removed from the portal (see below), the link shows Invalid Link.
A signing page left open, for example on the first device after a cross-device transfer, checks the request every 30 seconds while it is visible and whenever it regains focus, and replaces itself with the matching card once the signer has finished or the request has ended. These checks do not keep the device's verification alive.
A second submit is refused. If a signature is submitted again for a slot that is already signed, for example from a second tab or a second device, the portal does not show success again. The completion route answers 409 with code ALREADY_SIGNED ("This document has already been signed."), and the page shows the signed card: Signing Complete in the browser where the signer verified, Already signed on any other. A retry of the same submission, such as after a dropped connection, is still accepted once and not counted twice. Refused attempts to sign or decline a finished slot are recorded on the audit chain as SIGN_ATTEMPT_REFUSED ("Signing Attempt Refused"), at most once per slot, reason, and action in each 10-minute window.
How long the portal keeps a finished request
The portal removes signing requests in a daily cleanup at 02:00 UTC. A finished request is kept for 7 days after it finished, so a signer's link keeps showing its status page for that week:
| Request | Kept until |
|---|---|
| Completed (all signed) | 7 days after the last signature |
| Declined by a signer | 7 days after the decline |
| Expired with at least one signature | 7 days after it expired |
| Cancelled by the sender, or expired with no signatures | About 1 hour after its deadline |
A request is removed at the first cleanup after both 7 days from finishing and 1 hour past its deadline have passed. After that, the link shows Invalid Link. This only affects what the portal holds: the signed PDF, evidence certificate, and audit trail stay in your Azure subscription.
Consent Model
Before applying their signature, each signer confirms three consent statements:
- "I have reviewed the complete document and agree to its contents."
- "I consent to use of electronic signatures in place of handwritten signature."
- "I understand this electronic signature represents my intent to sign this document."
The verbatim text of all three statements, and their version identifier, is frozen at signing time and embedded in the evidence certificate, so the record shows exactly what was agreed regardless of any later change to the wording shown to new signers.
Evidence Certificate
When all slots are complete, the processor generates an evidence certificate and appends it to the signed PDF. REFRACT eSign produces simple electronic signatures under the ESIGN Act and UETA.
Certificate Contents:
- Document metadata: title, correlation ID, SHA-256 hash of rendered PDF (the reference hash for checking document integrity)
- Per-signer record: one section per signer, headed "Signer Evidence: Signer N of M, full name (role)" (the role is left out when there is none or it repeats the name; a missing name reads "Name not recorded"). Each section holds:
- Full legal name, email (as a SHA-256 hash), IP address, device (named from the browser, for example "Chrome on Android" or "Edge via Desktop"), signed time, and signature type
- The signature image and its SHA-256 hash, with "Signature Image Match" when the hash was cross-checked against the audit chain
- The signer-reported time and, when there is more than one signer, that signer's own three consent confirmations (Yes, No, or Not Established)
- An Identity Verification table with one row per successful code verification, oldest first, including one made after a cross-device transfer: Verified At (UTC), IP Address, Device, and Signing Device, which reads "Yes" on the verification made on the device that signed and is blank on every other row. A signature captured without a code verification has no table.
- Consent acknowledgments:
- Verbatim text of all three consent statements
- Timestamp each consent was accepted
- Session metadata:
- Signing mode (single/sequential/parallel)
- Session expiration time
- Engine version and tenant ID
- Footer on each page: "Generated by REFRACT eSign - Electronic Signature Certificate" and "Powered by REFRACT Platform - Reference correlation ID".
PII Protection in Storage:
- In the audit trail: signer email is stored as a SHA-256 hash (one-way; not reversible on its own)
- In operational lookup: the plaintext email is AES-256-GCM encrypted at rest, so it can be displayed in the portal without being logged in plaintext
- Field values and the signature PNG: stored in your Azure Table Storage protected by Azure Storage's platform-level encryption at rest, not by the application-layer AES-256-GCM scheme used for the email address
Email hashing
Signer email addresses are stored as SHA-256 hashes in Azure Table Storage. The plaintext email is separately AES-256-GCM encrypted at rest for operational display and is never stored in logs or audit blobs.
Data Handling
Architecture Overview
Customer-Hosted Infrastructure (Your Azure Subscription):
- REFRACT Dispatcher (API, session state management, token generation)
- REFRACT Processor (rendering, signing, sealing, audit logging)
- Azure Blob Storage (documents, PDFs, signed documents, certificates), deployed on every install
- Azure Table Storage (audit records, evidence, session state)
- SharePoint (optional integration for document storage/retrieval)
REFRACT Shared Services by Refract Logic:
- eSign Portal (signing UI, OTP verification, cross-device transfer, its own encrypted session store)
- Notification Service (email delivery via Azure Communication Services)
- Licensing API (metering, session validation, feature entitlements)
What Stays in Your Subscription, and What Crosses
Stored at rest in your Azure subscription:
- Rendered PDF bytes, signed PDF, and evidence certificate
- Signature PNG and field values (after the callback reaches your Dispatcher)
- Signing session and slot state, complete audit trail
Crosses to the REFRACT-hosted portal:
- Signer email address, name, and OTP codes - for invitation and identity verification
- Signature PNG and field values - in memory during the signed callback the portal relays to your Dispatcher; not written to the portal's own storage
- Cross-device transfer tokens - minted and stored by the portal itself, not by your Dispatcher, with the time and browser of the device that made each link. The portal reports a link being made and opened (the browsers involved and the opening device's IP address, never the token) to your Dispatcher for the audit trail
All traffic between the portal and your Dispatcher uses HTTPS with a TLS 1.2 minimum. Invitation and OTP emails are sent through Azure Communication Services, covered under a Data Processing Agreement with Refract Logic.
Compliance is a shared responsibility
This page describes how REFRACT eSign is designed to handle data. Achieving regulatory compliance requires your own policies, controls, and legal review. Refract Logic holds no independent certifications (SOC 2, ISO 27001, HIPAA BAA, or FedRAMP authorization) today. Nothing on this page constitutes legal or compliance advice.
Create Signing Session: Quick Reference
POST /api/esign/sessionAuth: Entra ID JWT. A signed-in user needs the Lifecycle.Reader role; an app-only token needs only the Render.Access application permission. Without it, 403 with code ROLE_MISSING naming the missing role. A session that a render creates through templateSettings.esign needs neither.
Request body:
{
"correlationId": "rnd-00123",
"documentId": "DOC-456",
"docType": "vendor-contract",
"pdfBlobPath": "generated-documents/vendor-contract/2026/06/rnd-00123.pdf",
"pdfHash": "sha256:abc123...",
"signingMode": "sequential",
"expiresInMinutes": 4320,
"signers": [
{
"slotIndex": 0,
"role": "vendor",
"email": "vendor@contoso.com",
"namePrefill": "Vendor Contact"
},
{
"slotIndex": 1,
"role": "manager",
"email": "manager@contoso.com"
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
correlationId | string | Yes | Correlation ID of the rendered PDF to be signed. Used for tracing and audit linking. |
documentId | string | Yes | Document identifier for audit/display purposes (e.g., invoice number, contract ID). |
docType | string | Yes | Document type label (e.g. vendor-contract, purchase-order, consent-form). |
pdfBlobPath | string | Yes | Blob Storage path of the rendered PDF to be signed. |
pdfHash | string | Yes | SHA-256 hash of the rendered PDF (e.g. sha256:abc123...). Stored in the evidence certificate as the reference hash for checking document integrity at signing time. |
signingMode | single | sequential | parallel | Yes | Controls slot activation order and signing logic. |
expiresInMinutes | number | No | Each signer's window, in whole minutes, from 60 to 10080 (7 days). Defaults to 1440 (1 day). Example: 4320 = 3 days. Outside the range: 400 with code ESIGN_WINDOW_INVALID. See Session Expiry. |
signers | array | Yes | One entry per signing slot. Max 10 signers per session. Order matters for sequential mode. |
signers[].slotIndex | number | Yes | Zero-based slot index. Must be unique within the request. Determines signer order. |
signers[].role | string | Yes | Matches the signature block role defined in the PDF template. Determines field positioning. |
signers[].email | string | Yes | Signer's email. Used for invitation delivery and OTP delivery. |
signers[].namePrefill | string | No | Pre-fills the signer name field in the portal. Optional, improves UX. |
templatePath | string | No | Path to the .rpkg template in your SharePoint Templates library. When provided, enables automatic field manifest extraction (initials, date fields). |
reminders | object | No | Automatic reminder settings for this session: enabled, firstAfterDays, repeatEveryDays, maxReminders, expiryWarningDays. Overrides the defaults field by field. Values outside the ranges in Reminders are refused with HTTP 400 (error: "Invalid reminders", with a detail naming the setting). |
download | object | No | The signed-document download links: requireCode and expiresInDays, as in Signed Document Downloads. Combined with the template's templateSettings.esign.download the same way a render's is. Refused with HTTP 400 and code ESIGN_DOWNLOAD_LINK_INVALID, with source naming the side that is wrong. |
ccRecipients | array | No | Up to 5 people, each { "name": "...", "email": "..." }, who are sent the sealed document when the envelope completes. They do not sign. An entry that names a signer, the caller, or an earlier entry is dropped and named in ccDropped. A malformed list or one longer than 5 is refused with HTTP 400 and code ESIGN_CC_RECIPIENTS_INVALID. See CC Recipients. |
Auto-triggering from REFRACT Workflow or a render request
This endpoint is one way to create a signing session directly. The more common path is auto-triggering REFRACT eSign after a render or workflow seal via templateSettings.esign, which uses a related but distinct shape (enabled, signers, signingMode, expiresInMinutes, plus the requester notification fields below). See Auto-Triggering from REFRACT Workflow or a Render Request.
Response 201:
{
"groupId": "a3f1c2d4-8e9b-4f0a-b2c3-1d4e5f6a7b8c",
"expiresAt": "2026-06-12T12:00:00.000Z",
"slots": [
{
"slotIndex": 0,
"sessionId": "b5c2d3e4-9f0a-4b1c-d2e3-2e5f6a7b8c9d",
"portalUrl": "https://portal.refractplatform.com/sign?t=b5c2d3e4-9f0a-4b1c-d2e3-2e5f6a7b8c9d&g=a3f1c2d4-8e9b-4f0a-b2c3-1d4e5f6a7b8c"
},
{
"slotIndex": 1,
"sessionId": "c6d3e4f5-0a1b-4c2d-e3f4-3f6a7b8c9d0e",
"portalUrl": "https://portal.refractplatform.com/sign?t=c6d3e4f5-0a1b-4c2d-e3f4-3f6a7b8c9d0e&g=a3f1c2d4-8e9b-4f0a-b2c3-1d4e5f6a7b8c"
}
]
}| Field | Type | Description |
|---|---|---|
groupId | string | UUID for this signing session. Use this to poll status, cancel, or revoke. |
expiresAt | string | ISO 8601 timestamp when the session expires. |
slots | array | Array of signing slots, one per signer. |
slots[].slotIndex | number | Zero-based slot index (matches request). |
slots[].sessionId | string | Per-slot UUID used by the portal for session state tracking. |
slots[].portalUrl | string | HTTPS URL the signer visits to complete signing, with the session ID (t) and group ID (g) as query parameters. Forward this to the signer via email or your own notification. |
ccCount | number | How many CC recipients the envelope will send the sealed document to. 0 when none were given. |
ccDropped | array | The ccRecipients entries that were dropped, each { "index": 0, "reason": "signer" }. index is the entry's position in the list as sent, from 0; reason is signer, requester, or repeat. Empty when nothing was dropped. |
Poll Session Status
GET /api/esign/session/{correlationId}/statusReturns the current session and per-slot state, keyed on the same correlationId the caller supplied at session creation. This is the polling channel for an integration that does not want email, query it on your own schedule instead of relying on the requester notifications described below.
Auth: Entra ID JWT with the Lifecycle.Reader role (an app-only token also needs Render.Access).
Auto-Triggering from REFRACT Workflow or a Render Request
Instead of calling POST /api/esign/session directly, most integrations trigger REFRACT eSign automatically after a render or a REFRACT Workflow seal by setting templateSettings.esign on the originating request. See Feature Flags for the vaultESign flag and REFRACT Workflow for the post-seal action pattern.
{
"templateSettings": {
"esign": {
"enabled": true,
"signers": [
{ "slotIndex": 0, "role": "manager", "email": "manager@contoso.com" }
],
"signingMode": "sequential",
"expiresInMinutes": 10080,
"organizationName": "Contoso Ltd",
"requestedBy": "Jane Smith",
"requestedByEmail": "jane.smith@contoso.com",
"sendPerSignerNotices": true,
"reminders": { "firstAfterDays": 3, "maxReminders": 2 },
"ccRecipients": [
{ "name": "Legal Team", "email": "legal@contoso.com" }
],
"download": { "requireCode": true, "expiresInDays": 14 }
}
}
}| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | Must be true to trigger REFRACT eSign after sealing. |
signers | ESignAutoTriggerSigner[] | Yes | One entry per signing slot. See the shape below. |
signingMode | single | sequential | parallel | No | Controls slot activation order and signing logic. Defaults to sequential. |
expiresInMinutes | number | No | Each signer's window, in whole minutes, from 60 to 10080. Defaults to 10080 (7 days) on this auto-trigger path. A value outside the range refuses the render with ESIGN_WINDOW_INVALID. See Session Expiry. |
organizationName | string | No | Display name shown as the invitation's requester in the footer text of signer invitations. |
requestedBy | string | No | Display name of the individual requester, shown alongside organizationName in the footer text of signer invitations. |
requestedByEmail | string | No | Email address of the individual requester. Gates all requester notifications, see Requester Notifications below. |
sendPerSignerNotices | boolean | No | Whether the requester is notified as each individual signer completes their slot, in addition to the final documents-ready notification. Defaults to true for multi-signer sessions (sequential, parallel) and false for single-signer sessions. Has no effect unless requestedByEmail is set. |
reminders | object | No | Automatic reminder settings for envelopes triggered from this template (enabled, firstAfterDays, repeatEveryDays, maxReminders, expiryWarningDays). Reminders are on by default; set only the fields you want to change. See Reminders. An out-of-range value fails the render before it runs, with ESIGN_REMINDERS_INVALID. |
ccRecipients | array | No | Up to 5 people, each { "name": "...", "email": "..." }, who are sent the sealed document when the envelope completes. They do not sign. An entry that names a signer, requestedByEmail, or an earlier entry is dropped. A malformed list or one longer than 5 fails the render before it runs, with ESIGN_CC_RECIPIENTS_INVALID. See CC Recipients. |
download | object | No | The signed-document download links: requireCode (boolean, default false) and expiresInDays (whole days from 1 to 30, default 7). A value out of range or of the wrong type fails the render before it runs, with ESIGN_DOWNLOAD_LINK_INVALID. See Signed Document Downloads. |
ESignAutoTriggerSigner:
| Field | Type | Required | Description |
|---|---|---|---|
slotIndex | number | Yes | Zero-based slot index. Matches the signature field layout in the template. |
role | string | Yes | Role label matching the signature field defined in the template. |
email | string | Yes | Signer's email. Where the invitation is sent. |
namePrefill | string | No | Pre-fills the signer's display name in the portal. |
CC Recipients
A CC recipient is told how the envelope ended without signing it. Set them on templateSettings.esign.ccRecipients for a render, or on ccRecipients in the body of POST /api/esign/session. Each entry is { "name": "...", "email": "..." } with a name of 1 to 200 characters and an email address; an envelope takes at most 5.
Entries that would send someone a second copy are dropped, not refused. An entry is dropped when its address is a signer's, the requester's (requestedByEmail on a render, the caller on POST /api/esign/session), or one an earlier entry already has. Addresses are compared without regard to case or surrounding spaces. Each drop is recorded on the envelope's audit chain (on SIGN_SESSION_CREATED, as the entry's position, a hash of its address, and the reason) and named in the answer. A list that is not a list of such entries, or has more than 5, is refused before anything is rendered or created, with ESIGN_CC_RECIPIENTS_INVALID.
The answer says what will be sent. A render that creates an envelope answers with two headers, both always present:
| Header | Value |
|---|---|
X-Refract-ESignCcCount | How many CC recipients the envelope will send to. 0 when none were given. |
X-Refract-ESignCcDropped | Each dropped entry as index:reason, joined by commas, for example 1:signer,3:repeat. index is the entry's position in the list as sent, from 0, and reason is signer, requester, or repeat. none when nothing was dropped. |
A queued render's 202 body also carries ccCount and ccDropped, and POST /api/esign/session answers them in its 201 body (see Create Signing Session). Neither ever carries a dropped entry's address.
After the seal, each CC recipient gets a REFRACT Delivery link. Once the envelope is sealed, the sealed PDF is sent to the CC recipients as one REFRACT Delivery, each with their own /d/ link in a completion email. The link always asks for a one-time code, whatever the template's delivery.requireOtp says, and is never reminded. It carries the sealed document; the evidence certificate is not part of the copy. Its life, consent step, and the organisation named in the email come from the template's templateSettings.delivery block, or the Delivery defaults when the template has none. CC copies are not counted toward any allowance: they reserve nothing and add nothing to the month's eSign or Delivery count. The copies are queued once; if they cannot be queued, the envelope's audit chain records SIGN_CC_COPIES_NOT_SENT with the reason, and they are not retried. If a signer declines, each CC recipient gets a decline notice instead (see When a Signer Declines).
Signed Document Downloads
Once the envelope is sealed, the documents-ready email gives each signer, and the requester when requestedByEmail is set, a link of their own to the portal's download page. Each link is signed for that recipient and carries its own expiry. templateSettings.esign.download sets how those links behave:
| Field | Type | Default | Description |
|---|---|---|---|
requireCode | boolean | false | The recipient enters a one-time code before the download buttons are shown. The code is sent to the address the link was sent to, never to one the page is given, so a forwarded link cannot be used by someone else. A correct code lets that browser download for up to 12 hours, or until the link expires if that is sooner. |
expiresInDays | number | 7 | How long the links work, in whole days from 1 to 30, counted from when the documents are ready. A signer's own completion page offers downloads for the same time. |
The template's values and the request's are combined: expiresInDays on the request may shorten the template's, never lengthen it, and requireCode is on when either side turns it on, so a request cannot turn off a template's true. A value out of range or of the wrong type is refused before anything is rendered or created, with ESIGN_DOWNLOAD_LINK_INVALID; it is never shortened to fit. After a link expires, its recipient may ask for a new one from the expired link's page for 7 more days. The new link is sent to the same address as the expired one, never to another.
Each click is its own short-lived token. The portal holds no document and no long-lived document URL. Each press of a download button asks the Dispatcher for a document token for that one click; the token lasts two minutes, and never past the link's own expiry. The Dispatcher then records DOCUMENT_DOWNLOADED on the envelope's audit chain, naming who the download was for (a signer by slot, or the requester) and whether it came from the documents-ready link or a signer's completion page. That record shows a request was made with that recipient's link, not that the recipient made it: a link can be forwarded.
Requester Notifications
requestedByEmail is not cosmetic
organizationName and requestedBy only affect the footer text on signer invitations. requestedByEmail is the field that matters functionally: without it, an auto-triggered session sends the requester nothing at all, no per-signer notice, no documents-ready email with the download link, and none of the revoke or seal-failure notices. The decline notice goes to the template's contact instead, when the template has one (see When a Signer Declines). If you want the person who requested the document to hear anything back, set requestedByEmail explicitly.
generatedBy on the outer options object (see Feature Flags) is deliberately not a fallback for requestedByEmail, even when both are present on the same request. generatedBy identifies a service principal or system account, and the platform will not use it to assert that a specific individual requested the document.
When requestedByEmail is set, the requester can receive up to five distinct notifications over the life of the session:
| Notification | Sent when | Gated by |
|---|---|---|
| Per-signer notice | Each signer completes their slot | requestedByEmail and sendPerSignerNotices |
| Documents-ready notification | The session completes and the signed PDF is sealed | requestedByEmail |
| Decline notice | A signer declines to sign | requestedByEmail, else the template's contact (see When a Signer Declines) |
| Revoke notice | The session is revoked | requestedByEmail |
| Seal-failure notice | Sealing fails after all slots are signed | requestedByEmail |
When the requester is also a signer. Each person gets one email per event. The requester is not sent a per-signer notice about their own signature. When the signed documents are ready, each address receives one documents-ready email: a requester who signed gets the signer's copy only, and a requester who did not sign gets the requester's email. Addresses are compared without regard to case or surrounding spaces. A decline notice still goes to the requester, and each signing slot still gets its own invitation.
When a Signer Declines
Once a signer's decline is recorded, the Dispatcher sends a decline notice, "Signing declined: document". It says whether the signer declined at the consent step, before opening the document, or at signing, and it carries no link.
| Who | Gets the notice |
|---|---|
| The requester | At requestedByEmail. When the session names no requester address, the notice goes to the template's contact: consent.contacts.withdrawal when it is an email address, else templateSettings.esign.contactEmail. With neither, no notice is sent. |
| Signers who had already signed | Each at their own address: "Another signer declined", and that no signed copy will be sent. A signer who is also the requester gets the requester's notice, not a second one. |
| CC recipients | Each at their own address, unless the requester or a signer was already told at that address. |
| Signers who had not signed yet | No email. Their link shows Request declined (see After Signing or Declining). |
| The signer who declined | No email. Their link shows Signing declined. |
In REFRACT Lifecycle, the decline is its own Signing Declined entry on the timeline, and its details name the stage: "Declined at the consent step: signer refused to conduct business electronically (policy id version)", or "Declined at signing by signer". On the Signatures tab, the signer's status says Declined at consent or Declined at signing. Each decline notice is listed under the entry as Email Queued, or as Email Not Sent with the reason, for example "no requester address and no template contact is on record".
These are requester-facing notifications, distinct from the signer-facing emails (invitation, OTP, completion). Both are sent the same way, following EMAIL_PROVIDER, as described in REFRACT eSign emails. For polling instead of email, see Poll Session Status above.
Cross-Device Signing
Learn how REFRACT eSign's one-time QR transfer lets signers move between devices without restarting sessions or bypassing identity verification.
Dispatcher Deployment and Link Lifecycle
What happens to outstanding signing links when you redeploy or move your Dispatcher, and how to plan around it.
Example: AP 3-Way Match
A complete governance workflow schema for AP 3-way match validation: PO vs. Goods Receipt vs. Invoice. Uses autoValidateOnCreate to run validation without portal interaction, then routes to Finance Director or AP Manager based on invoice total.
Field Manifest
Configure additional signing fields alongside the primary signature, including initials, date fields, checkboxes, and freeform text, using the REFRACT eSign Field Manifest.