DocuSign Embedded Signing with Elixir
View SourceThis LiveBook demonstrates how to create a complete embedded signing experience with the DocuSign Elixir SDK. It walks you through the entire process from authentication to retrieving a signed document.
Mix.install([
{:docusign, "~> 2.0.0"},
{:kino, "~> 0.15.3"}
])
Introduction
This LiveBook demonstrates how to create an embedded signing experience with the DocuSign Elixir SDK. Embedded signing allows you to integrate the DocuSign signing process directly into your application, keeping users in your environment.
Configuration
First, let's set up our DocuSign configuration:
alias Kino.Input
# Set up input forms for configuration
private_key_input = Input.textarea("Private Key (PEM format)",
placeholder: "Paste your RSA private key here including BEGIN and END markers",
label: "Your DocuSign integration's private key")
client_id_input = Input.text("Client ID (Integration Key)",
label: "Found in the DocuSign admin under Apps & Keys")
account_id_input = Input.text("Account ID",
label: "Your DocuSign account ID (found under Apps & Keys)")
user_id_input = Input.text("User ID",
label: "The API Username of the DocuSign user to impersonate")
is_sandbox_input = Input.checkbox("Use Sandbox", default: true)
return_url_input = Input.text("Return URL",
label: "Where the signer will be redirected after signing",
default: "https://www.docusign.com")
# Display input forms individually for proper rendering
Kino.render(private_key_input)
Kino.render(client_id_input)
Kino.render(account_id_input)
Kino.render(user_id_input)
Kino.render(is_sandbox_input)
Kino.render(return_url_input)
Now let's configure the DocuSign client:
# Get values from inputs
private_key = Kino.Input.read(private_key_input)
client_id = Kino.Input.read(client_id_input)
account_id = Kino.Input.read(account_id_input)
user_id = Kino.Input.read(user_id_input)
is_sandbox = Kino.Input.read(is_sandbox_input)
return_url = Kino.Input.read(return_url_input)
# Fix key format if needed
private_key_formatted =
private_key
|> String.replace(~r/\r\n/, "\n") # Convert Windows line endings
|> String.trim() # Remove leading/trailing whitespace
# Configure DocuSign application
hostname = if is_sandbox, do: "account-d.docusign.com", else: "account.docusign.com"
# Use runtime configuration instead of Mix.Config
Application.put_env(:docusign, :hostname, hostname)
Application.put_env(:docusign, :client_id, client_id)
Application.put_env(:docusign, :private_key_contents, private_key_formatted)
Create a Connection
Let's establish a connection to the DocuSign API:
# Create a connection with the specified user
connection_result = DocuSign.Connection.get(user_id)
conn = case connection_result do
{:ok, connection} ->
IO.puts("✅ Successfully connected to DocuSign")
connection
{:error, {:consent_required, consent_message}} when is_binary(consent_message) ->
original_url = Regex.run(~r/https:\/\/[^\s]+/, consent_message) |> List.first()
# Fix the URL if it's using the old redirect URI format
url =
if String.contains?(original_url, "redirect_uri=https://account") do
String.replace(original_url,
~r/redirect_uri=https:\/\/account[^&]+/,
"redirect_uri=https://www.docusign.com")
else
original_url
end
# Only use Kino.Markdown for cleaner output
Kino.Markdown.new("""
## ⚠️ DocuSign User Consent Required
Before using the API, you need to grant consent for this application to act on your behalf.
1. **Click this link**: [Grant Consent](#{url})
2. Sign in to DocuSign if prompted
3. Review and click "ALLOW ACCESS"
4. Return here and run this cell again
This is a one-time process per user.
> **Note**: If you're seeing redirect URI errors, ensure your DocuSign application has
> `https://www.docusign.com` configured as a redirect URI.
""") |> Kino.render()
nil # Return nil as conn value to avoid undefined variable
{:error, reason} ->
IO.puts("❌ Failed to connect to DocuSign")
IO.inspect(reason, label: "Error")
nil # Return nil as conn value to avoid undefined variable
end
Set Up Recipient Information
Let's create a form to input the recipient's information:
# Verify that conn is available from previous cell
conn = conn || (
IO.puts("⚠️ Connection not established. Please run the previous cell to connect to DocuSign.")
nil
)
recipient_name_input = Input.text("Recipient Name")
recipient_email_input = Input.text("Recipient Email")
client_user_id_input = Input.text("Client User ID", default: "1001",
placeholder: "Unique identifier for the recipient")
# Render each input individually for proper display
Kino.render(recipient_name_input)
Kino.render(recipient_email_input)
Kino.render(client_user_id_input)
Create a Document
Let's create a simple document for signing:
# Create a simple HTML document
html_document = """
<!DOCTYPE html>
<html>
<head>
<title>DocuSign Embedded Signing Example</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.header { text-align: center; margin-bottom: 30px; }
.content { margin-bottom: 50px; }
.signature-section { margin-top: 50px; border-top: 1px solid #ccc; padding-top: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>Embedded Signing Agreement</h1>
<p>Created with DocuSign Elixir SDK in LiveBook</p>
</div>
<div class="content">
<p>This document demonstrates embedded signing with the DocuSign Elixir SDK.</p>
<p>By signing this document, the signer acknowledges that:</p>
<ul>
<li>They have read and understood the embedded signing process</li>
<li>They agree to the terms outlined in this document</li>
<li>This is a demonstration of embedded signing capabilities</li>
</ul>
</div>
<div class="signature-section">
<p>Signed by: <span style="text-decoration: underline; padding: 0 100px;">____________________</span></p>
<p>Date: <span style="text-decoration: underline; padding: 0 100px;">____________________</span></p>
</div>
</body>
</html>
"""
# Base64 encode the document
encoded_document = Base.encode64(html_document)
Create an Envelope for Embedded Signing
Now let's create an envelope specifically for embedded signing. For embedded signing, we must:
- Create an envelope with a recipient that has a
clientUserId
(this identifies the recipient as embedded) - Set the envelope status to "created" (not "sent")
recipient_name = Kino.Input.read(recipient_name_input)
recipient_email = Kino.Input.read(recipient_email_input)
client_user_id = Kino.Input.read(client_user_id_input)
# Use DocuSign SDK models for proper object creation
# Create document object
document = %DocuSign.Model.Document{
documentBase64: encoded_document,
name: "Embedded Signing Example.html",
fileExtension: "html",
documentId: "1"
}
# Create the signer with signature and date tabs
sign_here_tab = %DocuSign.Model.SignHere{
anchorString: "Signed by:", # More specific anchor text
anchorUnits: "pixels",
anchorYOffset: "-5", # Slightly negative to position just above the line
anchorXOffset: "110", # Slightly increased from original 100
documentId: "1", # Match the document ID
recipientId: "1" # Match the recipient ID
}
date_signed_tab = %DocuSign.Model.DateSigned{
anchorString: "Date:", # Matches text in our document
anchorUnits: "pixels",
anchorYOffset: "-10", # Keep negative value to position above the line
anchorXOffset: "80", # Increased from 50 to better center the date
documentId: "1", # Match the document ID
recipientId: "1" # Match the recipient ID
}
# Create the signer object with signature and date tabs
signer = %DocuSign.Model.Signer{
email: recipient_email,
name: recipient_name,
recipientId: "1",
routingOrder: "1",
clientUserId: client_user_id, # This identifies the recipient as embedded
tabs: %DocuSign.Model.Tabs{
signHereTabs: [sign_here_tab],
dateSignedTabs: [date_signed_tab]
}
}
# Create envelope definition with status "sent"
envelope_definition = %DocuSign.Model.EnvelopeDefinition{
emailSubject: "Please sign this document (Embedded Signing)",
documents: [document],
recipients: %DocuSign.Model.Recipients{
signers: [signer]
},
status: "sent" # Important: For embedded signing, use "sent" (not "created")
}
# Create the envelope using the standard API - note we use body: instead of envelopeDefinition:
result = DocuSign.Api.Envelopes.envelopes_post_envelopes(conn, account_id, body: envelope_definition)
# Create a variable to hold the envelope ID
envelope_id =
try do
case result do
{:ok, response} ->
id = response.envelopeId
IO.puts("✅ Envelope created successfully!")
IO.puts("Envelope ID: #{id}")
# Return the envelope ID
id
{:error, error} ->
IO.puts("❌ Failed to create envelope")
IO.inspect(error, label: "Error details:")
nil
end
rescue
e in KeyError ->
IO.puts("⚠️ Received error response with unexpected format")
IO.inspect(e, label: "KeyError details")
IO.inspect(result, label: "Full response")
nil
end
Create the Embedded Signing URL
Now that we have created the envelope, we need to generate a URL that will allow the recipient to sign the document within your application:
# Verify that conn is available from previous cells
conn = conn || (
IO.puts("⚠️ Connection not established. Please run the previous cells to connect to DocuSign.")
nil
)
# Check if envelope_id is available from previous cell
# If not defined in previous cell execution, we'll let the user input it
envelope_id_input =
if envelope_id do
Input.text("Envelope ID", default: envelope_id)
else
Input.text("Envelope ID")
end
Kino.render(envelope_id_input)
signing_envelope_id = Kino.Input.read(envelope_id_input)
# Use client_user_id from previous cell if available
client_user_id_to_use = client_user_id
# Create the recipient view request properly using the DocuSign SDK model
recipient_view_request = %DocuSign.Model.RecipientViewRequest{
authenticationMethod: "none",
clientUserId: client_user_id_to_use, # Must match the clientUserId used in envelope creation
recipientId: "1", # Must match the recipientId used in envelope creation
returnUrl: return_url,
userName: recipient_name,
email: recipient_email
}
IO.puts("\nCreating recipient view URL for:")
IO.puts("- Envelope ID: #{signing_envelope_id}")
IO.puts("- Client User ID: #{client_user_id_to_use}")
IO.puts("- Return URL: #{return_url}")
# Get the recipient view URL using the standard API
result = DocuSign.Api.EnvelopeViews.views_post_envelope_recipient_view(
conn,
account_id,
signing_envelope_id,
body: recipient_view_request
)
# Create a frame at the beginning to hold the signing URL
signing_frame = Kino.Frame.new() |> Kino.render()
try do
case result do
{:ok, view_response} ->
signing_url = view_response.url
IO.puts("✅ Embedded signing URL generated successfully!")
# Render the HTML into the existing frame (replaces content instead of duplicating)
Kino.Frame.render(signing_frame, Kino.HTML.new("""
<div style="margin: 20px 0; padding: 15px; border: 1px solid #e0e0e0; border-radius: 5px; background-color: #f9f9f9;">
<h3 style="margin-top: 0;">Embedded Signing URL Generated</h3>
<p>Click the button below to open the DocuSign signing experience in a new tab:</p>
<a href="#{signing_url}" target="_blank" style="display: inline-block; background: #2F80ED; color: white;
padding: 10px 20px; text-decoration: none; border-radius: 4px; font-weight: bold;">
Open Signing Experience
</a>
</div>
"""))
{:error, error} ->
IO.puts("❌ Failed to generate embedded signing URL")
IO.inspect(error, label: "Error details:")
end
rescue
e in KeyError ->
IO.puts("⚠️ Received error response with unexpected format")
IO.inspect(e, label: "KeyError details")
IO.inspect(result, label: "Full response")
end
Envelope Status and Document Retrieval
After the recipient completes the signing process, the system will automatically check the envelope status and retrieve the completed document if available.
# Create status frame to display envelope status
status_frame = Kino.Frame.new() |> Kino.render()
# Create document frame to display retrieved document
document_frame = Kino.Frame.new() |> Kino.render()
# Verify that conn is available from previous cells
conn = conn || (
IO.puts("⚠️ Connection not established. Please run the previous cells to connect to DocuSign.")
nil
)
# Function to check status and update UI
check_envelope_status = fn ->
# Get envelope status using DocuSign client library
case DocuSign.Api.Envelopes.envelopes_get_envelope(conn, account_id, signing_envelope_id) do
{:ok, envelope} ->
status = envelope.status
status_description = case status do
"created" -> "The envelope has been created, but not yet signed."
"sent" -> "The envelope has been sent to the recipient, but not yet signed."
"delivered" -> "The envelope has been delivered to the recipient, but not yet signed."
"completed" -> "The envelope has been signed by all recipients."
"declined" -> "The envelope has been declined by at least one recipient."
"voided" -> "The envelope has been voided."
_ -> "The envelope is in an unknown state."
end
# Update status frame with current status
Kino.Frame.render(status_frame, Kino.Markdown.new("""
### Current Envelope Status: #{status}
#{status_description}
"""))
# Get recipient information
case DocuSign.Api.EnvelopeRecipients.recipients_get_recipients(conn, account_id, signing_envelope_id) do
{:ok, recipients} ->
if recipients.signers && length(recipients.signers) > 0 do
# Create a table of recipient info
table = recipients.signers
|> Enum.map(fn signer ->
%{
name: signer.name,
email: signer.email,
status: signer.status,
delivered: signer.deliveredDateTime || "N/A",
signed: signer.signedDateTime || "N/A"
}
end)
|> Kino.DataTable.new()
# Add the table to the status frame
Kino.Frame.append(status_frame, table)
end
{:error, _} ->
Kino.Frame.append(status_frame, Kino.Text.new("Could not retrieve recipient information"))
end
# If envelope is completed, automatically retrieve the document
if status == "completed" do
# Get the document using the DocuSign client library
result = DocuSign.Api.EnvelopeDocuments.documents_get_document(
conn,
account_id,
"1", # Document ID
signing_envelope_id,
decode_body: false # Get raw response to handle PDF
)
case result do
{:ok, %Tesla.Env{status: 200, body: document_content}} ->
# Check content type from headers
content_type =
result
|> elem(1)
|> Map.get(:headers)
|> Enum.find(fn {k, _v} -> String.downcase(k) == "content-type" end)
|> elem(1)
cond do
# For HTML content, display in iframe
String.contains?(content_type, "html") ->
Kino.Frame.render(document_frame, Kino.HTML.new("""
<div>
<h3>✅ Document Retrieved Successfully</h3>
<iframe
srcdoc="#{String.replace(document_content, "\"", """)}"
style="width: 100%; height: 500px; border: 1px solid #ccc;">
</iframe>
</div>
"""))
# For PDF content, create a download link
String.contains?(content_type, "pdf") ->
pdf_base64 = Base.encode64(document_content)
Kino.Frame.render(document_frame, Kino.HTML.new("""
<div>
<h3>✅ PDF Document Retrieved</h3>
<a href="data:application/pdf;base64,#{pdf_base64}" download="signed_document.pdf"
style="display:inline-block; background:#4CAF50; color:white; padding:10px 15px;
text-decoration:none; border-radius:4px; margin-top:10px;">
Download Signed PDF
</a>
</div>
"""))
# For other content types
true ->
Kino.Frame.render(document_frame, Kino.Text.new("Document retrieved but cannot be displayed (#{content_type})"))
end
{:error, error} ->
Kino.Frame.render(document_frame, Kino.Text.new("❌ Failed to retrieve document: #{inspect(error)}"))
end
end
# Return the status for potential further processing
{:ok, status}
{:error, error} ->
Kino.Frame.render(status_frame, Kino.Text.new("❌ Failed to retrieve envelope status: #{inspect(error)}"))
{:error, error}
end
end
# Check status immediately on cell execution
check_envelope_status.()
# Create a manual refresh button (more compatible with Kino 0.10.0)
refresh_button = Kino.Control.button("Refresh Status")
Kino.render(refresh_button)
Kino.listen(refresh_button, fn _ -> check_envelope_status.() end)
Conclusion
This LiveBook demonstrates how to create a complete embedded signing experience with the DocuSign Elixir SDK.
You've learned how to:
- Create an envelope with a recipient specifically marked for embedded signing
- Use status="sent" with a clientUserId to enable embedded signing
- Generate an embedded signing URL
- Automatically monitor the envelope status and retrieve the completed document
In a real application, you would typically:
- Create the envelope with a recipient marked for embedded signing (status="sent" and include clientUserId)
- Generate the embedded signing URL
- Redirect the user to the URL or embed it in an iframe
- Handle the redirect back to your application after signing
- Check the status of the envelope and retrieve the completed documents
For more information, check out the DocuSign Elixir GitHub repository and the official DocuSign API documentation.