Skip to content

Create and send your first envelope

This tutorial walks you through creating and sending an envelope via the API using the api-proxy. You’ll learn how to:

  1. Create an envelope
  2. Encrypt your document
  3. Upload the encrypted document to S3
  4. Confirm the document upload
  5. Add recipients
  6. Add blocks (signatures, text, images)
  7. Send the envelope
  • A Subnoto workspace with API credentials (access key and secret key)
  • The API proxy container running (see API Proxy Usage)
  • A programming language with HTTP client support (examples shown in Node.js)

Note: This tutorial presents examples in Node.js, but the Subnoto API can be used from any programming language. The API is language-agnostic and uses standard HTTP requests and JSON payloads. You can adapt these examples to Python, Go, Ruby, PHP, or any other language that supports HTTP requests.

For Node.js examples, install the required dependency for S3 uploads:

Terminal window
npm install form-data

Start by creating an envelope with a document placeholder:

const API_BASE_URL = "http://localhost:8080";
const ACCESS_KEY = "your-access-key";
const SECRET_KEY = "your-secret-key";
const WORKSPACE_UUID = "your-workspace-uuid";
// Create envelope
const createResponse = await fetch(`${API_BASE_URL}/public/envelope/create`, {
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_KEY}:${SECRET_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
workspaceUuid: WORKSPACE_UUID,
envelopeTitle: "My First Envelope"
})
});
const createResult = await createResponse.json();
const { envelopeUuid, documentUuid, presignedS3Params, revisionEncryptionKey } = createResult;

The response includes:

  • envelopeUuid: The unique identifier for your envelope
  • documentUuid: The unique identifier for the document
  • presignedS3Params: Pre-signed S3 URL and fields for uploading the encrypted document
  • revisionEncryptionKey: Base64-encoded encryption key for encrypting your document

Read your PDF file and encrypt it using AES-256-GCM with the revision encryption key:

import { readFileSync } from "fs";
import { createCipheriv, randomBytes } from "crypto";
// Read your PDF file
const pdfBuffer = readFileSync("path/to/your/document.pdf");
// Decode the base64 encryption key
const keyBuffer = Buffer.from(revisionEncryptionKey, "base64");
if (keyBuffer.length !== 32) {
throw new Error("Key must be 32 bytes (256-bit)");
}
// Generate a random IV (12 bytes for GCM)
const iv = randomBytes(12);
// Create the cipher
const cipher = createCipheriv("aes-256-gcm", keyBuffer, iv);
// Set the Additional Authenticated Data (AAD) with the document UUID
const contextJSON = JSON.stringify({ documentUuid });
cipher.setAAD(Buffer.from(contextJSON));
// Encrypt the document
const encrypted = Buffer.concat([cipher.update(pdfBuffer), cipher.final()]);
// Get the authentication tag
const authTag = cipher.getAuthTag();
// Combine IV, encrypted data, and auth tag
const encryptedDocument = Buffer.concat([iv, encrypted, authTag]);

Upload the encrypted document using the pre-signed S3 parameters:

import FormData from "form-data";
// Create FormData for multipart upload
const formData = new FormData();
// Add all required S3 fields
Object.entries(presignedS3Params.fields).forEach(([key, value]) => {
formData.append(key, value);
});
// Add the encrypted file
formData.append("file", encryptedDocument, {
filename: "document.pdf",
contentType: "application/octet-stream"
});
// Upload to S3
const uploadResponse = await fetch(presignedS3Params.url, {
method: "POST",
body: formData
});
if (!uploadResponse.ok) {
throw new Error(`S3 upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
}

After uploading to S3, confirm the upload with the API:

const confirmResponse = await fetch(`${API_BASE_URL}/public/envelope/complete-document-upload`, {
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_KEY}:${SECRET_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
workspaceUuid: WORKSPACE_UUID,
envelopeUuid,
documentUuid
})
});
if (!confirmResponse.ok) {
throw new Error(`Failed to confirm upload: ${confirmResponse.status}`);
}

Add recipients to your envelope. Recipients can be added as manual entries, contacts, or workspace members:

const addRecipientsResponse = await fetch(`${API_BASE_URL}/public/envelope/add-recipients`, {
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_KEY}:${SECRET_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
workspaceUuid: WORKSPACE_UUID,
envelopeUuid,
recipients: [
{
type: "manual",
firstname: "John",
lastname: "Doe"
}
]
})
});
const recipientsResult = await addRecipientsResponse.json();

You can add multiple recipients (up to 50) at once. Each recipient can be:

  • type: 'manual' - Manual entry with email, firstname, lastname
  • type: 'contact' - Existing contact using contactUuid
  • type: 'member' - Workspace member using userUuid

Add blocks to your document. Blocks can be signatures, text, or images:

const addBlocksResponse = await fetch(`${API_BASE_URL}/public/envelope/add-blocks`, {
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_KEY}:${SECRET_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
workspaceUuid: WORKSPACE_UUID,
envelopeUuid,
documentUuid,
blocks: [
{
type: "signature",
page: "1",
x: 100,
y: 200,
recipientEmail: "[email protected]"
},
{
type: "text",
page: "1",
x: 100,
y: 250,
text: "Please sign here",
width: 200,
height: 30
}
]
})
});
const blocksResult = await addBlocksResponse.json();

Signature Block:

{
type: 'signature',
page: '1', // Page number as string
x: 100, // X position in points
y: 200, // Y position in points
recipientEmail: '[email protected]' // Required: assign to specific recipient
}

Text Block:

{
type: 'text',
page: '1',
x: 100,
y: 200,
text: 'Your text here',
width: 200, // Optional
height: 30, // Optional
templatedText: 'email' // Optional: 'email', 'fullname', or 'phone'
}

Image Block:

{
type: 'image',
page: '1',
x: 100,
y: 200,
src: 'data:image/png;base64,iVBORw0KGgo...', // Base64 encoded (max 256KB)
fileType: 'image/png',
width: 100, // Optional
height: 100 // Optional
}

Finally, send the envelope to all recipients:

const sendResponse = await fetch(`${API_BASE_URL}/public/envelope/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_KEY}:${SECRET_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
workspaceUuid: WORKSPACE_UUID,
envelopeUuid,
useUserAsSenderName: false, // Optional: use user name instead of company name
customInvitationMessage: "Please review and sign this document" // Optional: custom message
})
});
if (!sendResponse.ok) {
throw new Error(`Failed to send envelope: ${sendResponse.status}`);
}

