Product and API documentation

ESign Platform Documentation

Use this guide to deploy the platform, create accounts, operate the dashboard, and integrate with the API, SDK, webhooks, and signer flow.

Deployment

Local or server setup

  1. Install Node.js 20 or newer and pnpm 11.1.3 or newer.
  2. Copy .env.example to .env and replace every secret before production use.
  3. Start Postgres, Redis, MinIO or S3-compatible storage, and SMTP.
  4. Run pnpm install, pnpm db:generate, pnpm db:migrate:deploy, and pnpm db:seed when you need the demo admin account.
  5. Run pnpm build before release, then start API, worker, web, and signer with production process management.
cp .env.example .env
pnpm install
pnpm db:generate
pnpm db:migrate:deploy
pnpm db:seed
pnpm build
pnpm dev

Required services

Postgres stores tenants, users, templates, documents, submissions, signed document records, audit events, metadata, webhook deliveries, and idempotency keys. Redis powers queues. MinIO or S3 stores PDFs and signatures. SMTP sends signer email.

Production services

Run API, worker, web, and signer as separate processes or containers. Use real secrets, TLS, persistent object storage, managed Postgres backups, Redis persistence where needed, and monitored SMTP delivery.

Account Creation

  1. Open the web app and choose Create account.
  2. Enter the organisation name, URL slug, admin name, email, and password.
  3. Use the dashboard to create templates, documents, API keys, and webhook endpoints.
  4. Invite team members from Dashboard - Team. Members receive an email link to set up their account and join with the assigned role.
  5. Use the Observability dashboard to monitor queue health, webhook delivery rates, and system activity.
  6. For seeded local data, sign in with admin@dcarbon.local and password admin123.

Available Features and Usage

Templates

Create reusable PDF or HTML templates, define signer roles, add fillable fields, and render previews before documents are sent.

Visual Field Editor

Drag and drop signature, text, date, checkbox, initial, and attachment fields directly onto your PDF template with visual placement, resizing, and per-role colour coding.

Documents and Submissions

Create documents from templates, assign submitters, send signing links, track statuses, void submissions, and resend reminders.

Advanced Signing Order

Choose parallel, sequential, or custom group signing. In custom mode, assign signers to numbered groups - groups execute sequentially while signers within a group sign in parallel. Conditional fields show or hide based on other field values.

Signer Experience

Signers open a secure signing URL, review the document, complete required fields, draw a signature, sign, or decline with a reason.

Verification

Upload a signed PDF or provide a SHA-256 hash to verify whether a document is valid, superseded, invalidated, tampered, or unknown.

Team Management

Invite team members by email with role-based access (Owner, Admin, Member). Manage roles, revoke invitations, and remove members from the dashboard.

Audit Trail

View, filter, and download the complete audit trail for your tenant. Export as CSV or JSON, filter by submission, date range, or event type.

API Keys

Create one-time-visible API keys for server integrations, list existing keys, and revoke keys that should no longer have access.

Webhooks

Subscribe to document, submission, signing, metadata, and invalidation events. Failed deliveries can be retried by the worker.

Bulk Send & Batches

Send a template to hundreds of recipients in one operation. Upload a CSV or add recipients manually. Track batch progress, success/failure counts, and per-recipient errors from the dashboard.

Custom Branding

Set your organisation's brand name, accent colour, and logo. Customise email headers and footers with HTML. All signing request, reminder, and invitation emails use your branding automatically.

Observability Dashboard

Monitor production health with real-time stats for job queues (render, email, webhook, PDF), storage counts, mail delivery, webhook success rates, recent failures, and activity breakdowns.

Team Management & Role-Based Access

Owners and Admins can invite new team members from Dashboard → Team. Each invitation is sent via email and expires after 7 days.

Owner

Full access. Can invite any role, promote or demote anyone, and remove members including other owners.

Admin

Can invite Members and Admins, manage roles for non-owners, access observability, and manage all resources.

Member

Can create and manage templates, documents, and submissions. Cannot invite or manage team members.

API Endpoints

