NestJS E-Signature Integration: Injectable Services, Guards & Webhooks (2026)
Architect a production-grade electronic signature module in NestJS 11 using dependency injection, rawBody buffer preservation, a reusable HMAC-SHA256 CanActivate Webhook Guard, and idempotent Prisma + BullMQ event workers.
Quick Answer (TL;DR)
NestJS 11 Enterprise Pattern • DI Service + CanActivate Guard
To integrate legally binding e-signatures into NestJS 11, enable { rawBody: true } inside NestFactory.create() in src/main.ts. Create an @Injectable() SignbeeService that injects ConfigService and dispatches CommonMark markdown or base64 PDF agreements to POST https://signb.ee/api/v1/send. Protect your webhook controller route with a custom @Injectable() SignbeeWebhookGuard implements CanActivate that verifies the X-Signbee-Signature header against req.rawBody using crypto.timingSafeEqual before persisting document.signed events in Prisma and BullMQ.
1. Why Enterprise NestJS Backends Are Replacing Legacy E-Sign SDKs
In enterprise Node.js engineering—spanning fintech lending pipelines, prop-tech lease automation, healthcare credentialing, and B2B procurement microservices—NestJS is the undisputed architectural standard. Its modular Dependency Injection (DI) container, declarative Guards, Interceptors, Exception Filters, and first-class integration with Prisma, TypeORM, and BullMQ allow backend teams to scale complex domain logic across dozens of microservices without descending into spaghetti code.
However, integrating legacy electronic signature providers into a clean NestJS codebase often feels like bolting a 2014 library onto a 2026 architecture. Legacy Node SDKs rely on mutable singleton clients, untyped callback patterns, and stateful RSA JWT token exchange ceremonies that fail silently when horizontal autoscaling spins up multiple Kubernetes replicas. Worse still, as documented in our DocuSign API Pricing Explained analysis, enterprise teams are routinely locked into five-figure annual seat and envelope contracts before writing a single line of production controller code.
When evaluating the best e-signature APIs for developers in 2026, backend architects increasingly favor stateless, zero-SDK REST primitives. Signbee is designed from the ground up for modern TypeScript microservices: authentication uses a stateless Bearer API key, agreements can be authored in dynamic CommonMark markdown or base64 PDFs without brittle x/y coordinate tags, and every completed contract includes an immutable A4 cryptographic certificate with a verifiable SHA-256 digest.
2. NestJS E-Signature Architecture Comparison
Before designing our SignbeeModule, let's compare how Signbee's stateless REST architecture stacks up against legacy enterprise SDKs and self-hosted containers (covered in depth in our open-source e-signature API comparison) inside a containerized NestJS microservice environment:
| Microservice Criterion | Signbee + NestJS DI Module | DocuSign / Adobe Node SDK | Self-Hosted Open-Source Docker |
|---|---|---|---|
| Authentication State | Stateless Bearer Token (Pod-Safe) | Stateful OAuth JWT Grant + Redis Lock | Custom API Key + Internal VPC Auth |
| Docker Image Overhead | 0 MB (Uses Node 22 Native Fetch) | +48 MB node_modules Bloat | +1.4 GB Separate App + DB Containers |
| Contract Generation | Markdown String or Base64 PDF | PDF Upload + Coordinate Tab Arrays | PDF Upload + Manual Field Mapping |
| Webhook Security | HMAC-SHA256 (X-Signbee-Signature) | DocuSign Connect HMAC / XML | Custom Header Secret |
| Production Pricing | Free Tier + $20/mo Unlimited | $3,000–$15,000+/yr Enterprise | $150+/mo AWS RDS + ECS Compute |
3. Enabling `rawBody` in `main.ts` & Typed `ConfigModule` Setup
The single most common bug when building webhook listeners in NestJS is losing access to the unparsed HTTP request body. By default, NestJS's JSON body parser middleware parses incoming application/json streams into a JavaScript object on req.body and discards the original buffer. If you attempt to verify an HMAC-SHA256 webhook signature against JSON.stringify(req.body), any minor formatting difference will cause signature validation to fail.
NestJS solves this cleanly with the built-in rawBody: true bootstrap option, which works across both @nestjs/platform-express and @nestjs/platform-fastify. Let's configure src/main.ts alongside a strongly typed signbee.config.ts namespace using @nestjs/config:
// src/main.ts
import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import type { NestExpressApplication } from "@nestjs/platform-express";
import { AppModule } from "./app.module";
async function bootstrap() {
// CRITICAL: Enable rawBody: true so req.rawBody Buffer is populated for HMAC guards
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
rawBody: true,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
// src/signbee/signbee.config.ts
import { registerAs } from "@nestjs/config";
export default registerAs("signbee", () => ({
apiKey: process.env.SIGNBEE_API_KEY || "",
webhookSecret: process.env.SIGNBEE_WEBHOOK_SECRET || "",
baseUrl: process.env.SIGNBEE_BASE_URL || "https://signb.ee/api/v1",
senderName: process.env.SIGNBEE_SENDER_NAME || "Apex Fintech Legal",
senderEmail: process.env.SIGNBEE_SENDER_EMAIL || "agreements@apexfintech.io",
webhookCallbackUrl:
process.env.SIGNBEE_WEBHOOK_URL ||
"https://api.apexfintech.io/v1/webhooks/signbee",
}));4. Building the `@Injectable() SignbeeService` Provider
Now let's implement src/signbee/signbee.service.ts. Following NestJS domain-driven design principles, SignbeeService encapsulates all outbound HTTP communication, timeout circuit breaking (AbortSignal.timeout(8000)), structured logging, and error mapping into standard NestJS HTTP exceptions.
Notice how the service exposes two clean methods: dispatchMarkdownContract() for dynamically generated CommonMark agreements (such as loan disclosures, vendor MSAs, or commercial leases) and getDocumentStatus() for real-time audit trail inspection. Teams looking for an affordable e-signature API integration can also pass pdf_base64 into the exact same endpoint whenever third-party PDF files are required:
// src/signbee/signbee.service.ts
import {
Injectable,
Inject,
Logger,
BadGatewayException,
InternalServerErrorException,
} from "@nestjs/common";
import type { ConfigType } from "@nestjs/config";
import signbeeConfig from "./signbee.config";
export interface DispatchContractInput {
title: string;
markdown?: string;
pdfBase64?: string;
recipientName: string;
recipientEmail: string;
expiresInDays?: number;
}
export interface SignbeeDispatchResult {
id: string;
status: "sent" | "pending_verification";
title: string;
verify_url: string;
created_at: string;
}
@Injectable()
export class SignbeeService {
private readonly logger = new Logger(SignbeeService.name);
constructor(
@Inject(signbeeConfig.KEY)
private readonly config: ConfigType<typeof signbeeConfig>
) {
if (!this.config.apiKey) {
this.logger.warn("SIGNBEE_API_KEY is not configured in environment");
}
}
async dispatchContract(
dto: DispatchContractInput
): Promise<SignbeeDispatchResult> {
if (!this.config.apiKey) {
throw new InternalServerErrorException(
"Signbee API key is missing from server configuration"
);
}
const response = await fetch(`${this.config.baseUrl}/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.config.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: dto.title,
markdown: dto.markdown,
pdf_base64: dto.pdfBase64,
sender_name: this.config.senderName,
sender_email: this.config.senderEmail,
recipient_name: dto.recipientName,
recipient_email: dto.recipientEmail,
expires_in_days: dto.expiresInDays ?? 14,
webhook_url: this.config.webhookCallbackUrl,
}),
signal: AbortSignal.timeout(8000),
});
if (!response.ok) {
const errorPayload = await response.json().catch(() => ({}));
this.logger.error(
`Signbee dispatch failed (${response.status}): ${JSON.stringify(errorPayload)}`
);
throw new BadGatewayException(
errorPayload.error || "Upstream e-signature dispatch failed"
);
}
const result = (await response.json()) as SignbeeDispatchResult;
this.logger.log(
`Dispatched contract ${result.id} to ${dto.recipientEmail}`
);
return result;
}
async getDocumentStatus(documentId: string) {
const response = await fetch(
`${this.config.baseUrl}/documents/${encodeURIComponent(documentId)}`,
{
headers: {
Authorization: `Bearer ${this.config.apiKey}`,
},
signal: AbortSignal.timeout(5000),
}
);
if (!response.ok) {
throw new BadGatewayException(
`Unable to fetch status for Signbee document ${documentId}`
);
}
return response.json();
}
}5. Writing a Reusable `SignbeeWebhookGuard` (`CanActivate`)
In NestJS, authentication and cryptographic request validation belong inside a Guard rather than cluttering your controller methods. By implementing CanActivate in SignbeeWebhookGuard, any controller endpoint decorated with @UseGuards(SignbeeWebhookGuard) is guaranteed to execute only after the X-Signbee-Signature header has been verified against req.rawBody using constant-time buffer comparison (crypto.timingSafeEqual):
// src/signbee/signbee-webhook.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
Inject,
UnauthorizedException,
} from "@nestjs/common";
import type { RawBodyRequest } from "@nestjs/common";
import type { Request } from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
import type { ConfigType } from "@nestjs/config";
import signbeeConfig from "./signbee.config";
@Injectable()
export class SignbeeWebhookGuard implements CanActivate {
constructor(
@Inject(signbeeConfig.KEY)
private readonly config: ConfigType<typeof signbeeConfig>
) {}
canActivate(context: ExecutionContext): boolean {
const request = context
.switchToHttp()
.getRequest<RawBodyRequest<Request>>();
const signatureHeader = request.headers["x-signbee-signature"];
if (!signatureHeader || typeof signatureHeader !== "string") {
throw new UnauthorizedException("Missing X-Signbee-Signature header");
}
if (!request.rawBody || !Buffer.isBuffer(request.rawBody)) {
throw new UnauthorizedException(
"Raw request body buffer unavailable; ensure rawBody: true in NestFactory.create"
);
}
const expectedHex = createHmac("sha256", this.config.webhookSecret)
.update(request.rawBody)
.digest("hex");
const cleanProvidedHex = signatureHeader.replace(/^sha256=/, "");
const providedBuf = Buffer.from(cleanProvidedHex, "hex");
const expectedBuf = Buffer.from(expectedHex, "hex");
if (
providedBuf.length !== expectedBuf.length ||
!timingSafeEqual(providedBuf, expectedBuf)
) {
throw new UnauthorizedException("Invalid Signbee webhook HMAC signature");
}
return true;
}
}6. Webhook Controller + Idempotent Prisma & BullMQ Queue Processing
Once SignbeeWebhookGuard validates the request, our SignbeeWebhookController handles the lifecycle transition. In high-throughput enterprise backends, your HTTP webhook controller should never block on slow third-party side effects (like generating invoices in NetSuite, archiving PDFs to S3, or sending customer onboarding emails).
Instead, perform a fast, idempotent Prisma status update and push a deduplicated background job to BullMQ keyed by jobId: `signbee-${payload.document_id}-${payload.event}`:
// src/signbee/signbee-webhook.controller.ts
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
UseGuards,
} from "@nestjs/common";
import { InjectQueue } from "@nestjs/bullmq";
import { Queue } from "bullmq";
import { SignbeeWebhookGuard } from "./signbee-webhook.guard";
import { PrismaService } from "../prisma/prisma.service";
interface SignbeeWebhookPayload {
event: "document.sent" | "document.viewed" | "document.signed" | "document.expired";
document_id: string;
title: string;
recipient_email: string;
signed_at?: string;
sha256_hash?: string;
signed_pdf_url?: string;
verify_url: string;
}
@Controller("v1/webhooks/signbee")
export class SignbeeWebhookController {
constructor(
private readonly prisma: PrismaService,
@InjectQueue("contract-fulfillment") private readonly fulfillmentQueue: Queue
) {}
@Post()
@HttpCode(HttpStatus.OK)
@UseGuards(SignbeeWebhookGuard)
async handleWebhook(@Body() payload: SignbeeWebhookPayload) {
if (payload.event === "document.signed") {
// Atomic idempotent conditional update: only transition if not already SIGNED
const updated = await this.prisma.contractRecord.updateMany({
where: {
signbeeDocumentId: payload.document_id,
status: { not: "SIGNED" },
},
data: {
status: "SIGNED",
sha256CertificateHash: payload.sha256_hash,
signedPdfUrl: payload.signed_pdf_url,
verifyUrl: payload.verify_url,
signedAt: payload.signed_at ? new Date(payload.signed_at) : new Date(),
},
});
// Only enqueue downstream fulfillment once even if webhook is delivered twice
if (updated.count > 0) {
await this.fulfillmentQueue.add(
"provision-signed-tenant",
{
documentId: payload.document_id,
recipientEmail: payload.recipient_email,
sha256Hash: payload.sha256_hash,
},
{
jobId: `signbee-${payload.document_id}-${payload.event}`,
attempts: 5,
backoff: { type: "exponential", delay: 2000 },
}
);
}
}
return { received: true, documentId: payload.document_id };
}
}7. NestJS Building Blocks for Enterprise Contract Automation
Structuring your e-signature integration around native NestJS lifecycle primitives ensures every layer of your application remains testable, observable, and decoupled. If you are building a multi-tenant platform that dispatches contracts on behalf of multiple organizations, combine this module structure with our white-label e-signature API patterns. Here is the responsibility matrix across your SignbeeModule components:
| NestJS Component | Decorator / Interface | Responsibility in Signbee Pipeline | Failure Mode Handled |
|---|---|---|---|
| signbee.config.ts | registerAs('signbee') | Injects API keys, sender identity & webhook secrets | Prevents hardcoded secrets & env drift |
| SignbeeService | @Injectable() | Dispatches Markdown/PDF contracts via POST /api/v1/send | 8s AbortSignal timeout + BadGatewayException |
| SignbeeWebhookGuard | CanActivate | Verifies HMAC-SHA256 against req.rawBody Buffer | Blocks forged payloads & timing attacks (401) |
| SignbeeWebhookController | @Controller() + BullMQ | Atomic Prisma update + async job enqueueing | Eliminates duplicate retries & 504 timeouts |
8. Testing & Scaling Your NestJS E-Signature Microservice
Because SignbeeService and SignbeeWebhookGuard rely strictly on constructor-injected configuration and Node's built-in node:crypto module, writing fast unit and integration tests with Jest or Vitest requires zero external network calls. In your test suite, create a TestingModule providing a mock signbeeConfig secret, generate a deterministic HMAC-SHA256 signature over a sample JSON Buffer, and verify that SignbeeWebhookGuard.canActivate() accepts authentic payloads while rejecting tampered buffers.
When deploying to staging and production, you can provision keys immediately using our free e-signature API tier (3 free legal documents per month with complete webhook and SHA-256 certificate support). For high-volume microservice fleets, review our 2026 e-signature API pricing comparison to see how Signbee's flat $20/month unlimited Pro plan eliminates per-envelope billing surprises across every environment.
9. Frequently Asked Questions: NestJS E-Signature Microservices
How do you structure an e-signature module in a NestJS 11 enterprise application?
In NestJS 11, encapsulate all electronic signature capabilities inside a dedicated SignbeeModule that imports ConfigModule.forFeature(signbeeConfig). Export an @Injectable() SignbeeService that injects typed configuration via @Inject(signbeeConfig.KEY) and wraps native Node 22 fetch calls to POST https://signb.ee/api/v1/send and GET https://signb.ee/api/v1/documents/:id. Pair the service with a reusable @Injectable() SignbeeWebhookGuard implementing CanActivate so any controller route decorated with @UseGuards(SignbeeWebhookGuard) automatically verifies cryptographic HMAC-SHA256 webhook signatures.
How do you access the unparsed raw request body in NestJS for Signbee webhook verification?
When bootstrapping your NestJS application in src/main.ts, pass { rawBody: true } as the second argument to NestFactory.create<NestExpressApplication>(AppModule, { rawBody: true }) (or NestFastifyApplication). This instructs NestJS's underlying Express or Fastify adapter to preserve the exact incoming byte stream on req.rawBody as a Node.js Buffer alongside the parsed req.body JSON object. Inside your SignbeeWebhookGuard, compute the HMAC-SHA256 digest directly against req.rawBody so key reordering or whitespace stripping never invalidates X-Signbee-Signature.
Why should NestJS webhook controllers offload document.signed events to BullMQ?
Webhook providers enforce strict delivery timeouts (typically 5 to 10 seconds) and expect an immediate 200 OK acknowledgment. If your NestJS webhook controller synchronously downloads the signed PDF, uploads it to AWS S3, updates five PostgreSQL tables, and triggers downstream ERP or CRM provisioning inside the HTTP request thread, transient latency spikes will cause webhook timeouts and duplicate retries. By verifying the HMAC signature in SignbeeWebhookGuard and immediately enqueuing the event into a Redis-backed BullMQ queue, your controller responds in under 8ms while worker processes handle heavy persistence with automatic exponential backoff.
How do you prevent duplicate webhook processing in NestJS with Prisma or TypeORM?
To guarantee idempotent webhook handling in NestJS, store a unique deterministic event key or use the Signbee document_id paired with the target lifecycle state (for example, status: 'SIGNED') inside an atomic database transaction. In Prisma, use an updateMany where status !== 'SIGNED' or an upsert keyed on a unique webhook_deliveries table before queueing side effects. In BullMQ, pass jobId: `signbee-${payload.document_id}-${payload.event}` when calling queue.add() so Redis automatically deduplicates concurrent deliveries of the same contract completion event.
Why is native fetch preferred over legacy e-signature SDKs in NestJS microservices?
Modern NestJS 11 microservices running on Node.js 22 LTS include a high-performance, Undici-powered global fetch implementation out of the box. Legacy SDKs like docusign-esign introduce over 45 MB of unmaintained CommonJS dependencies, lack strict TypeScript null-safety, and require stateful OAuth token caching across Kubernetes pods. Calling Signbee's REST API via a typed @Injectable() SignbeeService keeps your Docker container image small, supports native AbortSignal.timeout(8000) circuit breaking, and integrates cleanly with OpenTelemetry distributed tracing.
How do you unit test SignbeeService and SignbeeWebhookGuard in NestJS with Jest or Vitest?
Because SignbeeModule follows standard NestJS dependency injection patterns, you can spin up an isolated TestingModule using Test.createTestingModule(). To test SignbeeWebhookGuard, construct a mock ExecutionContext whose switchToHttp().getRequest() returns a sample rawBody Buffer and an X-Signbee-Signature header computed with a test secret, asserting that guard.canActivate(ctx) returns true for valid digests and throws UnauthorizedException when a single byte of rawBody is mutated.
Deploy Signbee in Your NestJS Microservice Today
Generate your stateless Bearer API key in 30 seconds and start sending cryptographically certified contracts from your NestJS backend.