Step 8: Track Envelope Events with Webhooks

Section titled “Step 8: Track Envelope Events with Webhooks”

After sending an envelope, you can track signing events and completion using webhooks. Webhooks allow you to receive real-time notifications when recipients sign documents or when envelopes are completed.

  1. Log into your Subnoto workspace at app.subnoto.com
  2. Navigate to Settings → Webhooks
  3. Create a webhook with your endpoint URL
  4. Configure the webhook to receive ENVELOPE_SIGNED and ENVELOPE_COMPLETED events

For detailed webhook configuration, see the Webhooks Setup Guide.

  • ENVELOPE_SIGNED: Triggered each time a recipient signs the document. Use this to track individual signing events.
  • ENVELOPE_COMPLETED: Triggered when the envelope is fully completed and all required signatures have been collected. Use this to know when the envelope process is complete.

When a recipient signs the document, you’ll receive an ENVELOPE_SIGNED event:

{
"eventUuid": "0f29a0a2-3a6f-44a4-b2a2-5d7b3f1b1f9b",
"eventType": "ENVELOPE_SIGNED",
"eventTime": 1704067200000,
"webhookUuid": "webhook-uuid",
"teamUuid": "team-uuid",
"data": {
"workspaceUuid": "workspace-uuid",
"envelopeUuid": "envelope-uuid",
"recipientEmail": "[email protected]",
"recipientName": "John Doe"
}
}

When the envelope is completed, you’ll receive an ENVELOPE_COMPLETED event:

{
"eventUuid": "f5a6c2d3-2e1b-4c7a-92ab-5e8f2d1c0b9a",
"eventType": "ENVELOPE_COMPLETED",
"eventTime": 1704067200000,
"webhookUuid": "webhook-uuid",
"teamUuid": "team-uuid",
"data": {
"workspaceUuid": "workspace-uuid",
"envelopeUuid": "envelope-uuid",
"documentUuid": "document-uuid",
"finalRevisionVersion": 1
}
}
  • Encryption: Documents must be encrypted using AES-256-GCM with the provided revisionEncryptionKey and include the documentUuid in the AAD (Additional Authenticated Data).
  • S3 Upload: The pre-signed S3 URL is valid for a limited time. Upload your document promptly after receiving it.
  • Envelope Status: Envelopes must be in draft status to add recipients, blocks, or make modifications. Once sent, the status changes and modifications are restricted.
  • Block Positioning: Block coordinates (x, y) are in PDF points (1/72 inch). Make sure to position blocks correctly on your pages.
  • Signature Blocks: Signature blocks always require a recipientEmail that must match one of the recipients added to the envelope.
  • Recipient Emails: When adding blocks with recipientEmail, the email must match one of the recipients added to the envelope.

The API may return various error codes. Common ones include:

  • WORKSPACE_NOT_FOUND: Invalid workspace UUID
  • ENVELOPE_NOT_FOUND: Invalid envelope UUID
  • ENVELOPE_NOT_IN_DRAFT: Envelope has already been sent
  • NO_RECIPIENTS: Attempting to send without recipients
  • DOCUMENT_NOT_FOUND: Invalid document UUID
  • INVALID_RECIPIENT_EMAIL: Recipient email doesn’t match any added recipients

Always check response status codes and handle errors appropriately in your application.