GET    /v1/team/members              List team members
GET    /v1/team/invitations           List pending invitations
POST   /v1/team/invitations           Invite a member (email, name, role)
POST   /v1/team/invitations/:token/accept  Accept invitation (name, password)
DELETE /v1/team/invitations/:id       Revoke a pending invitation
PATCH  /v1/team/members/:id/role      Change a member's role
DELETE /v1/team/members/:id           Remove a member

Visual Field Editor

Navigate to Templates → Edit Fields to open the drag-and-drop field editor. Click a field type in the toolbar, then click on the document canvas to place it.

Field Types

  • Signature – handwritten signature pad
  • Initial – initials field
  • Text – free text input
  • Date – date picker
  • Checkbox – boolean toggle
  • Attachment – file upload

Conditional Fields

Fields can be configured to appear only when another field has a specific value. Set the “Conditional On” property to reference another field and specify the trigger value.

Fields use normalised coordinates (0–1) for x, y, width, and height so placement scales to any PDF render size. Each field is colour-coded by its assigned signer role.

DCarbon Embedded Template Setup

Use this setup when creating each eSign template for the DCarbon embedded agreement flow.

Signer Role

Add exactly one signer role:

signer

Use lowercase signer, because the DCarbon webapp creates embed sessions with this role.

signer: {
  role: "signer"
}

Required Fields

After creating the template, open the template editor and add at least:

SIGNATURE
Role: signer
Required: true

The signature field is the critical one. Without it, the signer can complete the flow but the final document may not visibly show a signature where you expect.

Recommended Extra Fields

TEXT
Label: Full Name
Role: signer
Required: true
DATE
Label: Signed Date
Role: signer
Required: true
TEXT
Label: Address
Role: signer
Required: false or true

Variables To Add

The DCarbon webapp currently sends these variables into eSign:

signerName
signerEmail
signerAddress
agreementType
signedDate

For an HTML template, you can render those values directly:

<p>Name: {{signerName}}</p>
<p>Email: {{signerEmail}}</p>
<p>Address: {{signerAddress}}</p>
<p>Agreement Type: {{agreementType}}</p>
<p>Date: {{signedDate}}</p>

Use this variable schema in the template form:

{
  "signerName": { "type": "string" },
  "signerEmail": { "type": "string" },
  "signerAddress": { "type": "string" },
  "agreementType": { "type": "string" },
  "signedDate": { "type": "string" }
}

Best Practical Template Setup

  1. Create the template in the eSign dashboard.
  2. Add the role signer.
  3. Add the agreement content.
  4. Add variables like {{signerName}}, {{signerEmail}}, {{signerAddress}}, and {{signedDate}} when using an HTML template.
  5. Save the template.
  6. Open the template editor.
  7. Place a required SIGNATURE field near the signature line.
  8. Add DATE and TEXT fields if the signer should fill or confirm those values on the signing screen.
  9. Copy the template ID into the matching DCarbon environment variable.
ESIGN_TEMPLATE_ID_COMMERCIAL_OWNER=<Commercial Owner template id>
ESIGN_TEMPLATE_ID_COMMERCIAL_OPERATOR=<Commercial Operator template id>
ESIGN_TEMPLATE_ID_RESIDENTIAL=<Residential template id>
ESIGN_TEMPLATE_ID_PARTNER=<Partner template id>

Important Notes

If you create a PDF template, {{variables}} will not automatically render into the PDF body. For variable substitution inside the document body, use an HTML template. For PDF templates, rely on placed fields like SIGNATURE, TEXT, and DATE.

JSON variables are not signing fields. Variables are data injected into the document or session; fields are the actual places on the PDF where the signer signs, dates, checks, or types.

Because the DCarbon app already sends signer name, email, address, agreement type, and signed date, the signer does not need to type those again unless the PDF needs visible fillable text fields for them. The required field is usually SIGNATURE.

Advanced Signing Order

When creating a document, choose one of three signing order modes:

Parallel

All signers receive their invitation immediately and can sign in any order.

Sequential

Each signer is assigned to their own group. The next signer can only sign after the previous one completes.

Custom Groups

Assign signers to numbered groups. Groups execute in order (0 first, then 1, etc.). Within a group, signers can sign in parallel.

The signing order is enforced at sign time. If a signer in group 2 attempts to sign before all group 1 signers have completed, the API returns a 409 Conflict error.

