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)

Loading signer…

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.

Working…

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.
Provider-owned request shapes
Each remote signer exposes the documents, recipients, statuses, and limits its provider actually supports.
ProviderClicksign
clicksign.ts
import * as Alchemy from "alchemy"
import { ClicksignSignatureRequest, providers as clicksignProviders } from "@signature-kit/clicksign"
import { signatureHttpClientLive } from "@signature-kit/http"
import { Effect, Layer, Redacted } from "effect"

export default class Contracts extends Alchemy.Stack<Contracts>()(
  "Contracts",
  {
    providers: Layer.merge(
      clicksignProviders({
        accessToken: Redacted.make(process.env.CLICKSIGN_TOKEN ?? ""),
        environment: "sandbox",
        locale: "pt-BR",
        autoClose: true,
      }),
      signatureHttpClientLive,
    ),
    state: Alchemy.inMemoryState(),
  },
  Effect.gen(function* () {
    return yield* ClicksignSignatureRequest("membership-agreement", {
      title: "Membership agreement",
      documents: [{ fileName: "agreement.pdf", mimeType: "application/pdf", contentBase64: pdfBase64 }],
      recipients: [{ name: "Bruno Lima", email: "bruno@example.com", role: "signer" }],
    })
  }),
) {}
import * as Alchemy from "alchemy"
import { AssinafySignatureRequest, providers as assinafyProviders } from "@signature-kit/assinafy"
import { signatureHttpClientLive } from "@signature-kit/http"
import { Effect, Layer, Redacted } from "effect"

export default class Contracts extends Alchemy.Stack<Contracts>()(
  "Contracts",
  {
    providers: Layer.merge(
      assinafyProviders({
        accountId: process.env.ASSINAFY_ACCOUNT_ID ?? "",
        apiKey: Redacted.make(process.env.ASSINAFY_API_KEY ?? ""),
        environment: "sandbox",
      }),
      signatureHttpClientLive,
    ),
    state: Alchemy.inMemoryState(),
  },
  Effect.gen(function* () {
    return yield* AssinafySignatureRequest("contract", {
      title: "Contract",
      documents: [{ fileName: "contract.pdf", mimeType: "application/pdf", contentBase64: pdfBase64 }],
      recipients: [{ name: "Carla Nunes", email: "carla@example.com" }],
    })
  }),
) {}
import * as Alchemy from "alchemy"
import { ZapSignSignatureRequest, providers as zapSignProviders } from "@signature-kit/zapsign"
import { signatureHttpClientLive } from "@signature-kit/http"
import { Effect, Layer, Redacted } from "effect"

export default class Contracts extends Alchemy.Stack<Contracts>()(
  "Contracts",
  {
    providers: Layer.merge(
      zapSignProviders({
        apiToken: Redacted.make(process.env.ZAPSIGN_API_TOKEN ?? ""),
        environment: "sandbox",
        locale: "pt-br",
      }),
      signatureHttpClientLive,
    ),
    state: Alchemy.inMemoryState(),
  },
  Effect.gen(function* () {
    return yield* ZapSignSignatureRequest("contract", {
      title: "Contract",
      documents: [{ fileName: "contract.pdf", mimeType: "application/pdf", contentBase64: pdfBase64 }],
      recipients: [{ name: "Davi Rocha", email: "davi@example.com" }],
    })
  }),
) {}
import * as Alchemy from "alchemy"
import { DocuSealSignatureRequest, providers as docuSealProviders } from "@signature-kit/docuseal"
import { signatureHttpClientLive } from "@signature-kit/http"
import { Effect, Layer, Redacted } from "effect"

export default class Contracts extends Alchemy.Stack<Contracts>()(
  "Contracts",
  {
    providers: Layer.merge(
      docuSealProviders({
        apiKey: Redacted.make(process.env.DOCUSEAL_API_KEY ?? ""),
        baseUrl: "https://api.docuseal.com",
        submittersOrder: "preserved",
      }),
      signatureHttpClientLive,
    ),
    state: Alchemy.inMemoryState(),
  },
  Effect.gen(function* () {
    return yield* DocuSealSignatureRequest("service-agreement", {
      title: "Service agreement",
      documents: [{ fileName: "agreement.pdf", mimeType: "application/pdf", contentBase64: pdfBase64 }],
      recipients: [{ name: "Ana Silva", email: "ana@example.com", role: "signer" }],
    })
  }),
) {}
import * as Alchemy from "alchemy"
import { DocumensoSignatureRequest, providers as documensoProviders } from "@signature-kit/documenso"
import { signatureHttpClientLive } from "@signature-kit/http"
import { Effect, Layer, Redacted } from "effect"

export default class Contracts extends Alchemy.Stack<Contracts>()(
  "Contracts",
  {
    providers: Layer.merge(
      documensoProviders({
        apiKey: Redacted.make(process.env.DOCUMENSO_API_KEY ?? ""),
        baseUrl: "https://app.documenso.com/api/v2",
      }),
      signatureHttpClientLive,
    ),
    state: Alchemy.inMemoryState(),
  },
  Effect.gen(function* () {
    return yield* DocumensoSignatureRequest("service-agreement", {
      title: "Service agreement",
      documents: [{ fileName: "agreement.pdf", mimeType: "application/pdf", contentBase64: pdfBase64 }],
      recipients: [{ name: "Ana Silva", email: "ana@example.com", role: "approver", routingOrder: 1 }],
    })
  }),
) {}

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/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.
ClicksignSignatureRequest("contract", props)
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 { signPdfSignatureField } from "@signature-kit/pdf/workflow"

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/signatures @signature-kit/a1
npm install @signature-kit/signatures @signature-kit/a1
pnpm add @signature-kit/signatures @signature-kit/a1
yarn add @signature-kit/signatures @signature-kit/a1

@signature-kit/signatures 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/signatures"
import { Effect, Redacted } from "effect"

const layer = a1SignaturesLayer({
  pfx,
  password: Redacted.make(process.env.A1_PASSWORD ?? ""),
})

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