Open source · MIT · A Montte product

One typed boundary for every signature.

A1/PKCS#12, PDF/PAdES, XML-DSig, and remote providers, all Effect-native. Swap the signer, keep the program — browser or server, typed errors, Redacted secrets.

Read the docs
  • Clicksign
  • Assinafy
  • ZapSign
  • DocuSeal
  • Documenso

Live · in your browser

Sign a real PDF, right here.

No backend, no upload, no SaaS. Drop in a PDF, click where the signature goes, load your A1 (e-CPF / e-CNPJ), and sign with WebCrypto. The document and the private key never leave the page.

signaturekit — browser signer
01

Upload your PDF

Rendered locally with pdf.js.

02

Click to place

Drop the PAdES signature rectangle anywhere on any page.

03

Sign with your A1

Your .pfx and password sign in-browser via WebCrypto.

100% client-side · WebCrypto · PAdES (AdES-BES)

Auto-signature

Position once. Sign the whole stack.

Several real contracts are generated in your browser with @react-pdf/renderer, rendered on the same pdf.js placement canvas the signer uses, then auto-placed and signed in a queue — one document at a time.

Remote providers

One SDK for every signer.

Eight signature providers, one request shape. Pick a provider — the create*SignatureRequest call swaps, your application code doesn't.

Adapters
Every provider is one create*SignatureRequest over a shared SignatureHttpClient — provider-specific code stays out of your app.
Typed errors
Nothing throws. Every failure is one SignatureKitError on the Effect channel, tagged with the provider and a literal code.
Redacted secrets
Tokens and API keys stay wrapped in Redacted until the HTTP boundary — never logged, never leaked.
One request shape
The same title / documents / recipients request normalizes to a RemoteSignatureRequest across all eight providers.
ProviderClicksign
clicksign.ts
import { createClicksignSignatureRequest } from "@signature-kit/clicksign"
import { signatureHttpClientLive } from "@signature-kit/core/http"
import { Effect, Redacted } from "effect"

const request = yield* createClicksignSignatureRequest(
  {
    accessToken: Redacted.make(process.env.CLICKSIGN_TOKEN ?? ""),
    environment: "sandbox",
    locale: "pt-BR",
    autoClose: true,
  },
  {
    title: "Membership agreement",
    documents: [{ fileName: "agreement.pdf", mimeType: "application/pdf", content: pdfBytes }],
    recipients: [{ name: "Bruno Lima", email: "bruno@example.com", role: "signer" }],
  },
).pipe(Effect.provide(signatureHttpClientLive))

Seven capabilities

Everything you sign, one API.

A1 / PKCS#12 in the browser or on the server, PDF·PAdES and XML-DSig, eight remote providers, one typed error channel, and a React surface for in-browser signing — the same Effect-native ergonomics across every backend.

01
One service, every format
inspect, sign, and verify hang off one Signatures service — provide a layer and the same three accessors drive every backend.
import { signatures } from "@signature-kit/core/signatures"
02
A1 in the browser or on the server
Load an e-CPF / e-CNPJ A1 (.pfx / PKCS#12) once for a Signatures layer; same call server or browser, password kept in Redacted.
a1SignaturesLayer({ pfx, password: Redacted.make(password) })
03
PDF, PAdES, ICP-Brasil
signPdf seals under a PAdES policy: pades-icp-brasil for the Brazilian ICP chain, or pades-ades.
signPdf({ pdf, policy: "pades-icp-brasil" })
04
XML-DSig, by reference
signXml writes an enveloped XML-DSig over a referenced node; verifyXml needs no layer — the key rides in the document.
signXml({ xml, referenceId: "nfe-1" })
05
Remote providers, one typed channel
Clicksign, Assinafy, ZapSign, DocuSeal, and Documenso each get a create*SignatureRequest over one HTTP client.
createClicksignSignatureRequest(config, request)
06
One typed error channel
Nothing throws. Every failure is one SignatureKitError carrying one of 18 literal codes, caught in the Effect error channel.
Effect.catchTag("SignatureKitError", handle)
07
Sign PDF in the browser
@signature-kit/pdf owns browser file reads, placement state, and A1 PDF signing. The live signer above renders the app UI around that typed boundary.
import { signBrowserPdf } from "@signature-kit/pdf/browser"

Two steps

Your first signature, end to end.

Install the core package and the A1 signer, then construct one layer and run a program that inspects the certificate and signs.

011. Install
bun add @signature-kit/core @signature-kit/a1
npm install @signature-kit/core @signature-kit/a1
pnpm add @signature-kit/core @signature-kit/a1
yarn add @signature-kit/core @signature-kit/a1

@signature-kit/core is the typed entry point. Each signer and format installs only when you reach for it: /a1/pdf/xml

022. Make your first callfirst-signature.ts
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { signatures } from "@signature-kit/core/signatures"
import { Effect, Redacted } from "effect"

// Load the A1 / PKCS#12 container once — pfx is the .pfx/.p12 *bytes*.
const layer = a1SignaturesLayer({
  pfx,
  password: Redacted.make(process.env.A1_PASSWORD ?? ""),
})

// Inspect the certificate, then sign — typed errors, no thrown exceptions.
const program = Effect.gen(function* () {
  const identity = yield* signatures.inspect()

  const artifact = yield* signatures.sign({
    content,
    algorithm: "rsa-sha256",
  })

  return { identity, artifact }
}).pipe(Effect.provide(layer))

const { identity, artifact } = await Effect.runPromise(program)

runPromiseresolves to the parsedidentityand the signedartifact. From here, swap the A1 layer for a format —signPdforsignXml— or hand the program to a remote provider.

Backed by

Ready when you are

Ship the signing layer once.

Open source, MIT licensed, Effect-native. A1, PDF, XML, and remote providers behind one typed program — every failure a value you can match on.

Quickstart