Audit Trail Download

Navigate to Dashboard → Audit Trail to view, filter, and export the complete activity log for your tenant.

  • Filter by submission ID, date range (from/to)
  • Export as JSON or CSV for compliance and record-keeping
  • Events include: document creation, signing, verification, invalidation, team changes, and more
  • Each event records the actor, IP address, user agent, and timestamp
GET /v1/audit-trail/download?format=csv&from=2026-01-01&to=2026-06-01
GET /v1/audit-trail/download?format=json&submissionId=UUID

Production Observability

Admins and Owners can access Dashboard → Observability for real-time production health monitoring.

Job Queues

Monitor render, email, webhook, and PDF finalise queues with counts for waiting, active, delayed, completed, and failed jobs.

Storage

View counts for templates, documents, and signed documents in your tenant.

Mail Delivery

Track pending signers, viewed-in-24h counts, and current email queue depth.

Webhook Delivery

24h and 7d success rates, delivery counts, and recent failure details with HTTP status codes and retry counts.

API Endpoints

GET /v1/observability/queues      Queue stats (render, email, webhook, pdf)
GET /v1/observability/storage     Storage counts
GET /v1/observability/webhooks    Webhook delivery stats and failures
GET /v1/observability/mail        Mail queue and signer status
GET /v1/observability/activity    Event breakdown by type (7d)

API Integration

Create an API key from Dashboard - API Keys. Send requests with Authorization: Bearer YOUR_API_KEY. For retriable writes, include an Idempotency-Key header.

Create a client

import { ESignClient } from "@esign/sdk-node";

const client = new ESignClient({
  apiKey: process.env.ESIGN_API_KEY!,
  baseUrl: "http://localhost:3000",
});

Create a template

const template = await client.templates.create({
  name: "Mutual NDA",
  sourceType: "HTML",
  roles: [{ name: "Signer", displayOrder: 1 }],
  htmlSource: "<h1>{{companyName}} NDA</h1>",
  fields: [
    {
      role: "Signer",
      type: "SIGNATURE",
      page: 1,
      x: 0.12,
      y: 0.72,
      width: 0.32,
      height: 0.08
    }
  ]
});

Create a document

const document = await client.documents.create({
  templateId: template.id,
  name: "NDA for Example Co",
  metadata: { department: "Legal" },
  initialVersion: {
    variables: { companyName: "Example Co" },
    submitters: [
      { name: "Jane Doe", email: "jane@example.com", role: "Signer", signingGroup: 0 },
      { name: "John Smith", email: "john@example.com", role: "Approver", signingGroup: 1 }
    ],
    signingOrder: "parallel",
    signingOrderConfig: { mode: "CUSTOM" }
  }
});

Invite a team member

await fetch("http://localhost:3000/v1/team/invitations", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer YOUR_API_KEY"
  },
  body: JSON.stringify({
    email: "newmember@example.com",
    name: "New Member",
    role: "ADMIN"
  })
});

Verify a document hash

const response = await fetch("http://localhost:3000/v1/signed-documents/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ sha256: "SIGNED_DOCUMENT_SHA256" })
});

const result = await response.json();

Interactive API Reference (Swagger)

An interactive OpenAPI (Swagger) reference is published at {API_URL}/documentation — for example https://esign-api.dcarbon.solutions/documentation. It lists the integration endpoints (embedded signing sessions, templates, documents, submissions, signed-document download, and webhooks) with request and response schemas and live examples.

Access

  • The reference UI is protected by HTTP basic auth. Set DOCS_USER and DOCS_PASSWORD on the API service, and share those credentials with integration partners (e.g. the AWS team) who need to view the docs.
  • The API itself is authenticated with API keys, not the docs credentials. Create a key from Dashboard - API Keys and send it as Authorization: Bearer esk_live_...on every request.
  • To hide the reference entirely, set DOCS_ENABLED=false on the API service.

Typical embedded-signing flow: call POST /v1/embed/sessions to create a session, redirect the signer to the returned embedUrl, then poll GET /v1/embed/submissions/{id}/resultuntil signedDocument is returned with a downloadUrl.

Idempotency & Duplicate Prevention

