Documentation

Everything you need to embed Conovo.

Six steps to a signed contract, then the concepts and reference behind them. The whole get-started path runs against the sandbox, so you can see a real generated document before thinking about billing or a signing provider.

01

Get a secret key

Create an account, then open API keys in the console and generate one. The full key is shown once and stored hashed. If you lose it, revoke it and make another.

Keys are per-account, not per-business. One key serves every workspace you create. It is a server-side credential: if it ever reaches a browser bundle, revoke it.

.env.local
CONOVO_SECRET_KEY=sk_live_…
02

Register your payload schema

Describe the shape of the records a contract can draw from. Conovo uses it to propose bindings and to reject a binding path that could never resolve. Name the objects whatever your product calls them: a deal, a job, a case, a matter, a policy.

Register it on the Payload schema page. Versions are append-only, so adding a field later never invalidates a template that pinned an earlier version.

payload schema
{
  "org":    { "name": "string", "address": "string" },
  "client": { "fullName": "string", "email": "string" },
  "deal":   {
    "id": "string",
    "name": "string",
    "startDate": "date",
    "total": "money"
  }
}
03

Mint a session

Create this route once and forget it. You never call it yourself. The provider in step 04 calls it automatically whenever a Conovo component needs a token: on first render for a signed-in user, and again each time the 15-minute token expires. There is nothing to run when a business signs up or logs in.

What it does: the browser never sees your secret key, so your server exchanges it for a short-lived token scoped to exactly one workspace: whichever business is signed in right now. Pass your own stable ID for them as externalRef. Conovo creates the workspace on the first mint and matches it on every mint after, so there is no provisioning step.

app/api/conovo/session/route.ts
import { Conovo } from '@conovo/node'

const conovo = new Conovo({ secretKey: process.env.CONOVO_SECRET_KEY })

export async function POST() {
  const org = await currentOrg()   // however you resolve the signed-in business

  const session = await conovo.sessions.create({
    workspace: { externalRef: org.id, name: org.name },
    user: { id: currentUser.id, role: 'owner' },   // optional, for the audit trail
  })

  // { token, expiresAt } (15 minutes; re-mint freely)
  return Response.json(session)
}
Scope comes from the token, never the request. A session minted for one workspace cannot read another’s templates or contracts, whatever the client sends. You never pass a workspace ID to a component.
04

Mount the provider

Wrap anything that renders Conovo components. The provider caches the token, re-mints it on expiry, and surfaces a locked state if the account lapses, so a billing problem renders as your empty state rather than a stack trace.

app/providers.tsx
'use client'

import { ConovoProvider } from '@conovo/react'
import '@conovo/react/styles.css'

const getSession = () =>
  fetch('/api/conovo/session', { method: 'POST' }).then((r) => r.json())

export function Providers({ children }) {
  return <ConovoProvider getSession={getSession}>{children}</ConovoProvider>
}

Styles are scoped and driven by custom properties. Override them anywhere in your own stylesheet.

your stylesheet
.conovo {
  --cv-accent: #2e42c6;
  --cv-radius: 6px;
  --cv-font: 'Your UI Font', system-ui, sans-serif;
}
05

Set up a template

ContractStudio is upload and review in one component. Your user drops in the document they already send; Conovo proposes fields, formulas, signing parties, repeating tables, and conditional sections, each highlighted in the document. Nothing is saved until they confirm.

app/settings/contracts/page.tsx
import { ContractStudio } from '@conovo/react'

export default function ContractSetup() {
  return (
    <ContractStudio
      onConfirmed={({ fieldCount, autoFillCount }) => {
        toast(`Template ready: ${autoFillCount} of ${fieldCount} fields fill automatically`)
      }}
    />
  )
}
06

Send a contract

Hand SendContract the record the contract is about. Bindings resolve against it, standing defaults fill themselves, and anything left over is asked as a typed question. Your user reviews a real draft PDF before sending.

app/deals/[id]/contract.tsx
import { SendContract } from '@conovo/react'

export function DealContract({ deal, client }) {
  return (
    <SendContract
      subject={deal}
      defaultRecipient={{ name: client.fullName, email: client.email }}
      onSent={(contractId) => router.push(`/deals/${deal.id}/contracts/${contractId}`)}
    />
  )
}
§

