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:
- Create an envelope
- Encrypt your document
- Upload the encrypted document to S3
- Confirm the document upload
- Add recipients
- Add blocks (signatures, text, images)
- Send the envelope
Prerequisites
Section titled “Prerequisites”- 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:
npm install form-dataStep 1: Create an Envelope
Section titled “Step 1: Create an Envelope”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 envelopeconst 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 envelopedocumentUuid: The unique identifier for the documentpresignedS3Params: Pre-signed S3 URL and fields for uploading the encrypted documentrevisionEncryptionKey: Base64-encoded encryption key for encrypting your document
Step 2: Encrypt the Document
Section titled “Step 2: Encrypt the 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 fileconst pdfBuffer = readFileSync("path/to/your/document.pdf");
// Decode the base64 encryption keyconst 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 cipherconst cipher = createCipheriv("aes-256-gcm", keyBuffer, iv);
// Set the Additional Authenticated Data (AAD) with the document UUIDconst contextJSON = JSON.stringify({ documentUuid });cipher.setAAD(Buffer.from(contextJSON));
// Encrypt the documentconst encrypted = Buffer.concat([cipher.update(pdfBuffer), cipher.final()]);
// Get the authentication tagconst authTag = cipher.getAuthTag();
// Combine IV, encrypted data, and auth tagconst encryptedDocument = Buffer.concat([iv, encrypted, authTag]);Step 3: Upload to S3
Section titled “Step 3: Upload to S3”Upload the encrypted document using the pre-signed S3 parameters:
import FormData from "form-data";
// Create FormData for multipart uploadconst formData = new FormData();
// Add all required S3 fieldsObject.entries(presignedS3Params.fields).forEach(([key, value]) => { formData.append(key, value);});
// Add the encrypted fileformData.append("file", encryptedDocument, { filename: "document.pdf", contentType: "application/octet-stream"});
// Upload to S3const uploadResponse = await fetch(presignedS3Params.url, { method: "POST", body: formData});
if (!uploadResponse.ok) { throw new Error(`S3 upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);}Step 4: Confirm Document Upload
Section titled “Step 4: Confirm Document Upload”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}`);}Step 5: Add Recipients
Section titled “Step 5: Add Recipients”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, lastnametype: 'contact'- Existing contact usingcontactUuidtype: 'member'- Workspace member usinguserUuid
Step 6: Add Blocks
Section titled “Step 6: Add Blocks”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, }, { type: "text", page: "1", x: 100, y: 250, text: "Please sign here", width: 200, height: 30 } ] })});
const blocksResult = await addBlocksResponse.json();Block Types
Section titled “Block Types”Signature Block:
{ type: 'signature', page: '1', // Page number as string x: 100, // X position in points y: 200, // Y position in points}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}Step 7: Send the Envelope
Section titled “Step 7: Send the Envelope”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.
Setting Up Webhooks
Section titled “Setting Up Webhooks”- Log into your Subnoto workspace at app.subnoto.com
- Navigate to Settings → Webhooks
- Create a webhook with your endpoint URL
- Configure the webhook to receive
ENVELOPE_SIGNEDandENVELOPE_COMPLETEDevents
For detailed webhook configuration, see the Webhooks Setup Guide.
Available Events
Section titled “Available Events”- 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.
Webhook Payload Example
Section titled “Webhook Payload Example”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", "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 }}Important Notes
Section titled “Important Notes”- Encryption: Documents must be encrypted using AES-256-GCM with the provided
revisionEncryptionKeyand include thedocumentUuidin 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
draftstatus 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
recipientEmailthat 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.
Error Handling
Section titled “Error Handling”The API may return various error codes. Common ones include:
WORKSPACE_NOT_FOUND: Invalid workspace UUIDENVELOPE_NOT_FOUND: Invalid envelope UUIDENVELOPE_NOT_IN_DRAFT: Envelope has already been sentNO_RECIPIENTS: Attempting to send without recipientsDOCUMENT_NOT_FOUND: Invalid document UUIDINVALID_RECIPIENT_EMAIL: Recipient email doesn’t match any added recipients
Always check response status codes and handle errors appropriately in your application.