Two complementary ways stop a retried or re-initiated signing from creating duplicate documents on POST /v1/embed/sessions (and POST /v1/documents). With neither, every call creates a new document.

1. Idempotency-Key header

Send the same Idempotency-Key header when safely retrying an identical create request. The original response is replayed instead of creating a new document, until the key expires — controlled by IDEMPOTENCY_TTL_HOURS on the API service (default 24h). The caller must resend the same key.

fetch("https://api.yourdomain.com/v1/embed/sessions", {
  method: "POST",
  headers: {
    "Authorization": "Bearer esk_live_...",
    "Content-Type": "application/json",
    "Idempotency-Key": "a-stable-uuid-you-generate"
  },
  body: JSON.stringify({ templateId, signer })
});

2. Intent reference (no key bookkeeping)

Pass a stable reference (e.g. a facilityId or orderId) on POST /v1/embed/sessions. Repeat calls with the same templateId + signer email + reference return the existing open submission (with a fresh embed link) instead of a duplicate. There is no TTL — validity is gated by signing status: once that submission is completed, voided, or declined, the next call starts a fresh document. The key is derived server-side, so the client only needs to send a consistent reference.

body: JSON.stringify({
  templateId,
  signer: { name, email },
  reference: "facility_abc123"  // same value on every retry for this intent
})

Embeddable Signing Widget

Embed the signing experience directly into your application. Users sign documents inline without leaving your page, and you get the signed document back via API.

1. Create Session

Call the API to create an embed session. You get back a token and embed URL.

2. Embed Widget

Load the widget using the JS SDK or an iframe. The user signs right on your page.

3. Get Result

Receive a callback when signing completes. Fetch the signed PDF via API.

Option A: One-step embed session

// Server-side: create document + get embed token in one call
const session = await fetch("http://localhost:3000/v1/embed/sessions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer YOUR_API_KEY"
  },
  body: JSON.stringify({
    templateId: "TEMPLATE_UUID",
    name: "Contract for Jane",
    variables: { companyName: "Acme Inc" },
    signer: { name: "Jane Doe", email: "jane@example.com", role: "signer" },
    metadata: { orderId: "ORD-123" }
  })
}).then(r => r.json());

// session = { token, embedUrl, documentId, submissionId, submitterId }

Option B: Get token for existing submitter

// If you already created a document and have a submitter ID:
const result = await fetch("http://localhost:3000/v1/embed/tokens", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer YOUR_API_KEY"
  },
  body: JSON.stringify({ submitterId: "SUBMITTER_UUID" })
}).then(r => r.json());

// result = { token, embedUrl, submitterId }

Client-side: JS SDK

<div id="signing-widget" style="height: 700px;"></div>
<script src="https://cdn.yourdomain.com/esign-embed.js"></script>
<script>
  const widget = ESign.createWidget({
    container: "#signing-widget",
    token: "TOKEN_FROM_SERVER",
    signerUrl: "http://localhost:3002",
    onReady: (data) => {
      console.log("Ready to sign:", data.documentName);
    },
    onSigned: async (data) => {
      console.log("Signed!", data.submissionId);
      // Fetch the signed PDF from your server
      // Your server calls: GET /v1/embed/submissions/:id/result
      widget.destroy();
      loadNextDocument(); // Move to next document in your flow
    },
    onDeclined: (data) => {
      console.log("Declined:", data.reason);
      widget.destroy();
    },
    onError: (data) => {
      console.error("Error:", data.message);
    }
  });
</script>

Client-side: Direct iframe (no SDK)

<iframe
  id="esign-frame"
  src="http://localhost:3002/embed/sign/TOKEN"
  style="width: 100%; height: 700px; border: none;"
></iframe>
<script>
  window.addEventListener("message", (event) => {
    if (event.data?.source !== "esign-embed") return;
    if (event.data.event === "esign:signed") {
      // Signing complete — fetch signed doc via your backend
      fetchSignedDocument(event.data.submissionId);
    }
  });
</script>

Retrieve the signed document

// Server-side: poll until the signed PDF is ready
const result = await fetch(
  "http://localhost:3000/v1/embed/submissions/SUBMISSION_ID/result",
  { headers: { "Authorization": "Bearer YOUR_API_KEY" } }
).then(r => r.json());