Workspaces

A workspace is one of your businesses. It owns templates, standing defaults, and contracts, and it is the unit of isolation: nothing crosses between workspaces.

You never create one explicitly. The first session minted for an externalRef creates it; every mint after matches on that ref and refreshes the display name, so renames on your side follow through automatically.

§

Templates and versions

Confirming in the studio cuts a new immutable template version. Every contract pins the version it was generated from and stores the exact resolved values used, so a contract signed two years ago can be reproduced byte for byte even after the template has moved on.

Editing a template never rewrites history and never changes a contract already sent.

§

Where values come from

Every field has a source, and they resolve in a fixed order. Understanding this is most of understanding Conovo.

SourceFilled from
platform_boundThe subject you pass to the component, via a binding path
workspace_defaultA standing value the business set once
per_dealAsked at send time, as a typed input
computedA stored formula, evaluated in decimal-safe code
conditionalA section kept or removed based on a stored condition

A binding that misses (the path exists but your payload didn’t carry a value) falls through to being asked at send time rather than failing. Those misses are aggregated into a payload gap report in the console, so you can see which fields would benefit from being added to your schema.

§

Components

All of them require the provider above it and take their workspace scope from the session. None takes an API key. Two are setup surfaces a business visits occasionally (Studio, StandingTerms); the rest are the everyday surfaces where contracts get sent and tracked.

ComponentWhere it goes in your productWhat your user does there
ContractStudioA “contract templates” settings page, used once per template, not per dealUploads the contract they already use (DOCX/PDF); AI proposes the fillable fields, formulas and signer roles; they review, edit and confirm. Includes the optional document check-up. onConfirmed?
ContractTemplatesSame settings page, beside the StudioSees every template they’ve confirmed and its status
SendContractYour project / job / booking detail page, wherever a “send contract” button belongsThe per-deal panel: values auto-fill from the subject record you pass, they fill what’s left, preview the finished PDF, and send it for signature. subject, defaultRecipient, onSent?, notify?. Set notify="platform" and the signing vendor emails nobody, so you send the request yourself, from your own domain, to your own SignContract page
BulkSendBehind a “bulk send” action, for users with lists (renewals, a season’s clients)Uploads a CSV; AI proposes the column mapping, they confirm it; a pre-flight table shows every row’s issues before anything sends; then a live batch with pause/resume
ContractInboxA “contracts” page or tab in your appTracks everything sent and where each one stands (viewed, signed, completed), with the document itself. renderItemActions? adds your own per-row buttons
SignContractA recipient-facing page, if you want signing to happen inside your productThe counterparty signs without leaving your domain; falls back to the provider’s hosted signing link automatically when embedding is refused. Recipients sent with a mobile number pass an SMS identity check first, recorded in the audit trail. Build it as a stable /sign/[contractId] route: it resolves the live signing link on every request, so if the sender corrects the contract before it’s signed, the same link keeps working
StandingTermsYour settings areaEdits the values that pre-fill every contract, such as their own address, rates and standard terms, so repeat fields never get retyped
§

Implementation prompts

One self-contained prompt per surface, written for a coding agent. Each carries the exact import, the real prop types, the prerequisite, the constraints that are easy to get wrong, and something to verify at the end. Paste it into your agent and review the diff. They are generated from the same source we maintain alongside the SDK, so a prop named here exists.

Foundation

Build this first — every component needs it.

ConovoProvider + session route

The foundation every other component requires.

Setup surfaces

Visited occasionally, when a business adds or changes a contract.

ContractStudio

Upload a contract, confirm the AI's field mapping, save a template.

ContractTemplates

The templates a business has set up, and what each one fills.

StandingTerms

Set-once values that fill into every future contract.

Everyday surfaces

Where contracts actually get sent, signed, and tracked.

SendContract

Generate a filled contract for one deal and send it for signature.

SignDocument

Your own signing page — the recipient signs on your domain.

SignContract

Signing when the account uses an external signing vendor.

ContractInbox

Every contract sent, and where each one stands.

BulkSend

Send the same contract to many recipients from a CSV.

§

Server SDK

@conovo/node has no dependencies and does three things: mints sessions, verifies webhooks, and verifies data-connector requests.

@conovo/node
import { Conovo, ConovoError } from '@conovo/node'

