Contract templates and clauses
The documents a deal renders — templates in the versioned configuration, the placeholder whitelist they may use, clauses with pre-approved alternatives, redlines, and the evidence a signed contract leaves behind.
Live. Everything on this page works today: templates and clauses in the versioned configuration, publish-time compilation that fails closed, pre-approved alternatives the buyer picks without a human, redlines that always escalate, and the signature records, document hashes and certificate of completion a signed contract leaves behind. The curl examples call this deployment's API at
https://app.skipthesell.comwith the OpenAPI document's path templates — replace{placeholders}with your resource ids.
A template is one of your documents — a master subscription agreement, an order form, a DPA, an NDA — with placeholders the engine fills from the deal. A clause is a section of a template that offers pre-approved alternatives: wording you have already agreed to, which a buyer (or their agent) may choose without anybody escalating. Anything else the buyer wants is a redline, and a redline always goes to a human. (The vocabulary is in core concepts; the lifecycle from the buyer's side is in how a deal works.)
Where templates live#
Templates sit in your configuration document, beside your catalog, offers and rules — not in a separate library. The document is versioned: every publish creates an immutable configuration version, and each deal is pinned to the version it started on. A template edit is therefore a configuration change like any other: it publishes a new version, deals already in flight keep the templates they pinned, and a signed contract is never rewritten. (The publishing flow is in seller setup.)
{
"templates": {
"order_form": {
"title": "Order form",
"source": "<the document's Markdown>"
},
"msa": {
"title": "Master Subscription Agreement",
"source": "<Markdown>"
},
"nda": {
"title": "Mutual non-disclosure agreement",
"source": "<Markdown>",
"gated_by": "nda"
}
}
}
Three members, and nothing else:
title— the document's human title. It becomes the canonical HTML's<title>, and a template without one does not publish. Seller prose: it is served as data, never as instructions to a buyer's agent.source— the template itself, written in Markdown. This is authoring only: the engine renders it to canonical HTML exactly once per contract, stores those bytes and takes the content hash over them. The canonical HTML is the record of truth — Markdown is not stored as the signed artifact and PDF is only ever a convenience export.gated_by— optional. Names a gate key that withholds the document until that gate is satisfied; the document is listed but its content is not served before then. Publish checks that every offer able to unlock the template declares that gate (gated_item_gate_undeclared).
Templates are selected into a contract by the offer's rules, not by the buyer: a document effect names the template key ({"document": {"name": "msa"}}), and a gate can unlock one. A template nothing selects is never rendered, but publish still compiles it — against the fixed whitelist alone, without your offer's declared variables — so a broken one blocks the publish anyway. And a document effect naming a key that does not exist does not publish.
Read the draft that holds them, or replace the whole document (the same PUT that imports a published version verbatim):
# operationId: get_draft
curl https://app.skipthesell.com/api/v1/drafts/{draft_id} \
-H 'Authorization: Bearer <api key>'
The placeholder whitelist#
A template fills itself with {{a.b}} substitutions drawn from a fixed, engine-defined whitelist. The chain of trust is the point: the whitelist is additive-only, and every configuration version pins the version of it that it compiled against, so the bytes of a signed document can always be reproduced from the version it was rendered under.
The namespaces:
| Namespace | What it binds |
|---|---|
seller.* |
Your legal identity and legal parameters, from the pinned version's entity and legal blocks, plus the deployment's own URLs — e.g. {{seller.legal_name}}, {{seller.signatory_title}}, {{seller.ops_email}}, {{seller.breach_notification_hours}}. |
buyer.* |
The buyer organization and its inputs — {{buyer.legal_name}}, {{buyer.address}}, {{buyer.po_number}}. |
deal.* |
Deal facts and statuses — {{deal.id}}, {{deal.created_at}}, {{deal.done_condition}}, and the deal.gates / deal.approvals collections. |
quote.* |
The accepted quote: the only source of numbers in any document — {{quote.total}}, {{quote.currency}}, {{quote.term_months}}, and the quote.lines / quote.discounts collections. Money is formatted once by the engine. |
signer.* |
Signature blocks, filled from the signature records at display time — never typed into the stored bytes. |
contract.* |
{{contract.completed_at}}, a display-time fill. |
Plus your own declared variables: a key in an offer's variables map binds as {{deal.variables.<name>}} — the only place a third name segment is allowed. Enums display their declared label, integers display as plain decimals, dates as YYYY-MM-DD. A bool is never whitelisted: conditional content belongs in clause selection, which the rules do before render, not in a template.
What a template may not do: compute. No arithmetic, no conditionals, no formatting of its own. Every number comes from the quote. There is deliberately no effective_date placeholder for exactly that reason — dates are facts ({{deal.created_at}}, {{quote.accepted_at}}), and an effective date's legal meaning is a matter for your counsel, not your template.
Collections are laid out with a fixed loop vocabulary — the loop name and the fields are the engine's, not yours:
| # | Line | Qty | Amount |
|---|---|---|---|
{{each line in quote.lines}}
| {{line.seq}} | {{line.name}} | {{line.qty}} | {{line.amount}} |
{{end}}
A declared set iterates the same way, ordered by value: {{each member in deal.variables.<name>}} with member.value and member.label. Directives stand on their own line; loops and clause blocks sit at the top level of a template. A template that reaches outside this grammar does not publish.
Publishing fails closed#
Template compilation is part of publish-time validation, so a broken template is a diagnostic against your draft — never a live deal that fails to render. Every template in the document is compiled: one an offer can select, or a gate presents for e-signature, against that offer's registry (your declared deal.variables.* included), and one no offer references against the fixed whitelist alone. validate_draft is the answer to "would this publish?", and it returns every diagnostic at once, each with a JSON pointer into your document and a machine-readable code your agent can switch on:
# operationId: validate_draft
curl -X POST https://app.skipthesell.com/api/v1/drafts/{draft_id}/validate \
-H 'Authorization: Bearer <api key>'
The codes you will meet while writing templates:
template_placeholder_not_whitelisted— a{{a.b}}that is not on the whitelist; the message lists the namespaces.template_placeholder_invalid,template_placeholder_unclosed— a malformed substitution or directive.template_variable_undeclared— a{{deal.variables.<name>}}your rules document does not declare; the message lists the names it does.template_bool_variable— a bool variable referenced in a document. Use clause selection instead.template_collection_inline,template_collection_unknown,template_loop_field_unknown,template_loop_name_invalid— a collection used as a scalar, an unknown collection, an unknown loop field, or a loop named something other than the engine's fixed loop variable.template_clause_without_alternatives,template_alternative_unclosed,template_text_outside_alternative— a clause block the grammar cannot read.unknown_clause,clause_alternative_missing— a rule selects a clause no selectable template declares, or an alternative some declaring template does not offer.legal_member_missing— a template references aseller.*legal parameter yourlegalblock does not set. Legal members are optional until referenced; a referenced one is required at publish, and render fails closed on an empty binding rather than shipping a blank clause to a signature.
The same check runs again on publish, so what you validated is what ships.
Clauses and pre-approved alternatives#
A clause is a section of a template whose wording the buyer may pick from a fixed menu. In the template source it is three directives:
{{clause Support}}
{{alternative standard}}
Support: standard support, no uptime commitment.
{{end alternative}}
{{alternative enterprise}}
Support: enterprise support; response targets per the service level agreement at {{seller.sla_document}}.
{{end alternative}}
{{end clause}}
Alternatives are plain text with ordinary {{a.b}} placeholders — no loops, no nested clauses, no conditionals.
The menu is only offered if your rules can select from it. A rule with the clause effect picks the alternative for a clause name:
{
"id": "enterprise_support",
"phase": "lines",
"when": "plan == \"enterprise\"",
"effects": [
{"line": {"name": "Enterprise support", "kind": "flat_monthly", "amount": "price(\"support\")"}},
{"clause": {"name": "Support", "alternative": "enterprise"}}
]
}
That is how a buyer picks without escalating: the choice is an ordinary variable on the deal, and the rules turn the choice into a clause selection. The buyer sets the variable, and the quote tells them exactly what they will sign:
# operationId: update_deal
curl -X PATCH https://app.skipthesell.com/api/v1/deals/{deal_id} \
-H 'Authorization: Bearer <deal token>' \
-H 'Content-Type: application/json' \
-H 'If-Match: "<ETag from your read>"' \
-d '{"variables": {"plan": "enterprise"}}'
# operationId: create_deal_quote
curl -X POST https://app.skipthesell.com/api/v1/deals/{deal_id}/quotes \
-H 'Authorization: Bearer <deal token>' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: <generate a fresh value>'
The stored quote carries clauses[] — one {name, alternative, attribution} per clause selected, with the rule and inputs that selected it — beside the lines, discounts, gates and documents that make up the contract package. The buyer's agent can therefore enumerate what it is agreeing to before accepting.
Publish checks the menu's integrity: every template an offer can select that declares a clause must offer every alternative that can be selected for it. One selectable template missing an alternative is a clause_alternative_missing diagnostic at publish, because a clause map applies to the whole selected document set. A clause selection that is not in a template, or a document set where no selectable template declares the clause at all, is a render failure at signing time — so it is caught before any deal exists.
You can try the menu without a deal. A simulation runs a hypothetical deal against the draft with an explicit evaluation context and stores nothing:
# operationId: simulate_draft
curl -X POST https://app.skipthesell.com/api/v1/drafts/{draft_id}/simulations \
-H 'Authorization: Bearer <api key>' \
-H 'Content-Type: application/json' \
-d '{"offer": "svc", "context": {"currency": "USD", "term_months": 12, "event": {"kind": "start", "at": "2026-11-08"}, "variables": {"plan": "enterprise"}}}'
The simulation returns the quote — including the clause selections your rules made — so you can check both wording and price before publishing.
Redlines always escalate#
A change a buyer wants that is not one of your pre-approved alternatives is a redline. There is no knob for this: a redline always goes to a human, whoever sent it, and a buyer never uploads a modified document for signature or edits the rendered bytes in place. They sign your bytes, or they escalate.
An escalation is recorded on the deal, and a redline carries the contract and clause it refers to:
# operationId: escalate_deal
curl -X POST https://app.skipthesell.com/api/v1/deals/{deal_id}/escalations \
-H 'Authorization: Bearer <deal token>' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: <generate a fresh value>' \
-d '{"kind": "redline", "contract_id": "contract_<id>", "clause": "Support", "subject": "Support response times", "body": "<the change the buyer wants, in their own words>"}'
The escalation reaches your humans by signed webhook and by email to the recipients in your tenant settings, and stays on the record until it is answered and closed. subject and body are buyer-authored prose: rendered as data, never as instructions (the same rule your own template titles follow). The seller-side view is in the escalations guide; the buyer's side is in how a deal works.
The evidence a signed contract leaves#
When the buyer accepts a quote, the engine renders the selected document set from the pinned version and the accepted quote — deterministically, so the same version and quote give the same bytes. From then on the deal's contract is the system of record for both parties, in the sense how a deal works describes: signatures already given carry over nothing, and later configuration changes never touch it.
What is kept, and where to read it:
- The bytes.
get_contract_documentserves the canonical HTML exactly as stored, and itsETagis the content hash in quotes — you can verify what you got without a second call. - The hashes.
get_contractcarries each document'ssha256, thedocument_set_sha256every signature was made against, and the audit chain'schain_head_sha256. - The signature records. One append-only record per signature (
sig_…) insideget_contract: the party, the signing principal, the delegation when a buyer agent signed with a deal token, the typed name and title, the authority attestation, the consent it was made under, the document-set hash it bound, and the instant. Records are never edited. - The consent records. Scoped to principal, deal and consent-copy version; a contract shows every consent on file for it.
- The certificate of completion. Once the contract has formed,
get_contract_certificateserves the generated evidence artifact: every document hash, every signature record with its attribution, the consent records, the chain head and the engine's evidence statement, as canonical JSON whose own hash covers exactly those bytes. Before formation it is a 404 — a contract out for signature has an audit log, not a certificate. Read the log atlist_contract_events.
# operationId: get_contract
curl https://app.skipthesell.com/api/v1/contracts/{contract_id} \
-H 'Authorization: Bearer <deal token>'
# operationId: get_contract_certificate
curl https://app.skipthesell.com/api/v1/contracts/{contract_id}/certificate \
-H 'Authorization: Bearer <deal token>'
The events that announce all of this — deal.signed, contract.signature_added, contract.completed — are the webhooks guide's subject, including how to verify a delivery and replay one from the log.
Where next#
- How a deal works — what the buyer sees: quotes, gates, signatures and the deal room.
- Escalations — how a redline or a question reaches your humans, and how you answer it.
- Pricing rules primer — the
clauseanddocumenteffects in context, with worked examples. - Webhooks — the signing scheme, the events, retries and replay.