How to integrate e-signatures into a .NET application

Matt Kim
Matt Kim
Cover Image for How to integrate e-signatures into a .NET application

An e-signature integration lets your .NET service own the full signing loop instead of sending people out to another product. That keeps the workflow inside the app your users already know, and it removes the extra tabs and handoffs that usually slow the document signing process down.

In this blog post, we'll show you how to easily add e-signing into your own application using Formable. Formable offers a robust .NET SDK and detailed documentation to make the integration process as easy as possible.

What you'll need:


Table of Contents

Why an e-signature API from .NET?

Say you write C# for an insurance platform. Each claim still needs loss-payee forms and policy endorsements signed by the insured and the adjuster. When that lives in email attachments and a shared drive, the latest file goes missing, nobody can tell who has signed, and payouts slip. By integrating with Formable, all those manual signing processes can be automated within the platform.


Step 1: installing the Formable .NET SDK

dotnet add package Formable

Note:
For more details and advanced usage, you can also reference the Official Formable .NET SDK Documentation.


Step 2: creating the client

Register one client at startup. Pass HttpClient from IHttpClientFactory so the factory owns the handler lifetime. The SDK will not dispose an injected HttpClient.

using Formable;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("formable");
builder.Services.AddSingleton(sp =>
{
    var http = sp.GetRequiredService<IHttpClientFactory>().CreateClient("formable");
    return new FormableClient(new FormableOptions
    {
        ApiKey = builder.Configuration["FORMABLE_API_KEY"],
        HttpClient = http,
    });
});

var app = builder.Build();

If you are not in ASP.NET Core, the short form is fine:

using var formable = new FormableClient(
    Environment.GetEnvironmentVariable("FORMABLE_API_KEY")!);

Step 3: create a template from your document

A template is the re-usable document where you or your users place the fields for the signers to fill in.

Signer roles are optional for single signers. If you need to support multi-party signing, they are required as you need to specify which role needs to fill out which field.

var created = await formable.Templates.CreateAsync(
    "endorsement.pdf",
    [
        new TemplateSignerRole("Insured", 0),
        new TemplateSignerRole("Adjuster", 1),
    ]);

var templateId = created.TemplateId;
Console.WriteLine(created.EditTemplateAccess?.EditUrl);

Open the edit URL, place at least one required signature field, assign it Insured. The URL expires after a day:

var edit = await formable.Templates.CreateEditUrlAsync(templateId);
Console.WriteLine(edit.EditUrl);
Template editor with a Signature field selected over the Customer line, showing Required toggle and Reference ID in the sidebar
Place a Signature field on the document, then click Save template.

Save templateId and reuse it for later signature requests.


Step 4: choose a signing flow

You can choose to send a signing link via email to your user, or have your user sign within your own application within an iFrame.

If choosing email delivery, implement using the following code snippet.

var request = await formable.SignatureRequests.CreateAsync(
    new CreateSignatureRequest(
        templateId,
        [
            new Signer("jane@example.com", "Jane Doe", "Insured"),
            new Signer("adjuster@yourco.com", "Alex Chen", "Adjuster"),
        ],
        TestMode: true));

Console.WriteLine(request.SignatureRequestId); // save this

If you want your users to sign inside your application, the next section will show you how to implement the iframe path.


Step 5: embed signing in ASP.NET Core

Embedded signing requires two server calls.

First: Create the signature request.

Second: Mint a short-lived signing URL using the id from the signature request.

app.MapPost("/api/signature-requests", async (CreateRequest body, FormableClient formable) =>
{
    var request = await formable.SignatureRequests.CreateEmbeddedAsync(
        new CreateSignatureRequest(
            body.TemplateId,
            [new Signer(body.Signer.Email, body.Signer.Name, "Insured")],
            TestMode: true));

    return Results.Json(new
    {
        signatureRequestId = request.SignatureRequestId,
        recipientSignatureId = request.Signers[0].RecipientSignatureId,
    });
});

app.MapGet("/api/signing-url", async (string recipientSignatureId, FormableClient formable) =>
{
    var signing = await formable.SignatureRequests.CreateSigningUrlAsync(recipientSignatureId);
    return Results.Json(new { signing.SigningUrl, signing.ExpiresAt });
});

record CreateRequest(string TemplateId, SignerBody Signer);
record SignerBody(string Email, string Name);

On the client, fetch the URL from your ASP.NET service and load it in an iframe. Formable posts onSigningComplete to the parent window:

const { signingUrl } = await fetch(
  `/api/signing-url?recipientSignatureId=${recipientSignatureId}`
).then((res) => res.json());

document.getElementById("signing-frame").src = signingUrl;

window.addEventListener("message", (event) => {
  if (event.origin !== "https://app.formabledocs.com") return;
  if (event.data?.type === "onSigningComplete") {
    // close the iframe or show a success state
  }
});
<iframe
  id="signing-frame"
  width="100%"
  height="800"
  allow="fullscreen"
  style="border: none;"