const conovo = new Conovo({ secretKey: process.env.CONOVO_SECRET_KEY })

// mint a workspace-scoped token (15 minutes)
await conovo.sessions.create({ workspace: { externalRef, name } })

// timing-safe HMAC over the raw request body
conovo.webhooks.verify(rawBody, signatureHeader, webhookSecret)   // → boolean
conovo.connector.verify(rawBody, signatureHeader, signingSecret)  // → boolean

// entitlement failures surface as a typed error
try {
  await conovo.sessions.create({ workspace })
} catch (err) {
  if (err instanceof ConovoError && err.reason === 'account_lapsed') {
    // show your own billing message
  }
}
§

Coding agents (MCP)

Most of this integration can be done by your coding agent. @conovo/mcp is an MCP server that gives Claude Code, Cursor, or any MCP client direct tools over your account: point it at your repo, say “integrate Conovo”, and it can read your data model, build and register the payload schema, mint sandbox sessions, drive test contracts through the real API, wire up webhooks, and read the request inspector when a call fails.

one-time setup (Claude Code)
claude mcp add conovo -e CONOVO_SECRET_KEY=sk_test_… -- npx -y @conovo/mcp

Any MCP client works. The server is npx -y @conovo/mcp with CONOVO_SECRET_KEY in its environment. Use an sk_test_ key while integrating: every session it mints is test mode end to end, so nothing your agent does can bind or bill.

The tools: register_payload_schema / infer_payload_schema / get_payload_gaps (the schema loop), create_sandbox_session + get_api_reference (drive the /v1 API for real), list_requests / get_held_contracts / list_events (debugging), and get_webhook / set_webhook / test_webhook.

What it deliberately cannot do: no tool touches a live contract, produces a document value, or returns contract content. The agent integrates the product; it doesn’t operate it.
§

Webhooks

Conovo posts status changes as the recipient moves through the signing flow. Verify the signature over the raw body before trusting anything, and return 2xx for events you don’t handle so they aren’t retried.

app/api/conovo/webhook/route.ts
import { Conovo } from '@conovo/node'

const conovo = new Conovo({ secretKey: process.env.CONOVO_SECRET_KEY })

export async function POST(req: Request) {
  const raw = await req.text()              // raw bytes, not the parsed body
  const signature = req.headers.get('conovo-signature') ?? ''

  if (!conovo.webhooks.verify(raw, signature, process.env.CONOVO_WEBHOOK_SECRET)) {
    return new Response('bad signature', { status: 401 })
  }

  const event = JSON.parse(raw)
  switch (event.type) {
    case 'contract.viewed':    break
    case 'contract.signed':    break
    // Everyone has signed — but the executed file may not exist YET.
    case 'contract.completed': await markSigned(event.data.contractId); break
    // The signed PDF and certificate now exist. Fetch them here, not above.
    case 'contract.executed':
      await store(event.data.contractId, event.data.executionHash)
      break
    case 'contract.declined':  break
  }

  return new Response('ok')
}

Delivery is at-least-once. Handlers must be idempotent: key on event.id if you write on receipt.

§

Errors

Errors are RFC 7807 problem+json. The status tells you the class; the reason field is the machine-readable detail worth branching on.

StatusMeansDo
401Token missing, expired or malformedRe-mint a session and retry once
402Account not entitled; see the reasonShow your billing message; the provider exposes a locked state
403Out of workspace scopeA bug: the token doesn’t own that resource
409State conflict, e.g. sending a contract needing attentionSurface the validation issues instead
422Understood but impossible, e.g. a source file that can’t be filledRead the plain-English reason; don’t retry
§

Going live

Swap the sandbox key for a live one, point your webhook at your production URL, and add billing in the console. Nothing else in your code changes.

Two things worth doing before real contracts move: send one to your own inbox and sign it end to end, and confirm your app renders sensibly on a 402. That is what your users see if a card fails.

Auto-send unlocks on evidence, not on a setting. A template must be reviewed and sent by a human several times before unattended sending is available for it. Until then, requesting auto-send returns a draft and tells you why. Build your UI to expect that from day one.

Next

Get a key and try it against test data.

The sandbox generates real documents and fires real webhooks. Nothing binds until you move to a live key.

Documentation | Conovo