Inngest + Signbee: Event-Driven Contract Signing & waitForEvent in Next.js (2026)
Serverless functions time out in seconds, yet enterprise contracts take days to sign. Learn how to build zero-polling, durable contract orchestration in Next.js using Inngest's step.run, step.waitForEvent, and Signbee's cryptographic document.signed webhooks.
Quick Answer (TL;DR)
Durable Serverless E-Signature Architecture • Next.js App Router Blueprint
To pause a Next.js serverless workflow until a user signs a contract without burning compute or writing database polling crons, wrap Signbee's POST https://signb.ee/api/v1/send inside Inngest's step.run("dispatch-contract", ...), then immediately call step.waitForEvent("wait-for-signature", { event: "signbee/document.signed", timeout: "7d" }). Inngest checkpoints state and terminates the Lambda invocation at zero cost. When the recipient completes Signbee's signing ceremony, your Next.js webhook route verifies X-Signbee-Signature and fires inngest.send(), waking the exact workflow instance to provision tenant infrastructure and archive the SHA-256 certified PDF.
1. The Impedance Mismatch: Ephemeral Serverless Functions vs. Human Signing Velocity
Modern SaaS applications built on the Next.js App Router execute inside ephemeral serverless environments such as Vercel Functions, AWS Lambda, or Cloudflare Workers. These runtimes are engineered for high-concurrency, short-lived HTTP lifecycles ranging from 10 seconds to a hard ceiling of 300 or 800 seconds. Conversely, legally binding commercial agreements operate on human wall-clock time. Whether you are onboarding a B2B enterprise tenant under a Master Services Agreement (MSA), issuing an independent contractor Statement of Work (SOW), or collecting a vendor Data Processing Addendum (DPA), the counterparty may sign in four minutes on their phone or four days later after legal review.
Historically, engineering teams bridged this temporal divide by scattering onboarding logic across disconnected state machines. One API route generated the agreement and updated a PostgreSQL onboarding_state column to AWAITING_SIGNATURE; a background cron job ran every fifteen minutes to check for expired envelopes; a separate webhook endpoint mutated database rows upon signature completion; and yet another queue worker provisioned Stripe subscriptions and tenant workspaces. Whenever a step failed mid-flight, engineers had to manually reconstruct state across four log streams.
When evaluating the best e-signature APIs for developers in 2026, teams increasingly prioritize how cleanly an API composes with durable execution engines like Inngest. By pairing Signbee's synchronous Markdown-to-PDF dispatch endpoint (POST /api/v1/send) with Inngest's step.waitForEvent primitive, an entire multi-day contract lifecycle—generation, dispatch, human signature wait, reminder escalation, SHA-256 verification, and tenant provisioning—lives inside a single, sequential TypeScript function.
“Replacing four fragmented database state-polling crons and webhook Glue handlers with a single 60-line Inngest function cut our customer onboarding failure rate to zero while eliminating $1,400/month in legacy envelope licensing.”— Principal Systems Architect, Series B FinTech Platform
2. Architecture Breakdown: How Inngest + Signbee Orchestrate Multi-Day Workflows
To understand why Inngest and Signbee form a high-reliability stack, examine how control flows between your Next.js App Router application, the Inngest durable execution engine, and Signbee's cryptographic signing infrastructure:
Trigger Event Ingestion (onboarding/deal.approved)
When a customer upgrades to an enterprise tier or a sales representative marks an opportunity as closed-won, your application emits inngest.send({ name: "onboarding/deal.approved", data }). Inngest durably persists the event payload and invokes your Next.js /api/inngest route.
Memoized Contract Dispatch (step.run("dispatch-contract"))
Inside the first durable step, your server compiles the dynamic CommonMark contract markdown (injecting customer legal name, seat count, pricing table, and effective date) and calls POST https://signb.ee/api/v1/send. Signbee renders an A4 archival PDF, binds Party A (your organization), emails Party B a zero-login signing link, and returns { id: "doc_...", status: "sent", signing_url }. Inngest stores this return value in its step state store.
Zero-Cost Hibernation (step.waitForEvent("wait-for-signature"))
The workflow registers an event listener correlated on async.data.document_id == dispatch.id with a 7d timeout. The Next.js function exits immediately. Zero Vercel fluid compute or Lambda GB-seconds are billed while the contract sits in the customer's inbox.
Cryptographic Webhook Bridge & Deterministic Resumption
When the customer signs, Signbee seals the PDF with a SHA-256 certificate and sends a document.signed webhook to /api/webhooks/signbee. Your webhook handler validates X-Signbee-Signature and fires inngest.send({ name: "signbee/document.signed", data }). Inngest immediately wakes the paused workflow at the exact line after waitForEvent.
Architectural Comparison: Cron Polling vs. Raw Webhooks vs. Inngest + Signbee
Before writing code, compare the operational characteristics of traditional polling state machines, raw stateless webhooks, and durable event-driven orchestration:
| Engineering Dimension | Inngest + Signbee (Durable) | Stateless Webhook Handlers | Cron Polling + DB State Table |
|---|---|---|---|
| Idle Compute Cost (7-Day Wait) | $0.00 (Suspended outside Lambda) | $0.00 | 672 cron runs + DB queries per week |
| Post-Signature Provisioning Latency | < 180ms event wake-up | ~250ms (Fails if downstream DB spikes) | 5 to 60 minutes (Cron interval bound) |
| Duplicate Contract Protection | Automatic via step.run memoization | Manual idempotency keys required | Requires row-level locking (SELECT FOR UPDATE) |
| Reminder & Expiration Escalation | Native timeout return value in same file | Cannot detect non-events without separate cron | Complex SQL timestamp queries |
| Downstream Step Retries (CRM/S3) | Per-step isolated exponential backoff | Re-triggers entire webhook if 500 returned | Custom retry counter columns in DB |
3. Defining Type-Safe Event Schemas in Next.js App Router
Every production Inngest deployment should enforce strict TypeScript contracts across both trigger events and inbound webhook events. When building an affordable e-signature API integration, strict typing prevents subtle field-name mismatches (such as confusing documentId with document_id) that could cause a step.waitForEvent expression to miss its wakeup call.
Create your shared Inngest client in src/inngest/client.ts and register the three core lifecycle events: onboarding/deal.approved, signbee/document.signed, and signbee/document.expired.
import { EventSchemas, Inngest } from "inngest";
export type OnboardingDealApprovedEvent = {
name: "onboarding/deal.approved";
data: {
organizationId: string;
organizationName: string;
customerContactName: string;
customerContactEmail: string;
accountExecutiveEmail: string;
annualContractValueUsd: number;
seatCount: number;
planTier: "GROWTH" | "ENTERPRISE";
};
};
export type SignbeeDocumentSignedEvent = {
name: "signbee/document.signed";
data: {
document_id: string;
title: string;
status: "signed";
recipient_name: string;
recipient_email: string;
signed_at: string;
sha256_hash: string;
signed_pdf_url: string;
verify_url: string;
};
};
export type SignbeeDocumentExpiredEvent = {
name: "signbee/document.expired";
data: {
document_id: string;
title: string;
status: "expired";
recipient_email: string;
expired_at: string;
};
};
type AppEvents =
| OnboardingDealApprovedEvent
| SignbeeDocumentSignedEvent
| SignbeeDocumentExpiredEvent;
export const inngest = new Inngest({
id: "b2b-contract-orchestrator",
schemas: new EventSchemas().fromUnion<AppEvents>(),
});4. Building the Durable Contract Workflow with step.run and step.waitForEvent
Now let's implement the multi-step durable workflow inside src/inngest/functions/enterprise-onboarding.ts. Notice how we structure the function around four engineering invariants:
- Idempotent Contract Generation & Dispatch: Calling
POST https://signb.ee/api/v1/sendhappens strictly insidestep.run("dispatch-contract", ...). Even if downstream steps retry ten times, Signbee is called only once. - Multi-Stage Reminder Loop Before Final Expiry: Instead of waiting a blind 7 days and failing silently, we first wait
3dviastep.waitForEvent("wait-initial-3d", ...). If the customer hasn't signed after 72 hours, we send an automated Slack/email nudge with the cachedsigning_urland wait another4d. - Dynamic CEL Correlation: Because
dispatch.idis returned dynamically at runtime by Signbee (rather than existing on the initial trigger event), we use Inngest's Common Expression Language (if: `async.data.document_id == "${dispatch.id}"`) to match the exact inbound webhook event. - Cryptographic Archival & Provisioning: Once signed, separate
step.runblocks verify the SHA-256 hash, archive the immutable PDF to S3/R2, and activate the enterprise tenant.
import { inngest } from "../client";
import { NonRetriableError } from "inngest";
interface SignbeeSendResponse {
id: string;
status: "sent" | "pending_verification";
title: string;
signing_url: string;
verify_url: string;
expires_at: string;
}
export const enterpriseOnboardingWorkflow = inngest.createFunction(
{
id: "enterprise-contract-onboarding",
retries: 4,
concurrency: {
limit: 1,
key: "event.data.organizationId",
},
},
{ event: "onboarding/deal.approved" },
async ({ event, step }) => {
const deal = event.data;
// Step 1: Compile Markdown MSA & dispatch via Signbee REST API
const dispatch = await step.run("dispatch-contract", async (): Promise<SignbeeSendResponse> => {
const effectiveDate = new Date().toISOString().split("T")[0];
const contractMarkdown = [
`# Enterprise Master Services Agreement — ${deal.organizationName}`,
``,
`This Master Services Agreement ("Agreement") is entered into as of **${effectiveDate}** between **Acme Cloud Inc.** ("Provider") and **${deal.organizationName}** ("Customer").`,
``,
`## 1. Commercial Schedule`,
`| Item | Specification | Annual Fee (USD) |`,
`|---|---|---|`,
`| Subscription Tier | ${deal.planTier} Plan | $${deal.annualContractValueUsd.toLocaleString()} |`,
`| Licensed Seats | ${deal.seatCount} Named Users | Included |`,
`| Cryptographic Audit SLA | 99.99% Uptime + SHA-256 Sealing | Included |`,
``,
`## 2. Payment & Term`,
`This Agreement shall remain in force for twelve (12) months from the Effective Date. Invoices are payable Net-30.`,
].join("\n");
const response = await fetch("https://signb.ee/api/v1/send", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SIGNBEE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: `Enterprise MSA — ${deal.organizationName} (${effectiveDate})`,
markdown: contractMarkdown,
sender_name: "Legal Operations",
sender_email: deal.accountExecutiveEmail,
recipient_name: deal.customerContactName,
recipient_email: deal.customerContactEmail,
expires_in_days: 7,
webhook_url: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/signbee`,
}),
});
if (response.status === 401 || response.status === 422) {
const errBody = await response.text();
throw new NonRetriableError(`Signbee permanent rejection (${response.status}): ${errBody}`);
}
if (!response.ok) {
throw new Error(`Transient Signbee API error: HTTP ${response.status}`);
}
return (await response.json()) as SignbeeSendResponse;
});
// Step 2: Suspend execution for up to 3 days waiting for signature
let signedEvent = await step.waitForEvent("wait-for-signature-initial", {
event: "signbee/document.signed",
timeout: "3d",
if: `async.data.document_id == "${dispatch.id}"`,
});
// Step 3: If unsigned after 3 days, send automated nudge and wait remaining 4 days
if (!signedEvent) {
await step.run("send-72h-reminder-nudge", async () => {
await fetch(process.env.SLACK_DEAL_DESK_WEBHOOK_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `Reminder: ${deal.organizationName} MSA (${dispatch.id}) is still awaiting signature after 72h. Direct link: ${dispatch.signing_url}`,
}),
});
return { remindedAt: new Date().toISOString() };
});
signedEvent = await step.waitForEvent("wait-for-signature-final", {
event: "signbee/document.signed",
timeout: "4d",
if: `async.data.document_id == "${dispatch.id}"`,
});
}
// Step 4: Handle 7-day expiration cleanly without throwing unhandled errors
if (!signedEvent) {
await step.run("mark-deal-contract-expired", async () => {
return {
organizationId: deal.organizationId,
documentId: dispatch.id,
outcome: "EXPIRED_AFTER_7_DAYS",
};
});
return { status: "expired", documentId: dispatch.id };
}
// Step 5: Archive SHA-256 sealed PDF and provision enterprise workspace
const provisioningResult = await step.run("provision-enterprise-tenant", async () => {
const { sha256_hash, signed_pdf_url, signed_at, verify_url } = signedEvent.data;
return {
organizationId: deal.organizationId,
documentId: dispatch.id,
sha256Hash: sha256_hash,
signedPdfUrl: signed_pdf_url,
verifyUrl: verify_url,
signedAt: signed_at,
provisionedSeats: deal.seatCount,
tenantStatus: "ACTIVE_ENTERPRISE",
};
});
return {
status: "completed",
provisioning: provisioningResult,
};
}
);5. Inbound Next.js Webhook Route: Verifying X-Signbee-Signature & Emitting inngest.send
The bridge that wakes your hibernating Inngest function is a lightweight Next.js App Router webhook endpoint at src/app/api/webhooks/signbee/route.ts. Because this endpoint triggers downstream infrastructure provisioning, you must never trust an unverified HTTP POST body.
Signbee signs every outbound webhook request with an HMAC-SHA256 digest in the X-Signbee-Signature header using your endpoint's signing secret. Unlike legacy incumbents where webhook verification requires complex XML envelope parsing—a pain point we dissected in DocuSign API pricing and architecture explained—Signbee delivers a flat, deterministic JSON payload that validates in 12 lines of standard Node.js crypto code:
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
import { inngest } from "@/inngest/client";
function verifySignbeeSignature(rawBody: string, signatureHeader: string | null, secret: string): boolean {
if (!signatureHeader) return false;
const cleanSig = signatureHeader.replace(/^sha256=/, "");
const expectedHmac = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
const sigBuffer = Buffer.from(cleanSig, "hex");
const expectedBuffer = Buffer.from(expectedHmac, "hex");
if (sigBuffer.length !== expectedBuffer.length) return false;
return crypto.timingSafeEqual(sigBuffer, expectedBuffer);
}
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get("x-signbee-signature");
const webhookSecret = process.env.SIGNBEE_WEBHOOK_SECRET;
if (!webhookSecret) {
return NextResponse.json({ error: "Server webhook secret not configured" }, { status: 500 });
}
const isAuthentic = verifySignbeeSignature(rawBody, signature, webhookSecret);
if (!isAuthentic) {
return NextResponse.json({ error: "Invalid X-Signbee-Signature digest" }, { status: 401 });
}
const payload = JSON.parse(rawBody);
// Forward verified Signbee lifecycle event into Inngest durable bus
if (payload.event === "document.signed") {
await inngest.send({
name: "signbee/document.signed",
id: `signbee-signed-${payload.data.document_id}`, // Built-in 24h Inngest deduplication ID
data: {
document_id: payload.data.document_id,
title: payload.data.title,
status: "signed",
recipient_name: payload.data.recipient_name,
recipient_email: payload.data.recipient_email,
signed_at: payload.data.signed_at,
sha256_hash: payload.data.sha256_hash,
signed_pdf_url: payload.data.signed_pdf_url,
verify_url: payload.data.verify_url,
},
});
} else if (payload.event === "document.expired") {
await inngest.send({
name: "signbee/document.expired",
id: `signbee-expired-${payload.data.document_id}`,
data: {
document_id: payload.data.document_id,
title: payload.data.title,
status: "expired",
recipient_email: payload.data.recipient_email,
expired_at: payload.data.expired_at,
},
});
}
return NextResponse.json({ received: true, forwardedToInngest: true }, { status: 200 });
}Notice the id: `signbee-signed-${payload.data.document_id}` property passed to inngest.send(). Passing a deterministic event ID instructs Inngest's ingestion layer to deduplicate identical webhook deliveries over a 24-hour window, providing a second layer of idempotency on top of step.waitForEvent.
6. Handling Production Edge Cases: Superseding Contracts, Cancellation & Fan-Out
In real-world B2B sales cycles, contracts rarely follow a happy path on the first draft. Frequently, a prospect requests a redline change to seat count or payment terms after the first contract has already been dispatched and your Inngest workflow is paused inside step.waitForEvent.
If an account executive updates the deal in your CRM and clicks “Send Revised Contract,” you do not want the old paused workflow to stay alive or accidentally provision stale terms if the customer clicks the outdated email link. Inngest provides declarative Cancel On rules that pair naturally with Signbee's white-label e-signature API architecture:
- Automatic Superseding via
cancelOn: By addingcancelOn: [{ event: "onboarding/deal.approved", match: "data.organizationId" }]to your function configuration, emitting a newonboarding/deal.approvedevent for the sameorganizationIdautomatically terminates the previous waiting workflow instance while starting a fresh run with the revised contract markdown. - Zero-Cost Self-Hosted vs. Managed Economics: Teams comparing self-hosted stacks in our open-source e-signature API comparison often discover that running persistent Docker workers and Redis queues solely to track contract state costs $80–$250/month in baseline cloud instances. Running Inngest on top of Next.js serverless routes with Signbee's free e-signature API tier (3 free documents/month, then $20/month flat Pro unlimited) keeps infrastructure overhead near zero.
2026 Total Cost & Execution Benchmark (1,000 Enterprise Contracts / Month)
We benchmarked total monthly infrastructure + API cost and end-to-end execution latency for 1,000 monthly contract workflows across three common stacks (see our full e-signature API pricing comparison 2026 for deeper vendor breakdowns):
| Metric (1,000 Signed Docs / Mo) | Signbee Pro + Inngest + Vercel | DocuSign API + AWS Step Functions | Dropbox Sign + BullMQ (ECS Redis) |
|---|---|---|---|
| E-Signature API Cost | $20 / month (Flat Unlimited) | ~$3,800 / month ($3.80/envelope) | ~$1,500 / month ($1.50/envelope) |
| Orchestration & Queue Compute | $0 (Within Free 50k Inngest runs) | $42 / month (State transitions) | $115 / month (24/7 Redis + Worker) |
| Contract Dispatch P95 Latency | 340ms (Markdown to PDF + Email) | 2,150ms (Multi-call envelope + tabs) | 1,480ms (Template merge + send) |
| Local Dev Testing Experience | npx inngest-cli@latest dev (1 command) | AWS SAM / LocalStack JSON ASL files | Local Docker Redis + ngrok tunnel |
| Total Monthly Stack Cost | $20.00 / month | $3,842.00 / month | $1,615.00 / month |
7. Exposing the Next.js Serve Handler & Local Replay Testing
To expose your durable workflow to the Inngest execution engine in the Next.js App Router, create the standard src/app/api/inngest/route.ts endpoint using inngest/next. This single route exports GET, POST, and PUT handlers that allow Inngest Cloud (or the local Inngest Dev Server) to discover registered workflows, invoke individual steps, and checkpoint step outputs securely using your INNGEST_SIGNING_KEY.
import { serve } from "inngest/next";
import { inngest } from "@/inngest/client";
import { enterpriseOnboardingWorkflow } from "@/inngest/functions/enterprise-onboarding";
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [enterpriseOnboardingWorkflow],
streaming: "allow",
});One of the biggest engineering productivity gains of combining Inngest and Signbee is deterministic local replay. During local development, you do not need to wait 72 hours to test your 3-day reminder branch or manually sign a PDF twenty times to debug your PostgreSQL provisioning step. Simply run npx inngest-cli@latest dev alongside npm run dev and open http://localhost:8288 to inspect the visual step timeline.
Once step.run("dispatch-contract") completes and the function pauses on step.waitForEvent("wait-for-signature-initial"), you can click Send Test Event directly in the Inngest Dev UI with the returned document_id to simulate an instant signature—or fast-forward step timeouts to verify your expiration escalation path. Furthermore, if you deploy a hotfix to your downstream Salesforce or Stripe provisioning code in Step 5, you can replay failed production runs directly from the cached Step 4 checkpoint without ever re-sending the legal contract to the customer.
8. End-to-End Auditability: Linking Inngest Run IDs with Signbee SHA-256 Certificates
For SOC 2 Type II, ISO 27001, and financial compliance audits, engineering teams must prove not only that a contract was signed by an authorized counterparty, but also that automated production access or credit line provisioning occurred strictly after cryptographic execution. When your workflow resumes from step.waitForEvent, persist both the Inngest runId and Signbee's sha256_hash plus verify_url inside your PostgreSQL audit table. Any compliance auditor can inspect the immutable timeline showing the exact millisecond the contract was dispatched, the exact UTC timestamp Party B completed the signing ceremony, the SHA-256 hash of the sealed PDF, and the deterministic step execution that unlocked tenant resources.
9. Frequently Asked Questions: Inngest & Signbee Next.js Workflows
How does Inngest step.waitForEvent suspend a Next.js workflow without consuming serverless compute?
When an Inngest function encounters step.waitForEvent in a Next.js App Router API route, the Inngest SDK serializes the completed step state and returns an HTTP 206 Partial Content response to the Inngest execution engine, immediately terminating the Vercel or AWS Lambda invocation. Your serverless function consumes zero CPU or memory while waiting days or weeks for the recipient to sign the contract. Once your webhook route receives Signbee's document.signed callback and emits inngest.send({ name: 'signbee/document.signed', data }), Inngest invokes your Next.js function again with the memoized step history, resuming execution seamlessly at the exact line following step.waitForEvent.
How do you correlate an outbound Signbee contract with the right paused Inngest workflow?
Correlation is achieved deterministically using the document_id returned by Signbee's POST https://signb.ee/api/v1/send endpoint. Inside your first step.run('dispatch-contract', ...) block, you return the parsed JSON response containing id (for example, doc_01J9Z8K2M4). In the immediately following step.waitForEvent('wait-for-signature', ...) call, you configure either match: 'data.document_id' when the triggering event shares the same field or a CEL expression such as if: 'async.data.document_id == "' + dispatch.id + '"'. When Signbee fires the document.signed webhook, your webhook handler forwards data.document_id into inngest.send, waking only the matching workflow instance.
What happens if a recipient never signs the contract before the step.waitForEvent timeout expires?
If the human counterparty does not complete the signing ceremony within the configured timeout window (such as timeout: '7d' or '14d'), step.waitForEvent resolves to null rather than throwing an unhandled exception. This enables clean branching logic inside the same durable function: you can check if (!signedEvent) and trigger automated remediation steps using step.run, such as sending a polite reminder notification via Slack or email, dispatching a replacement contract with updated terms, or marking the enterprise onboarding record as expired in PostgreSQL without writing separate cron jobs.
Why is memoizing POST /api/v1/send inside step.run critical for e-signature idempotency?
Durable execution engines replay your handler function from the top whenever a subsequent step completes or retries. If you call fetch('https://signb.ee/api/v1/send') outside of a step.run wrapper, every workflow resumption or downstream database retry would execute a brand-new HTTP POST request, emailing duplicate legal contracts to your customer. Wrapping the API call inside step.run('dispatch-contract', ...) guarantees that Inngest executes the Signbee dispatch exactly once, caches the returned document_id and signing URL in its durable state store, and injects the cached result on all future replays.
How do I verify X-Signbee-Signature inside a Next.js App Router webhook before calling inngest.send?
In your Next.js App Router POST handler (for example, src/app/api/webhooks/signbee/route.ts), always read the raw request body via await req.text() before parsing JSON. Extract the X-Signbee-Signature header and compute an HMAC-SHA256 digest of the raw body string using Node's crypto.createHmac('sha256', process.env.SIGNBEE_WEBHOOK_SECRET). Compare the computed hex digest against the header using crypto.timingSafeEqual to prevent timing side-channel attacks. Only after cryptographic verification succeeds should you invoke await inngest.send({ name: 'signbee/document.signed', data: payload.data }) to resume the paused workflow.
Can I implement automated reminder loops while waiting for a Signbee signature in Inngest?
Yes. Instead of waiting for a single monolithic 14-day timeout, you can place step.waitForEvent({ timeout: '3d' }) inside a bounded sequence or loop of up to three iterations. If the 3-day wait returns null, your workflow runs step.run('send-reminder-1', ...) to notify the counterparty or account executive with the original signing_url, then enters a second step.waitForEvent({ timeout: '4d' }). As soon as the counterparty signs during any stage, step.waitForEvent immediately returns the webhook payload, skips remaining reminders, and proceeds straight to account provisioning.
Ship Durable E-Signature Workflows in Under 10 Minutes
Grab a free Signbee API key, drop POST /api/v1/send into your Inngest step.run handler, and eliminate per-envelope fees forever.