></iframe>
Embedded signing view showing a Sign here field on the Customer line, a Required callout, and a Start button
The signer clicks Start and completes each required field on the document.
***

Step 6: handle webhooks

Register the path in Settings and keep the secret. Do not bind the body to a DTO. [FromBody] parses JSON, and HMAC is over the original bytes. Call EnableBuffering() if any middleware might read the stream first.

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

app.MapPost("/webhooks/formable", async (HttpRequest httpRequest, FormableClient formable, CancellationToken ct) =>
{
    httpRequest.EnableBuffering();
    using var reader = new StreamContent(httpRequest.Body);
    var rawBody = await reader.ReadAsByteArrayAsync(ct);
    var received = httpRequest.Headers["Content-Sha256"].ToString();
    var secret = Convert.FromBase64String(
        app.Configuration["FORMABLE_WEBHOOK_SECRET"]!);

    using var hmac = new HMACSHA256(secret);
    var expected = Convert.ToBase64String(hmac.ComputeHash(rawBody));

    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(received)))
    {
        return Results.Unauthorized();
    }

    using var doc = JsonDocument.Parse(rawBody);
    var root = doc.RootElement;
    var eventType = root.GetProperty("event").GetProperty("event_type").GetString();

    if (eventType == "document_completed")
    {
        var signatureRequestId = root.GetProperty("signing")
            .GetProperty("signature_request_id")
            .GetString()!;
        var envelope = await formable.SignatureRequests.GetSignedEnvelopeAsync(signatureRequestId, ct);
        _ = envelope.SignedEnvelopePresignedUrl;
        // download, store the PDF, mark the record complete
    }

    return Results.Ok();
});

document_completed is the terminal event. Once you receive this event, this means all parties have signed the document, and you may now fetch the signed document.

Alternatively, if you don't want to expose a webhook endpoint, you can poll for signature request status updates.

var current = await formable.SignatureRequests.GetAsync(signatureRequestId);
if (current.Status == SignatureRequestStatus.Completed)
{
    // safe to download
}

var recent = await formable.SignatureRequests.ListAsync(
    DateTimeOffset.UtcNow.AddDays(-1));

Step 7: download the signed PDF

The signed envelope URL is a short-lived presigned link. Pass CancellationToken so a cancelled request does not keep downloading.

var envelope = await formable.SignatureRequests.GetSignedEnvelopeAsync(signatureRequestId, ct);
var pdf = await httpClient.GetByteArrayAsync(envelope.SignedEnvelopePresignedUrl, ct);

The signed envelope includes the Formable audit trail of all relevant events.

Formable audit trail for document.docx showing Created, Sent, Signed, and Completed events with status Completed
Every signed document includes an audit trail with timestamps and actors.

Error handling and test mode

Non-2xx responses throw a FormableException. A 409 from GetSignedEnvelopeAsync means the document is not finished:

try
{
    await formable.SignatureRequests.GetSignedEnvelopeAsync(signatureRequestId);
}
catch (FormableException error) when (error.Status == 409)
{
    // wait for document_completed
}

400 is usually a FieldId that is not on the template. 401 is the API key. 404 is an unknown id.

Set TestMode: true while you integrate. Test documents are watermarked, not legally binding, and do not count toward billing. Clear the flag for production.


Conclusion

Your .NET service now owns signing end to end: a reusable template, email or embedded signing, a verified webhook, and a signed PDF with an audit trail stored on your side. The same handlers work whether the document is finance, HR, real estate, or anything else your app already creates.

To learn more about the Formable API, take a look at our API documentation. We support SDKs for a wide variety of languages, and are always looking to support more in the near future.

If you have questions, email matt@formabledocs.com. We are always happy to assist you!


FAQs

How do I add e-signatures to a .NET application?

Install the Formable NuGet package, create a template from your PDF or DOCX, then create a signature request. Formable can email signers, or you can generate a signing URL and embed it in an iframe. A webhook tells you when to download the signed PDF.

Does the SDK depend on a web framework?

No. It is a plain FormableClient. The examples use ASP.NET Core Minimal APIs. MVC controllers and worker services call the same async methods. Read the raw body on the webhook route, and pass CancellationToken into SDK calls.

How do I know when a document has been signed?

Handle document_completed on your webhook, or poll GetAsync until Status is Completed.

Can I test without sending legally binding documents?

Yes. Set TestMode: true on signature requests. Test documents are watermarked, not legally binding, and do not count toward billing.

Does the signed PDF include an audit trail?

Yes. Every signed document includes an appended audit trail with created, sent, signed, and completed events, including timestamps, actors, and IP addresses, which supports enforceability under ESIGN and UETA.

Formable
© 2026 Formable Inc. All rights reserved