DEV Community

Wes E
Wes E

Posted on

Your contract templates should live in your repo, not in a WYSIWYG editor

Every time I've needed signatures inside a product I was building, the same thing
happened. The signature part took an afternoon. The contract part took weeks —
because the legal text lived in someone else's WYSIWYG editor, behind a login,
with no version history anybody on my team could review.

That's a strange place to keep the one document with legal consequences.

So the tool I ended up building keeps templates as Markdown files. Here is the
whole format:

---
title: "Freelance Agreement"
description: "Flexible agreement for freelance work and projects."
category: Services
---

# FREELANCE AGREEMENT

**Effective Date:** {{effective_date}}

This Freelance Agreement is entered into by:

**Freelancer:**
{{freelancer_name}}
{{freelancer_address}}
{{freelancer_email}}

## 1. PROJECT DESCRIPTION

{{project_description}}
Enter fullscreen mode Exit fullscreen mode

Frontmatter, headings, {{variables}}. That's it. There is no other syntax to
learn, because there is no other syntax.

Why this shape matters more than it sounds like it should

A Markdown template is a file. Files do things that editor content can't:

It diffs. When someone changes the indemnity clause, that change shows up in
a pull request as a red line and a green line. A non-engineer can read that diff
and tell you what changed. Try getting that out of a rich-text editor's revision
history.

It reviews. The contract your company sends is now subject to the same
approval process as the code your company ships. Same reviewers, same audit
trail, same blame. If your legal counsel wants to own the clause language, they
own a file — and git log says when they last touched it.

It tests. You can lint it. You can grep every template for a term you're
retiring. You can write a test that fails if a template loses a required
variable. I have all of these; none of them were possible when the source of
truth was a form field on someone else's server.

It's yours. The template is in your repo whether or not you keep using my
product. That's a deliberate property, not an accident — I've been on the wrong
end of an export button before.

The full flow, end to end

Four calls. Auth is an x-api-key header on all of them; the base is
https://apisign.io/api.

1. Push a template. content is the Markdown above, verbatim:

curl -X POST https://apisign.io/api/template/create \
  -H "x-api-key: $APISIGN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Freelance Agreement",
    "content": "# FREELANCE AGREEMENT\n\n**Effective Date:** {{effective_date}}\n..."
  }'
Enter fullscreen mode Exit fullscreen mode

Returns { "template": { "id": "...", ... } }. Every save writes a version row,
so the server keeps its own history alongside your git history.

The natural next step is a CI job: on merge to main, POST each changed
.md file. Your templates are then deployed the same way your code is.

2. Create a contract by filling the variables in:

curl -X POST https://apisign.io/api/contract/create \
  -H "x-api-key: $APISIGN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Freelance Agreement — Jane Doe",
    "template_id": "TEMPLATE_ID",
    "variables": {
      "effective_date": "2026-08-10",
      "freelancer_name": "Jane Doe",
      "freelancer_email": "jane@example.com",
      "project_description": "Design system refresh, 6 weeks."
    },
    "expires_in_days": 14,
    "signers": [
      { "email": "jane@example.com", "name": "Jane Doe", "signing_order": 1 }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

You get back the contract and its signers. Each signer carries an explicit
signing_order, so sequential signing is a number rather than a workflow
builder. The signing link itself is generated at send time and goes into the
email — it is not on the create response.

3. Send it:

curl -X POST https://apisign.io/api/contract/send \
  -H "x-api-key: $APISIGN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contract_id": "CONTRACT_ID" }'
Enter fullscreen mode Exit fullscreen mode

Signers get an email with their link. They sign in the browser — no account, no
app, no download.

4. Find out when they signed. Register a webhook endpoint and you'll get
contract_signed when a signer finishes and contract_completed when everyone
has. (Yes, underscores. Stripe and GitHub use dots and I'd probably pick dots if
I were starting today, but renaming them is a migration with a live integration
on the other end, not a Tuesday.)

One thing I'd tell you to do anyway: reconcile against GET /contract/get
rather than trusting delivery.
Webhooks retry with backoff, but the retry is
kicked by your org's next event rather than by a scheduler, so a failed delivery
to a quiet account waits. Every attempt is visible via the webhook API. I'd
rather you know that than discover it.

What it costs, since that's the next question

$0.25 per contract sent. No subscription, no per-seat charge, no minimum. A
contract with six signers costs the same as one with one signer, because the
charge is per send, not per signature. A resend goes through the same path, so it
costs another $0.25 — there's no free reminder, and I'd rather say that here than
have you find it on the invoice.

Sign up, create an organization, and you have $5.00 on it — 20 contracts, no
card.

Where it falls short

Nobody outside my own companies has paid for this yet. That's the honest number
and I'd rather lead with it than build a logo wall.

Some specifics, because "early" is a word people use to avoid a list:

  • API key read / read_write permissions aren't enforced yet. The setting is stored and displayed; nothing checks it. Treat every key as read-write and don't hand one to a third party expecting it to be scoped.
  • There's no rate limiting on the API.
  • No idempotency key on send. If your HTTP client retries a timed-out send, you can be charged twice and your signer gets two emails. Guard the call on your side.
  • Webhook retries are best-effort, per above.

There are 37 templates in the library to start from, and an MCP server if you
want an agent driving it.

If you keep contract text in a WYSIWYG editor today and it's working fine for
you, this isn't worth switching for. If you've ever tried to find out who changed
a clause and when, I built this for that.

Docs: https://apisign.io/docs — happy to answer anything here, including the
parts that don't work.

Top comments (0)