if (result.signedDocument) {
  // Download the signed PDF using the presigned URL
  const pdf = await fetch(result.signedDocument.downloadUrl);
  // Save to your own storage
}

postMessage Events

EventDataWhen
esign:readysubmitterId, documentNameWidget loaded and ready
esign:signedsubmitterId, submissionId, documentId, documentNameUser completed signing
esign:declinedsubmitterId, submissionId, reasonUser declined to sign
esign:errormessageAn error occurred

API Endpoints

POST /v1/embed/sessions              Create document + embed token in one call
POST /v1/embed/tokens                Get embed token for existing submitter
GET  /v1/embed/submissions/:id/result  Get submission result + signed PDF URL

Webhooks

Webhooks can be configured for document creation, version supersession, submission lifecycle events, submitter signing, signed document invalidation, and metadata changes.

{
  "id": "evt_DELIVERY_ID",
  "type": "submission.completed",
  "createdAt": "2026-06-03T00:00:00.000Z",
  "data": {
    "tenantId": "TENANT_ID",
    "submissionId": "SUBMISSION_ID"
  }
}

Verify webhook signatures with the SDK helper before trusting payloads. Failed deliveries are queued and retried by the worker.

Production Checklist

  • Replace JWT_SECRET and API_KEY_PEPPER with high-entropy values.
  • Set NODE_ENV=production and enable secure cookies behind HTTPS.
  • Run pnpm db:migrate:deploy during release, not db push.
  • Run the worker process for render, PDF finalise, email, and webhook queues.
  • Use durable S3 storage and configure bucket lifecycle policies.
  • Monitor API errors, queue failures, webhook retries, SMTP failures, and storage errors.
  • Back up Postgres and test restore procedures.
  • Rotate API keys and webhook secrets regularly.

Production Deployment (Easypanel on Netcup)

This guide walks through deploying the full ESign platform on a Netcup root server using Easypanel as the control panel. Easypanel handles Docker builds, SSL certificates, reverse proxying, and service networking automatically.

Minimum requirements

Netcup RS 1000 or higher — at least 2 GB RAM (4 GB recommended), 40 GB disk, Ubuntu 22.04+

Step 1 — Install Easypanel

SSH into your Netcup server and run the one-line installer:

curl -sSL https://get.easypanel.io | sh

Once installed, open http://YOUR_SERVER_IP:3000 in your browser to create your admin account.

Step 2 — DNS setup

Create three A records pointing to your Netcup server IP:

RecordTypeValue
api.yourdomain.comAYOUR_SERVER_IP
app.yourdomain.comAYOUR_SERVER_IP
sign.yourdomain.comAYOUR_SERVER_IP

Step 3 — Connect GitHub and create project

  1. In Easypanel, go to Settings → GitHub and connect your account
  2. Click New Project, name it esign
  3. You will add 7 services inside this project (detailed below)

Step 4 — Add infrastructure services

PostgreSQL

  1. + New Service → Postgres
  2. Name: postgres
  3. Version: 16
  4. Database: esign
  5. Set a strong password (save it)

Redis

  1. + New Service → Redis
  2. Name: redis
  3. Version: 7
  4. Set a password (save it)

MinIO (S3 Storage)

  1. + New Service → App
  2. Image: minio/minio:latest
  3. Command: server /data --console-address :9001
  4. Set MINIO_ROOT_USER and MINIO_ROOT_PASSWORD
  5. Mount volume at /data
  6. After startup, create bucket esign-documents
  7. If you expose MinIO for presigned document links, set the public endpoint as S3_PUBLIC_ENDPOINT on the API service.

Step 5 — Generate secrets

Run these locally and save the output — you will need them for every app service:

openssl rand -hex 32   # → JWT_SECRET
openssl rand -hex 32   # → API_KEY_PEPPER
openssl rand -base64 24 # → POSTGRES_PASSWORD (if not already set)
openssl rand -base64 24 # → REDIS_PASSWORD (if not already set)

Step 6 — Add application services

Each app service uses GitHub as source, with a Dockerfile from the docker/ folder. Services communicate internally via Easypanel’s Docker network using hostnames like esign-postgres, esign-redis, esign-minio.

