Legal
Package legal is the paperwork your company needs, drafted, signed and filed.
Package legal is the paperwork your company needs, drafted, signed and filed.
| Base URL | https://api.hanzo.ai |
| Operations | 11 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1135 · Legal — Documents Drafted, Signed and Filed — Draft · read the specification →
/v1/legal is the paperwork an org needs, drafted, signed and filed: a
versioned, org-overridable library of standardized templates, a pure
merge-field engine that renders them from the org's own data, a sealed store
for the generated documents, and the e-signature and filing seams that carry a
document to execution. It is implemented in hanzoai/cloud at apps/legal
(HIP-0106).
Motivation
Formation and securities paperwork is templated work over data the platform
already holds — the company record, the cap table. What must never be templated
away is the boundary: the platform manages documents; it does not give legal
advice and does not determine that a document is valid or sufficient. That
boundary is a design invariant enforced in the engine and the data model, not a
disclaimer pasted on a page (apps/legal/model.go:10-18).
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The store and the renderer
One encrypted SQLite file, the deployment's own legal, opened through the one
opener so it is born encrypted (apps/legal/store.go:30) — a rendered contract
carries names and terms and is sealed on disk. The renderer is text/template
and pure: deterministic, no clock, no I/O, so the same inputs yield identical
bytes and a rendered contract is reproducible (apps/legal/model.go:20-24).
§2 The boundary in the model
There is no legally_valid state: a document is draft, out_for_signature,
signed or voided, nothing more. Formation and securities templates carry a
mandatory counsel-review notice the engine prepends to every rendered document —
the platform can never emit such a document without it
(apps/legal/model.go:13-18). A disclaimer rides on every generation and
template response.
§3 The address
Eleven operations under /v1/legal, all typed except one. The exception is
POST /v1/legal/documents/{id}/sign/complete, untyped by design: it discards
its decode error so a provider-reported completion still lands when the
callback body is unparseable — the typed layer refuses such a body before the
handler runs, so typing it would turn today's 200 into a 400
(apps/legal/legal.go:87-91). The provider's own status is checked first; an
explicit body signal exists for the stub (apps/legal/legal.go:170-182). A
1 MiB body cap sits in front of every route, registered before the typed ops
because a size check inside one would run after the parse it exists to precede
(apps/legal/legal.go:66-77).
§4 The seams fabricate nothing
E-signature and filing are provider-agnostic seams with honest stub defaults: a
seam with no real backend records an honest state and never fakes a completed
signature or a filed record (apps/legal/providers.go:10-16). Every generate,
sign and file action is recorded on the shared tamper-evident audit plane,
referencing opaque document ids (apps/legal/model.go:26-28).
§5 Tenancy, money, events, observability, stage, upstream
The tenant is principal.Org off the validated principal (HIP-0026), read back
from what the composer's bridge parked — never a header, never an In field
(apps/legal/legal.go:156). Free (cloud.Free, plugin/legal/main.go). It
publishes nothing on the bus and emits nothing beyond the request span; the
audit records above are the audit plane's, not telemetry. Stage beta: a
vertical application. It derives from no upstream — the renderer is the Go
standard library's text/template and the templates are authored here.
Rationale
The alternative to a pure renderer is one that reaches for live data at render time, which makes a contract unreproducible: the same request on two days yields two different documents and no record of why. Determinism is what makes the sealed store an archive rather than a cache. The alternative to honest stubs is refusing to mount without providers, which would couple a template library to two vendor contracts nobody needs on day one.
Security Considerations
A legal store is a disclosure surface — names, terms, equity — so documents are sealed at rest and the tenant is never an input. The execution path is where a wrong implementation does real-world harm: a seam that fabricated a "signed" or "filed" state would manufacture legal evidence, which is why the stubs are honest and completion consults the provider before any caller-supplied signal. The completion endpoint is org-scoped and moves only the document's status; it mints no provider record.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | legal at its own prefix | 11 operations |
| CLI | hanzo legal … | 11 of 11 |
| SDK | LegalApi in every published client | 11 methods |
| MCP | tool legal on https://api.hanzo.ai/v1/mcp | 11 operations, 1 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/legal/health, operation get_legal_health:
hanzo legal healthimport { Configuration, LegalApi } from 'hanzoai';
const api = new LegalApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getLegalHealth();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import LegalApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = LegalApi(client).get_legal_health()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.LegalAPI.GetLegalHealth(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, legal_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = legal_api::get_legal_health(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.LegalApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new LegalApi(client).getLegalHealth();curl https://api.hanzo.ai/v1/legal/health \
-H "Authorization: Bearer $HANZO_API_KEY"Tool legal, op get_legal_health — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "legal",
"arguments": {
"op": "get_legal_health",
"input": {}
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/legal/documents/{id}/sign/complete | Record that a generated document's signature request completed |
POST /v1/legal/documents/{id}/sign | Opens an e-signature request over one document and moves it to out_for_signature, returning the provider's reference for the request. |
GET /v1/legal/documents/{id} | Returns one of the org's documents WITH its rendered body. |
GET /v1/legal/documents | Returns the org's generated documents, newest first, WITHOUT their rendered content — fetch one document to read its body. |
POST /v1/legal/documents | Renders a document from a template and the caller's own merge data, seals it in the org's store, and returns it with its rendered body. |
GET /v1/legal/filings | Returns the org's filing records, newest first — which documents were filed where, through which provider, and what the filing's honest status is. |
POST /v1/legal/filings | Records a filing of one or more of the org's documents with a state or agency, and returns the tracking record. |
GET /v1/legal/health | Reports that the legal subsystem is serving and how many built-in templates its catalog carries. |
GET /v1/legal/templates/{id} | Returns one template resolved for the caller's org — the org's own override if it has saved one, else the built-in — with its full text/template body… |
PUT /v1/legal/templates/{id} | Saves the org's own version of a template — a custom NDA, a house MSA — and returns it with its new version number. |
GET /v1/legal/templates | Returns the org's effective template catalog: every built-in template, with any the org has overridden replaced by its own latest version. |
How is this guide?