Skip to content
PDF

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

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

  1. Log into your Adobe Sign Developer Console
  2. Create a new application following the Quickstart
  3. Configure the OAuth redirect URI to https://your-domain/your-otcs-path/runcs/am/authflow/adobesign-profile-id
  4. Note down the following credentials:
  5. Client ID
  6. Client Secret
  7. Authorization URI
  8. Token URI
  9. 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

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

  1. Create a category folder Adobe Sign containing the Agreement and Document categories
  2. Create a Business Workspace structure that uses these categories
  3. When creating a Business Workspace, configure the Agreement category data
  4. 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:

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

  1. Enable Detailed Logging:

    log.setLevel(Level.DEBUG)
    

  2. Validate Configuration:

    def isValid = adobesign.isClientIdValid("your-client-id")
    

  3. Check Token Status:

    def token = adobesign.getAccessTokenFromStore("profile-id")
    if (token) {
        log.info("Token expires: ${token.expireDate}")
    }
    

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

  1. Batch Operations: Use uploadDocuments() for multiple files
  2. Async Processing: Use webhooks instead of polling when possible
  3. Error Handling: Implement proper retry logic for transient failures

Security Best Practices

  1. Access Control: Limit Adobe Sign profile access to authorized users
  2. Network Security: Use HTTPS for all communications
  3. Audit Trail: Log all Adobe Sign operations for compliance

Additional Resources


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.