ServiceDockerfileDomainPort
apidocker/Dockerfile.apiapi.yourdomain.com3000
workerdocker/Dockerfile.workernone (background jobs)
webdocker/Dockerfile.webapp.yourdomain.com3001
signerdocker/Dockerfile.signersign.yourdomain.com3002

Enable HTTPS on every domain — Easypanel provisions SSL certificates via Let’s Encrypt automatically.

Environment variables for API and Worker

Add these to both the api and worker services:

NODE_ENV=production
DATABASE_URL=postgresql://postgres:YOUR_PG_PASSWORD@esign-postgres:5432/esign
REDIS_URL=redis://:YOUR_REDIS_PASSWORD@esign-redis:6379
S3_ENDPOINT=http://esign-minio:9000
S3_PUBLIC_ENDPOINT=https://s3.yourdomain.com
S3_ACCESS_KEY=your_minio_user
S3_SECRET_KEY=your_minio_password
S3_BUCKET=esign-documents
JWT_SECRET=your_jwt_secret
JWT_ISSUER=esign-platform
API_KEY_PEPPER=your_api_key_pepper
API_URL=https://api.yourdomain.com
WEB_URL=https://app.yourdomain.com
SIGNER_URL=https://sign.yourdomain.com
SMTP_HOST=smtp.resend.com
SMTP_PORT=587
SMTP_SECURE=true
SMTP_USER=resend
SMTP_PASS=re_your_api_key
SMTP_FROM=noreply@yourdomain.com
LOG_LEVEL=warn

Environment variables for Web and Signer

Add NEXT_PUBLIC_API_URL=https://api.yourdomain.com as both an environment variable and a build argument (Next.js bakes public env vars into the client bundle at build time).

NODE_ENV=production
NEXT_PUBLIC_API_URL=https://api.yourdomain.com

Step 7 — Run database migrations

After the API service deploys, open its Terminal in Easypanel and run:

npx prisma migrate deploy --schema=packages/db/prisma/schema.prisma

Or sign up at https://app.yourdomain.com/signup to create the first admin account.

Step 8 — Set up email

The platform sends emails when documents need signing. Use a real SMTP provider:

ProviderFree tierSMTP host
Resend3,000 emails/mosmtp.resend.com
Postmark100 emails/mosmtp.postmarkapp.com
Brevo300 emails/daysmtp-relay.brevo.com

Final architecture

Easypanel Project: esign
├── postgres    (Postgres 16, internal :5432)
├── redis       (Redis 7, internal :6379)
├── minio       (MinIO, internal :9000)
├── api         (Fastify → api.yourdomain.com)
├── worker      (BullMQ background jobs, no public domain)
├── web         (Next.js → app.yourdomain.com)
└── signer      (Next.js → sign.yourdomain.com)

All services communicate via Easypanel's internal Docker
network. Only api, web, and signer are publicly exposed
with automatic HTTPS via Let's Encrypt.

Troubleshooting

IssueFix
API returns 500Check logs — usually a missing env var. Verify DATABASE_URL and REDIS_URL
Web shows “fetch failed”NEXT_PUBLIC_API_URL must match the API’s public domain exactly (include https://)
Emails not sendingCheck worker logs. Verify SMTP credentials. Test with Resend’s free tier first
MinIO bucket errorOpen MinIO console (port 9001) and create the esign-documents bucket manually
Build failsDockerfile path must be docker/Dockerfile.api (not ./Dockerfile)
CORS errorsAPI allows WEB_URL and SIGNER_URL origins — they must match your domains exactly

Maintenance

  • View logs: Click any service → Logs tab in Easypanel
  • Redeploy: Push to master branch → click Rebuild (or enable auto-deploy)
  • Database backup: Postgres service → Backups tab → set up scheduled backups
  • Update env vars: Service → Environment tab → update → Redeploy

Coming Soon

  • OAuth or SSO login options for enterprise tenants.
  • Document expiry and auto-reminder scheduling.
  • Multi-language support for signer portal and emails.

Local Test Data

Admin email: admin@dcarbon.local

Admin password: admin123

Tenant slug: dcarbon

Local API: http://localhost:3000

Local web: http://localhost:3001

Local signer: http://localhost:3002