Adobe Sign Integration Guide¶
This comprehensive guide covers the Module Suite Extension for Adobe Sign, providing detailed documentation for setting up and using document signing processes within your Content Server environment.
Overview¶
The Module Suite Extension for Adobe Sign enables seamless integration between Content Server and Adobe Sign's electronic signature platform. This extension provides a comprehensive API for:
- Creating and managing signing agreements
- Uploading documents for signature
- Managing participants and workflows
- Tracking agreement status
- Downloading signed documents
- Setting up webhooks for real-time notifications
- Managing reminders and notifications
Key Benefits¶
- Streamlined Workflow: Direct integration with Content Server eliminates manual document handling
- Flexible Participant Management: Support for multiple signature types and participant roles
- Real-time Monitoring: Webhook support for instant status updates
- Comprehensive API: Full access to Adobe Sign's REST API capabilities
- Security: OAuth 2.0 authentication with token management
System Requirements¶
Before implementing the Adobe Sign integration, ensure your system meets the following requirements:
Adobe Sign Requirements¶
- Adobe Sign Developer Account
- Adobe Sign application with appropriate permissions
- Valid Adobe Sign API credentials
Content Server Requirements¶
- Module Suite Base Configuration
- Content Script API support
- Network access to Adobe Sign endpoints
Network Requirements¶
- Outbound HTTPS access to Adobe Sign APIs
- Inbound HTTPS access for webhook callbacks (if using webhooks)
- Firewall configuration to allow Adobe Sign IP ranges
For detailed system requirements, refer to the official Adobe Sign documentation.
Configuration¶
Step 1: Create Adobe Sign Application¶
- Log into your Adobe Sign Developer Console
- Create a new application following the Quickstart
- Configure the OAuth redirect URI to
https://your-domain/your-otcs-path/runcs/am/authflow/adobesign-profile-id - Note down the following credentials:
- Client ID
- Client Secret
- Authorization URI
- Token URI
- Refresh URI
Step 2: Configure Module Suite Profile¶
Create an Adobe Sign profile in the Module Suite Base Configuration with the following properties:
| Property | Description | Example Value |
|---|---|---|
client_id |
Adobe Sign application client ID | 3AAABLblqZhA... |
client_secret |
Adobe Sign application client secret | your-secret-key |
auth_uri |
Authorization endpoint | https://secure.eu1.adobesign.com/public/oauth/v2 |
token_uri |
Token endpoint | https://api.eu1.adobesign.com/oauth/v2/token |
refresh_uri |
Refresh token endpoint | https://api.eu1.adobesign.com/oauth/v2/refresh |
redirect_uri |
Content Server redirect URL | https://your-domain:8443/otcs/cs |
scope |
Application permissions | agreement_write+agreement_read+agreement_send |
additional_props |
Additional properties | api_access_point@,web_access_point@ |
web_app_client_id |
Web application client ID | UB7E5BXCXY |
Step 3: Profile Configuration Example¶
// Example profile configuration
def profileConfig = [
client_id: "3AAABLblqZhA...",
client_secret: "your-secret-key",
auth_uri: "https://secure.eu1.adobesign.com/public/oauth/v2",
token_uri: "https://api.eu1.adobesign.com/oauth/v2/token",
refresh_uri: "https://api.eu1.adobesign.com/oauth/v2/refresh",
redirect_uri: "https://your-domain:8443/otcs/cs",
scope: "agreement_write+agreement_read+agreement_send",
additional_props: "api_access_point@,web_access_point@",
web_app_client_id: "UB7E5BXCXY"
]
Authentication Flow¶
The Adobe Sign integration uses OAuth 2.0 for secure authentication. The following sequence diagram illustrates the complete authentication flow:
sequenceDiagram
participant User as User
participant CS as Content Server
participant AS as Adobe Sign
participant API as Adobe Sign API
User->>CS: Request Adobe Sign operation
CS->>CS: Check for valid access token
alt No valid access token
CS->>AS: Request valid access token using refresh token
alt Refresh token invalid
AS->>CS: Return authentication error
CS->>User: Authentication error, contact the Administrator
else Refresh token valid
AS->>CS: Return access
CS->>CS: Store tokens securely
CS->>API: Make API request with token
API->>CS: Return response
CS->>User: Return result
end
else Valid access token exists
CS->>API: Make API request with token
API->>CS: Return response
CS->>User: Return result
end
Initial Authentication¶
The first time you use an Adobe Sign profile, you must authenticate the application:
def profileId = 'adobesign-profile'
try {
// Validate profile and check for existing authentication
adobesign.validateProfile(profileId)
out << "Successfully authenticated with Adobe Sign profile: ${profileId}"
} catch(AuthenticationException e) {
// Generate authorization URL and redirect user
def redirectUrl = "${url}/runcs/${self.ID}"
def authURL = adobesign.getAuthorizationUrl(profileId, redirectUrl)
log.info("Redirecting to Adobe Sign authorization: ${authURL}")
redirect authURL
}
Token Management¶
The extension automatically handles token refresh:
- Access tokens expire after 1 hour
- Refresh tokens expire after 60 days of inactivity
- Tokens are stored securely in Content Server's system data
- Automatic refresh occurs when needed
Core Concepts¶
Agreements¶
An Agreement is the central concept in Adobe Sign, representing a complete signing transaction. Each agreement contains:
- Documents: Files to be signed
- Participants: People who need to sign or approve, and in what order they need to do so
- Status: Current state of the agreement
Agreement States¶
stateDiagram-v2
[*] --> DRAFT: Create Agreement
DRAFT --> AUTHORING: Start Editing
AUTHORING --> IN_PROCESS: Send for Signature
IN_PROCESS --> COMPLETED: All Signatures Complete
IN_PROCESS --> CANCELLED: Cancel Agreement
IN_PROCESS --> EXPIRED: Time Expired
COMPLETED --> [*]
CANCELLED --> [*]
EXPIRED --> [*]
note right of DRAFT
Agreement created but not sent
end note
note right of AUTHORING
Documents can be modified
end note
note right of IN_PROCESS
Active signing workflow
end note
Participant Roles¶
| Role | Description | Use Case |
|---|---|---|
SIGNER |
Must sign the document | Primary signers |
APPROVER |
Must approve the document | Management approval |
ACCEPTOR |
Must accept terms | Contract acceptance |
FORM_FILLER |
Must fill out forms | Data collection |
CERTIFIED_RECIPIENT |
Receives copy after signing | Audit trail |
NOTARY_SIGNER |
Notarization required | Legal documents |
Document Types¶
Transient Documents¶
- Temporary documents uploaded for specific agreements
- Expire after 7 days
- Used for one-time signing processes
Library Documents¶
- Reusable document templates
- Stored permanently in Adobe Sign
- Ideal for standard contracts and forms
API Reference¶
Service Methods¶
Authentication & Configuration¶
| Method | Description | Parameters |
|---|---|---|
validateProfile(profileId) |
Validates configuration and authentication | profileId (optional) |
getAuthorizationUrl(profileId, redirectUrl, additionalParams) |
Gets OAuth authorization URL | profileId, redirectUrl, additionalParams (optional) |
isClientIdValid(clientId) |
Validates client ID | clientId |
Document Management¶
| Method | Description | Parameters |
|---|---|---|
uploadDocument(document, profileId) |
Upload single document | document (CSDocument/CSResource/File), profileId |
uploadDocuments(documents, profileId) |
Upload multiple documents | documents (List), profileId |
Agreement Management¶
| Method | Description | Parameters |
|---|---|---|
createAndSendAgreement(documents, name, participants, message, profileId) |
Create and send agreement | documents, agreementName, participants, message, profileId |
createAgreementDraft(documents, name, participants, message, profileId) |
Create draft agreement | documents, agreementName, participants, message, profileId |
getAgreementStatus(agreementId, profileId) |
Get agreement status | agreementId, profileId |
getFullAgreementStatus(agreementId, profileId) |
Get complete agreement info | agreementId, profileId |
updateAgreementState(agreementId, state, comment, notifyOthers, profileId) |
Update agreement state | agreementId, state, comment, notifyOthers, profileId |
Document Retrieval¶
| Method | Description | Parameters |
|---|---|---|
downloadAgreement(agreementId, profileId) |
Download combined PDF | agreementId, profileId |
downloadDocument(agreementId, documentId, profileId) |
Download specific document | agreementId, documentId, profileId |
getAuditTrail(agreementId, profileId) |
Get audit trail PDF | agreementId, profileId |
Builder Classes¶
The service provides several builder classes for constructing complex objects:
Agreement Request Builder¶
def agreementRequest = adobesign.newAgreementRequestBuilder()
.withName("Contract Agreement")
.withMessage("Please review and sign this contract")
.withElectronicSignature()
.inProcess()
.withExpirationTime(new Date() + 30) // 30 days from now
.enableDocumentVisibility()
.build()
Participant Set Builder¶
def participantSet = adobesign.newParticipantSetInfoBuilder()
.withRole("SIGNER")
.withOrder(1)
.addRecipient(users.current) // Current user
.addRecipient([email: "user@example.com", name: "John Doe"])
.build()
File Info Builder¶
def fileInfo = adobesign.newFileInfoBuilder()
.withTransientDocumentId("transient-doc-id")
.withLabel("Contract Document")
.build()
Usage Examples¶
The following examples demonstrate how to use the Module Suite Extension for Adobe Sign:
- Basic Agreement Creation
- Advanced Agreement with Multiple Participants
- Using JSON Configuration
- Library Template Management
- Agreement Status Monitoring
Basic Agreement Creation¶
try {
// 1. Retrieve document from Content Server
def contract = docman.getDocument(1234567)
// 2. Upload document to Adobe Sign
def documentId = adobesign.uploadDocument(contract)
// 3. Create participant set
def participants = [
adobesign.createSignerGroup([
[email: "homer@example.com", name: "Homer J. Simpson"]
])
]
// 4. Create and send agreement
def agreementId = adobesign.createAndSendAgreement(
[documentId],
"Springfield Nuclear Power Plant - Employment Contract",
participants,
"Dear Homer, please sign the employment contract."
)
out << "Agreement created successfully with ID: ${agreementId}"
} catch (Exception e) {
log.error("Error creating agreement: ${e.message}", e)
out << "Error: ${e.message}"
}
Advanced Agreement with Multiple Participants¶
try {
// Upload multiple documents
def documents = adobesign.uploadDocuments([
docman.getDocument(1234567),
docman.getDocument(1234568)
])
// Create complex participant workflow
def participants = [
// First: Legal team approval
adobesign.createParticipantSet([
[email: "legal@company.com", name: "Legal Team"]
], 1, "APPROVER"),
// Second: Department heads
adobesign.createParticipantSet([
[email: "hr@company.com", name: "HR Manager"],
[email: "finance@company.com", name: "Finance Manager"]
], 2, "SIGNER"),
// Third: Final approval
adobesign.createParticipantSet([
[email: "ceo@company.com", name: "CEO"]
], 3, "SIGNER")
]
// Create agreement with custom configuration
def agreementId = adobesign.createAndSendAgreement(
documents,
"Multi-Party Service Agreement",
participants,
"Please review and sign the attached service agreement. This document requires approval from legal, department heads, and final CEO approval."
)
out << "Complex agreement created: ${agreementId}"
} catch (Exception e) {
log.error("Error creating complex agreement: ${e.message}", e)
}
Using JSON Configuration¶
For complex agreements, you can use JSON configuration:
try {
def documents = adobesign.uploadDocuments([
docman.getNode(2350027),
docman.getNode(2349903)
])
def agreementConfig = [
participantSetsInfo: [
[
role: "SIGNER",
memberInfos: [
[email: "homer@example.com", name: "Homer J. Simpson"],
[email: "marge@example.com", name: "Marge Simpson"]
],
order: 1
]
],
name: "Family Contract Agreement",
signatureType: "ESIGN",
fileInfos: documents.collect { [transientDocumentId: it] },
state: "IN_PROCESS",
message: "Family contract requiring both signatures",
ccs: [
[email: "lisa@example.com"],
[email: "bart@example.com"]
],
locale: 'en_US',
expirationTime: new Date() + 14 // 14 days from now
]
def agreementId = adobesign.sendAgreement(agreementConfig)
out << "JSON-configured agreement created: ${agreementId}"
} catch (Exception e) {
log.error("Error creating JSON agreement: ${e.message}", e)
}
Library Template Management¶
try {
// Upload documents for template
def uploadedDocs = adobesign.uploadDocuments([
docman.getDocument(1234567),
docman.getDocument(1234568)
])
// Create library files
def libraryFiles = uploadedDocs.collect { docId ->
adobesign.newLibraryFileBuilder()
.withTransientDocumentId(docId)
.build()
}
// Create template
def template = adobesign.newTemplateRequestBuilder()
.withName("Standard Contract Template")
.shareWithUser()
.withTemplateTypeDocument()
.withFileInfos(libraryFiles)
.build()
def templateId = adobesign.createLibraryTemplate(template)
out << "Library template created: ${templateId}"
} catch (Exception e) {
log.error("Error creating library template: ${e.message}", e)
}
Agreement Status Monitoring¶
try {
def agreementId = "your-agreement-id"
// Get basic status
def status = adobesign.getAgreementStatus(agreementId)
out << "<p>Agreement Status: ${status}</p>"
// Get detailed status
def fullStatus = adobesign.getFullAgreementStatus(agreementId)
out << "<p>Detailed Status: ${fullStatus}</p>"
// Check if signed
if (status == "SIGNED") {
// Download signed documents
def responseBody = adobesign.downloadAgreement(agreementId)
if (responseBody) {
CSResource signedPdf = docman.getTempResource("signed-contract.pdf", ".pdf")
signedPdf.content.withOutputStream { os ->
os << responseBody.byteStream()
}
}
}
} catch (Exception e) {
log.error("Error monitoring agreement: ${e.message}", e)
}
Advanced Features¶
Reminder Management¶
Basic Reminder¶
try {
def agreementId = "your-agreement-id"
// Send immediate reminder to next signers
def reminderId = adobesign.sendImmediateReminder(
agreementId,
"Please sign the contract as soon as possible."
)
out << "Reminder sent: ${reminderId}"
} catch (Exception e) {
log.error("Error sending reminder: ${e.message}", e)
}
Scheduled Reminders¶
try {
def agreementId = "your-agreement-id"
//Retrieve the IDs of the next signers in the flow
def participantIds = adobesign.getNextSignersIds(agreementId)
// Create reminder with custom schedule
def reminderId = adobesign.sendReminder(
agreementId,
participantIds,
"Friendly reminder to complete your signature",
24, // First reminder after 24 hours
"AGREEMENT_AVAILABILITY", // Start counting from when agreement becomes available
"DAILY_UNTIL_SIGNED" // Send daily until signed
)
out << "Scheduled reminder created: ${reminderId}"
} catch (Exception e) {
log.error("Error creating scheduled reminder: ${e.message}", e)
}
Deliverable Access¶
try {
def agreementId = "your-agreement-id"
// Create participant access info
def participantAccessInfos = [
adobesign.newParticipantAccessInfoBuilder()
.withParticipantId("participant-id")
.withEmail("user@example.com")
.build()
]
// Generate access URLs
def accessResponse = adobesign.createDeliverableAccessURL(
agreementId,
participantAccessInfos
)
out << "Access URLs generated: ${accessResponse}"
} catch (Exception e) {
log.error("Error creating deliverable access: ${e.message}", e)
}
Business Workspace Integration¶
The Adobe Sign integration can be seamlessly integrated with Content Server's Business Workspace feature, allowing for structured document signing workflows with category-based metadata management.
Integration Workflow¶
The following diagram illustrates the complete Business Workspace integration workflow:
flowchart TD
A[Create Business Workspace] --> B[Configure Agreement Category]
B --> C[Add Documents with Document Category]
C --> D[Execute Agreement Creation Script]
D --> E{Agreement Already Exists?}
E -->|Yes| F[Display Current Status]
E -->|No| G[Extract Agreement Details]
G --> H[Upload Documents to Adobe Sign]
H --> I[Create Participant Sets]
I --> J[Send Agreement for Signature]
J --> K[Update Business Workspace Status]
K --> L[Monitor Agreement Status]
L --> M{Status Changed?}
M -->|No| N[Continue Monitoring]
M -->|Yes| O[Update Local Status]
O --> P{Status = SIGNED?}
P -->|No| N
P -->|Yes| Q[Download Signed Documents]
Q --> R[Add as New Versions]
R --> S[Complete Workflow]
style A fill:#e1f5fe
style S fill:#c8e6c9
style F fill:#fff3e0
style Q fill:#f3e5f5
Category Structure¶
Create the following category structure for managing agreements:
Agreement Category¶
- Agreement Name (Text, 254 chars): Name shown to participants
- Message (Text, 254 chars): Message sent to participants
- Participants (Set):
- Role (Text, 254 chars): SIGNER, APPROVER, etc.
- Order (Integer): Workflow order
- User (User Field): Content Server user
- Full Name (Text, 254 chars): Participant name
- Email (Text, 254 chars): Participant email
- Signature State (Set):
- Agreement ID (Text, 254 chars): Adobe Sign agreement ID
- Status (Text, 254 chars): Current status
Document Category¶
- Transient Document ID (Text, MultiLine): Adobe Sign document ID
- AM Unique ID (Text, 254 chars): Unique identifier for linking
System Architecture¶
The following diagram shows the system architecture and data flow for Business Workspace integration:
graph TB
subgraph "Content Server"
BW[Business Workspace]
AC[Agreement Category]
DC[Document Category]
CS[Content Server Documents]
AS[Adobe Sign Service]
end
subgraph "Adobe Sign Cloud"
ASAPI[Adobe Sign API]
AD[Agreement Data]
TD[Transient Documents]
SD[Signed Documents]
end
subgraph "Monitoring"
POLL[Polling Script]
WH[Webhook Handler]
end
BW --> AC
BW --> DC
BW --> CS
AS --> ASAPI
ASAPI --> AD
ASAPI --> TD
ASAPI --> SD
POLL --> AS
WH --> AS
ASAPI -.->|Status Updates| POLL
ASAPI -.->|Webhook Events| WH
style BW fill:#e3f2fd
style AS fill:#f3e5f5
style ASAPI fill:#fff3e0
style POLL fill:#e8f5e8
style WH fill:#e8f5e8
Business Workspace Setup¶
- Create a category folder Adobe Sign containing the Agreement and Document categories
- Create a Business Workspace structure that uses these categories
- When creating a Business Workspace, configure the Agreement category data
- Assign the Document category to all documents within the Business Workspace
Complete Business Workspace Script¶
The following script demonstrates a complete Business Workspace integration that handles agreement creation, status monitoring, and document synchronization:
/*
* Complete Business Workspace integration script
* Handles agreement creation, status monitoring, and document synchronization
*/
def bwID = params.bwID
try {
if (!bwID) {
throw new ExecutionFaultException("Business Workspace ID is required")
}
// Get Business Workspace
CSNode bw = docman.getNode(bwID)
if (!bw) {
throw new ExecutionFaultException("Business Workspace not found: ${bwID}")
}
def bwNameId = "Business Workspace ${bw.name} (${bw.ID})"
// Check if already processed
def agreementCat = bw."Agreement".asType(Map)
def agreementID = agreementCat["Signature State"]["Agreement ID"].flatten()[0]
def agreementStatus = agreementCat["Signature State"]["Status"].flatten()[0]
if (agreementID) {
out << "${bwNameId} already sent for signature.<br>ID: <b>${agreementID}</b>, Status: <b>${agreementStatus}</b>"
return
}
// Extract agreement details
def agreementName = agreementCat["Agreement Name"][0] ?: bw.name
def message = agreementCat["Message"][0] ?: "Please review and sign the attached documents"
def participants = agreementCat["Participants"]
if (!participants.size()) {
throw new ExecutionFaultException("${bwNameId} has no participants configured")
}
// Get documents from Business Workspace
def bwDocuments = bw.getChildrenFast()
if (!bwDocuments) {
throw new ExecutionFaultException("${bwNameId} has no documents to sign")
}
// Upload documents and create file info objects
def documentsNodes = bwDocuments.collect { it.getOriginalNode() }
def transientDocumentsIds = adobesign.uploadDocuments(documentsNodes)
def transientDocuments = []
transientDocumentsIds.eachWithIndex { docId, index ->
def amUniqueId = UUID.randomUUID().toString()
// Update document category
bwDocuments[index]."Document"."Transient Document ID" = docId
bwDocuments[index]."Document"."AM Unique ID" = amUniqueId
bwDocuments[index].update()
// Create file info for agreement
transientDocuments.add(
adobesign.newFileInfoBuilder()
.withTransientDocumentId(docId)
.withLabel(amUniqueId)
.build()
)
}
// Build participant sets from category data
def participantSets = []
def rolesKeys = participants*."Role".unique()
rolesKeys.each { roleKey ->
def orders = agreementCat["Participants"]
.findAll { it."Role" == roleKey }
.sort { it."Order" }
*."Order".unique()
orders.each { order ->
def orderedParticipants = agreementCat["Participants"]
.findAll { it."Role"[0] == roleKey[0] && it."Order"[0] == order[0] }
def memberInfos = orderedParticipants.collect {
if (it."User"[0]) {
users.getMemberById(it."User"[0])
} else {
[name: it."Full Name"[0], email: it."Email"[0]]
}
}
participantSets.add(
adobesign.newParticipantSetInfoBuilder()
.withRole(roleKey[0].toUpperCase())
.withOrder(order[0] as Integer)
.withParticipantsFromList(memberInfos)
.build()
)
}
}
// Create and send agreement
agreementID = adobesign.createAndSendAgreementWithTransientDocs(
transientDocuments,
agreementName,
participantSets,
message
)
if (agreementID) {
// Update Business Workspace with agreement info
bw."Agreement"."Signature State"."Agreement ID" = agreementID
bw."Agreement"."Signature State"."Status" = "IN_PROCESS"
bw.update()
out << "Agreement created successfully for ${bwNameId}<br>Agreement ID: <b>${agreementID}</b>"
log.info("Agreement created for Business Workspace ${bwNameId}: ${agreementID}")
}
} catch (Exception e) {
log.error("Error processing Business Workspace ${bwID}: ${e.message}", e)
out << "Error: ${e.message}"
}
The Agreement is now created and the documents are being sent to signature. The next step is to check the status of the Agreement and, when SIGNED, to download the signed version of each document.
Status Monitoring and Synchronization¶
An important action to be performed when a signing workflow is concluded is to retrieve the signed documents and synchronize them back on your Content Server system. Module Suite Extension for Adobe Sign doesn't provide an automation, it's up to you to handle these operations in your Content Server system.
Monitoring Approaches¶
The following diagram compares the two monitoring approaches:
graph LR
subgraph "Polling Approach"
P1[Scheduled Script] --> P2[Check All Agreements]
P2 --> P3[Query Adobe Sign API]
P3 --> P4[Update Local Status]
P4 --> P5[Download if Signed]
P5 --> P6[Wait for Next Schedule]
P6 --> P1
end
subgraph "Webhook Approach"
W1[Agreement Status Change] --> W2[Adobe Sign Webhook]
W2 --> W3[Content Server Callback]
W3 --> W4[Process Event]
W4 --> W5[Update Local Status]
W5 --> W6[Download if Signed]
end
style P1 fill:#e3f2fd
style W1 fill:#f3e5f5
style P5 fill:#c8e6c9
style W6 fill:#c8e6c9
To retrieve the signed documents there are two different approaches:
-
Poll Adobe Sign for the agreement status and update the local instance when a change is detected
-
Subscribe to Adobe Sign push notifications for the Agreement status changes (webhook pattern)
Polling-Based Monitoring¶
One way to retrieve the status updates for the Agreement is to poll Adobe Sign to detect changes.
It can be implemented by using the getAgreementStatus(...) API on the adobesign service.
The following code example can be scheduled to periodically check and update all the Adobe Sign agreements in your system (in our scenario, the Business Workspaces root directory).
Correct API Usage
Adobe Sign monitors that the usage of the API is compliant with certain guidelines. Specifically, certain APIs cannot be invoked with a frequency that goes over a certain threshold. When scheduling polling scripts, make sure that the scheduling frequency complies with the Adobe Sign guidelines.
/*
* Scheduled script for monitoring agreement status
* Run this script periodically to check for status changes
*/
import org.apache.commons.io.FilenameUtils
// The path to the Adobe Sign Workspace, modify it to match your setup
def adobeSignWorkspacePath = "Workspaces:Adobe Sign Workspace"
// Retrieves the information about the category in order to be able to retrieve a specific document via the "AM Unique ID" category attribute
def adobeSignDocumentCategory = docman.getCategory("Adobe Sign:Document")
def adobeSignDocumentCategoryDefinitionID = adobeSignDocumentCategory.definitionID
def adobeSignAMUniqueIdAttributeID = adobeSignDocumentCategory.getAttributeID('AM Unique ID')
// The method which peforms query to retrieve the specific document via the "AM Unique ID" category attribute that corresponds to the label of the transient document
def getDocumentByLabel = { amUniqueId ->
try{
// Define SQL code
def sqlCode = """
SELECT
D.DataID "DATAID"
FROM DTree D INNER JOIN LLAttrData LLA
ON D.DataID = LLA.ID AND D.VersionNum = LLA.VerNum
WHERE
LLA.AttrID = ${adobeSignAMUniqueIdAttributeID}
AND LLA.DefID = ${adobeSignDocumentCategoryDefinitionID}
AND LLA.ValStr = '${amUniqueId}'
"""
// Set cursor and transaction enabled flags
Boolean cursorEnabled = true
Boolean transactionEnabled = true
// Set record limit
Integer recordLimit = 1
// Run the SQL query
def result = docman.runSQL(sqlCode, cursorEnabled, transactionEnabled, recordLimit).rows
return result[0].DATAID
} catch(e) {
log.error("Unable to retrieve the Document with the AM Unique ID ${amUniqueId}", e)
}
}
try {
def workspacesNode = docman.getNodeByPath(adobeSignWorkspacePath)
def businessWorkspaces = docman.getChildrenFast(workspacesNode).findAll { it.subtype == 848 }
businessWorkspaces.each { bw ->
def agreementID = bw."Agreement"."Signature State"."Agreement ID"[0]
if (agreementID) {
try {
def agreementStatus = adobesign.getAgreementStatus(agreementID)
def currentStatus = bw."Agreement"."Signature State"."Status"[0]
if (agreementStatus.status != currentStatus) {
// Status changed - update local system
bw."Agreement"."Signature State"."Status" = agreementStatus.status
bw.update()
log.info("Agreement ${agreementID} status updated: ${currentStatus} -> ${agreementStatus.status}")
// Handle completed agreements
if (agreementStatus.status == "SIGNED") {
processSignedAgreement(agreementID, bw)
}
}
} catch (Exception e) {
log.error("Error checking status for agreement ${agreementID}: ${e.message}", e)
}
}
}
} catch (Exception e) {
log.error("Error in agreement monitoring: ${e.message}", e)
}
def processSignedAgreement(agreementID, businessWorkspace) {
try {
// Get agreement documents
def documents = adobesign.getAgreementDocuments(agreementID)
if (documents?.documents) {
documents.documents.each { doc ->
// Find corresponding Content Server document
def bwDocument = getDocumentByLabel(doc.label)
if (bwDocument) {
// Download signed document
def responseBody = adobesign.downloadDocument(agreementID, doc.id)
if (responseBody) {
// Save signed version
CSResource signedPdf = docman.getTempResource(
FilenameUtils.getBaseName(bwDocument.name),
".pdf"
)
signedPdf.content.withOutputStream { os ->
os << responseBody.byteStream()
}
// Add as new version
if (bwDocument.getOriginalNode()) {
bwDocument.getOriginalNode().addVersion(signedPdf.content)
} else {
bwDocument.addVersion(signedPdf.content)
}
log.info("Added signed version for document: ${bwDocument.name}")
}
}
}
}
} catch (Exception e) {
log.error("Error processing signed agreement ${agreementID}: ${e.message}", e)
}
}
Webhook-Based Monitoring¶
Another way to retrieve the Agreement status and the signed Documents is to subscribe to the Adobe Sign push notification. This solution doesn't suffer from the limitations of the polling approach such as the number of requests to Adobe Sign because it will be Adobe Sign itself that pushes the notification when necessary.
To have a better understanding of this pattern, take a look to the Adobe Sign Webhook overview.
To prevent any malicious attack, explicitly allow the Adobe Sign IP ranges for webhooks in your Application Server/Firewall. Take a look to the System Requirements for Adobe Sign.
In order to handle payloads from the Adobe Sign webhooks, the Script Console Extension for Adobe Sign must be installed.
How to create an Adobe Sign webhook¶
There are two ways to handle a webhook in Adobe Sign: - through the Adobe Sign Administration Panel - using the Module Suite Extension for Adobe Sign APIs
Creating a webhook using the Module Suite Extension for Adobe Sign¶
try {
def webhookId = adobesign.createAgreementWebhook(
"WEBHOOK_FROM_SCRIPT",
"https://your_address:8443/csconsole/ext/adobesign/adobeSign.cs",
["AGREEMENT_CREATED", "AGREEMENT_ACTION_COMPLETED", "AGREEMENT_WORKFLOW_COMPLETED"],
true, // Include detailed info
true, // Include participants info
true, // Include documents info
true // Include signed documents
)
out << "Webhook created successfully: ${webhookId}"
} catch (Exception e) {
log.error("Error while creating the webhook on Adobe Sign: ${e.message}", e)
}
Deleting a webhook using the Module Suite Extension for Adobe Sign¶
try {
adobesign.deleteWebhook(webhookId)
out << "Webhook deleted successfully"
} catch (Exception e) {
log.error("Error while deleting the webhook on Adobe Sign: ${e.message}", e)
}
How to handle webhook payloads¶
After installing the extension for the Script Console it is necessary to create an asyncronous callback script for the directory Adobe Sign Sync Inbox. This directory is the receptacle of the payloads from the Adobe Sign webhook events.
Warning
The Module Suite Script Console Extension for Adobe Sign only provides an automation to handle the webhook payloads in your Content Server system under the directory Adobe Sign Sync Inbox, if configured. It's up to you to define an asyncronous callback script to convert the payload in your system.
The following script is an example of callback that saves the signed document in the Business Workspace structure.
import org.apache.commons.io.FilenameUtils;
// Child node create is triggered when you add to a container
// a node that was not already on Content Server
log.debug( "Executing Adobe Sign synchronization callback. Adobe Sign data doc: '{}', parent folder: '{}', callbackID: '{}', eventSourceID: '{}'", newNodeID, nodeID, callbackID, eventSourceID)
def adobeSignAgreementCategory
def adobeSignAgreementCategoryDefinitionID
def adobeSignAgreementIDAttributeID
def adobeSignDocumentCategory
def adobeSignDocumentCategoryDefinitionID
def adobeSignAMUniqueIdAttributeID
try {
// Retrieves the information about the category in order to be able to retrieve a specific workspace via the "Agreement ID" category attribute
adobeSignAgreementCategory = docman.getCategory("Adobe Sign:Agreement")
adobeSignAgreementCategoryDefinitionID = adobeSignAgreementCategory.definitionID
adobeSignAgreementIDAttributeID = adobeSignAgreementCategory.getAttributeID('Agreement ID')
// Retrieves the information about the category in order to be able to retrieve a specific document via the "AM Unique ID" category attribute
adobeSignDocumentCategory = docman.getCategory("Adobe Sign:Document")
adobeSignDocumentCategoryDefinitionID = adobeSignDocumentCategory.definitionID
adobeSignAMUniqueIdAttributeID = adobeSignDocumentCategory.getAttributeID('AM Unique ID')
} catch( Exception e ) {
log.error("Error retrieving categories for Adobe Sign synchronization callback:" , e)
}
// The method which peforms query to retrieve the specific document via category attribute
def getNodeByCatAttrValue = { defID, attrID, valStr ->
try{
// Define SQL code
def sqlCode = """
SELECT
D.DataID "DATAID"
FROM DTree D INNER JOIN LLAttrData LLA
ON D.DataID = LLA.ID AND D.VersionNum = LLA.VerNum
WHERE
LLA.DefID = ${defID}
AND LLA.AttrID = ${attrID}
AND LLA.ValStr = '${valStr}'
"""
// Set cursor and transaction enabled flags
Boolean cursorEnabled = true
Boolean transactionEnabled = true
// Set record limit
Integer recordLimit = 1
// Run the SQL query
def result = docman.runSQL(sqlCode, cursorEnabled, transactionEnabled, recordLimit).rows
return result[0]?.DATAID
} catch(e) {
log.error("Unable to retrieve the Document with the AM Unique ID ${valStr}", e)
}
}
try{
def newDocument = asCSNode(newNodeID)
if( newDocument.mimeType == "application/json" ){
def jsonSlurper = new JsonSlurper()
def jsonResponse = jsonSlurper.parseText(newDocument.content.content.text)
def agreement = jsonResponse.agreement
if (agreement){
def agreementID = agreement.id
// Retrieves the Workspace linked to the agreement via the Agreement ID category attribute value and update its status
def bwID = getNodeByCatAttrValue(adobeSignAgreementCategoryDefinitionID, adobeSignAgreementIDAttributeID, agreementID)
if (bwID){
def bWorkspace = docman.getNodeFast(bwID)
if (bWorkspace){
bWorkspace."Agreement"."Status" = agreement.status
bWorkspace.update()
// If the workflow as been completed and the agreement is SIGNED, download and store the signed version of each document
if (agreement.status == "SIGNED"){
log.debug("#### Process Adobe Sign Webhook File - agreement signed: {}", agreementID)
agreement.documentsInfo.documents.each{
// Retrieves the Content Server document linked to the agreement document using the AM Unique ID attribute value
def bwDocumentID = getNodeByCatAttrValue(adobeSignDocumentCategoryDefinitionID, adobeSignAMUniqueIdAttributeID, it.label)
if (bwDocumentID) {
def bwDocument = docman.getNodeFast(bwDocumentID)
if (bwDocument){
// Download the signed document
def responseBody = adobesign.downloadDocument(agreementID, it.id)
if (responseBody){
CSResource pdfFile = docman.getTempResource(FilenameUtils.getBaseName(bwDocument.name), ".pdf")
pdfFile.content.withOutputStream { os ->
os << responseBody.byteStream()
}
// Add the signed version to the original node, if exists
if (bwDocument instanceof CSGeneration) {
bwDocument.getOriginalNode().addVersion(pdfFile.content)
} else {
bwDocument.addVersion(pdfFile.content)
}
log.error("Added signed version for the document ${bwDocument.name} for ${bWorkspace.name}")
}
}
}
}
}
}
}
}
}
} catch( Exception e ) {
log.error("Error synchronizing Adobe Sign data. Data file ID: {}", newNodeID, e)
}
Troubleshooting¶
Common Issues¶
Authentication Errors¶
Problem: AuthenticationException when calling Adobe Sign methods
Solution: 1. Verify profile configuration 2. Check if user has completed OAuth flow 3. Ensure tokens are not expired
try {
adobesign.validateProfile("your-profile-id")
} catch (AuthenticationException e) {
// Redirect to authorization
def authURL = adobesign.getAuthorizationUrl("your-profile-id", redirectUrl)
redirect authURL
}
Document Upload Failures¶
Problem: Documents fail to upload to Adobe Sign
Solution: 1. Check document format (PDF recommended) 2. Verify document size limits 3. Ensure network connectivity
try {
def documentId = adobesign.uploadDocument(document)
} catch (CSAdobeSignDocumentUploadException e) {
log.error("Document upload failed: ${e.message}")
// Handle specific upload errors
}
Agreement Creation Failures¶
Problem: Agreements fail to create or send
Solution: 1. Validate participant information 2. Check document requirements 3. Verify agreement configuration
try {
def agreementId = adobesign.createAndSendAgreement(documents, name, participants, message)
} catch (ExecutionFaultException e) {
log.error("Agreement creation failed: ${e.message}")
// Check specific error details
}
Debugging Tips¶
-
Enable Detailed Logging:
log.setLevel(Level.DEBUG) -
Validate Configuration:
def isValid = adobesign.isClientIdValid("your-client-id") -
Check Token Status:
def token = adobesign.getAccessTokenFromStore("profile-id") if (token) { log.info("Token expires: ${token.expireDate}") } -
Test API Connectivity:
try { def baseUris = adobesign.getBaseUris("profile-id") log.info("Connected to Adobe Sign: ${baseUris}") } catch (Exception e) { log.error("Connection failed: ${e.message}") }
Performance Considerations¶
- Batch Operations: Use
uploadDocuments()for multiple files - Async Processing: Use webhooks instead of polling when possible
- Error Handling: Implement proper retry logic for transient failures
Security Best Practices¶
- Access Control: Limit Adobe Sign profile access to authorized users
- Network Security: Use HTTPS for all communications
- Audit Trail: Log all Adobe Sign operations for compliance
Additional Resources¶
- Adobe Sign REST API Documentation
- Adobe Sign Developer Guide
- Module Suite Documentation
- Content Script API Reference
This guide provides comprehensive coverage of the Module Suite Extension for Adobe Sign. For additional support or advanced use cases, please refer to the official documentation or contact your system administrator.