Hanzo AI

CRM

Your sales pipeline: the companies, the people, the deals in play.

Also for this capability: API · CLI · MCP · SDKs

Your sales pipeline: the companies, the people, the deals in play.

Base URLhttps://api.hanzo.ai
Operations20
AuthAuthorization: Bearer $HANZO_API_KEY

Specification

HIP-1120 · CRM — The Sales Pipeline — Draft · read the specification →

/v1/crm is an org's sales pipeline: companies, contacts and opportunities, plus the Startup Program intake that lands as a scored application. It is implemented in hanzoai/cloud at apps/crm. This HIP states the store, the two audiences the surface serves — staff behind the identity boundary and an anonymous applicant on one public form — and why exactly one route is not typed.

Motivation

A CRM contact is a prospect the org tracks, not a product user: the org's own users live in IAM, and the marketing subsystem resolves audiences from that roster, never from this table (apps/crm/crm.go:9-13). Without a capability that owns the prospect universe, prospect rows leak into user stores and the two contact universes join by accident — which is a privacy defect, not a modelling choice. This capability is the one place prospects live.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

§1 The store

crm owns one store: a single SQLite file named crm in the deployment's data directory (apps/crm/store.go:40, sqlpool.Open). Every org's rows share the file; isolation is the org column, which leads every uniqueness and lookup index so tenancy is a physical property of the index, not only a WHERE clause (apps/crm/store.go:56-59). The entity model follows Twenty's company / person / opportunity standard objects, with the composite fields flattened to scalar columns for SQLite (apps/crm/crm.go:5-8); no upstream code is imported for it.

§2 The addresses

Every route is under /v1/crm (manifest/apps.go:273). The CRUD over companies, contacts and opportunities, GET /v1/crm/summary, and the staff reads of applications are typed operations; each DELETE answers no body and so carries no response schema. One route is a raw handler and MUST stay one: POST /v1/crm/applications, the public Startup Program intake. Its rate limit and 64 KiB body cap are HTTP middleware, and the MCP and CLI projections of a typed op do not run middleware — typing it would publish an unmetered alias of a deliberately limited public endpoint (apps/crm/crm.go:200-207). Its prose is declared beside the wire fact instead (apps/crm/applications.go:130).

§3 Tenancy

Staff routes resolve the org from the validated principal (principal.Acting, apps/crm/crm.go:433), minted by the identity boundary (HIP-0026); a request the boundary refuses never reaches the store. The intake POST is the one unauthenticated route: it takes no principal and never reads a caller org — the application is filed against the deployment's own program org, so there is no tenant to name and none to leak. Re-submitting the same (email, company) refreshes the existing application rather than filing a second.

§4 Money, events, telemetry

crm is free, in those words: its plugin declares cloud.Free (plugin/crm/main.go:21) and crm is not in the metered set (spend.go:275). The AI screen a filed application receives runs on the deployment's own gateway credential, not the applicant's. crm publishes no events on the bus, and emits nothing to observability beyond the request span every route gets.

§5 Stage

crm is beta: a vertical application, not the agentic-OS core. The manifest row declares it (manifest/apps.go:273, Stage: Beta; HIP-0139 §8).

§6 Upstream

crm derives from no third-party code. The one third-party fact is the schema lineage stated in §1: the entity model mirrors Twenty's standard objects so a migration is a column mapping, and nothing of Twenty's implementation is in the tree.

Rationale

One shared file with a leading org index, rather than a file per org, because CRM rows are small and the summary read is a cross-table count within one org — the per-org-file pattern buys physical isolation at the cost of N file handles, and here the leading index gives the same fail-closed property for one handle. The intake staying raw, rather than teaching typed ops about middleware, keeps the typed registry's promise intact: every typed op is safe to project everywhere.

Security Considerations

The wrong implementation leaks the pipeline: every prospect, every deal and its amount, to any tenant that can name another's id. The gate is the org from the validated principal, never a client-supplied field, and every query filters on it. The intake is the other exposure — an unauthenticated write — and it is bounded three ways: IP rate limit, body cap, and upsert-on-resubmit, so an attacker can neither flood the store nor amplify a row.

Four surfaces

SurfaceReaches this capability asCoverage
RESTcrm at its own prefix20 operations
CLIhanzo crm …20 of 20
SDKno published client declares one yet — regenerating the clients is what adds them
MCPtool crm on https://api.hanzo.ai/v1/mcp20 operations, 1 under the document's own id — ask describe for the rest

Quickstart

export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys

Then the first call — a read that needs nothing but the key. GET /v1/crm/summary, operation get_crm_summary:

hanzo crm summary

Answers 200 with object — ok.

Endpoints

EndpointWhat it does
GET /v1/crm/applications/{id}Returns one Startup Program application with its AI screen and stage history.
PATCH /v1/crm/applications/{id}Moves one Startup Program application through the pipeline.
GET /v1/crm/applicationsReturns the org's Startup Program applications, newest first.
POST /v1/crm/applicationsApply to the Startup Program from the public form
GET /v1/crm/companies/{id}Returns one of the caller org's companies.
PUT /v1/crm/companies/{id}Replaces one of the caller org's companies.
DELETE /v1/crm/companies/{id}Removes one of the caller org's companies and answers 204.
GET /v1/crm/companiesReturns the caller org's companies, most recently updated first.
POST /v1/crm/companiesAdds a company to the caller's org and answers 201 with the stored record.
GET /v1/crm/contacts/{id}Returns one of the caller org's contacts.
PUT /v1/crm/contacts/{id}Replaces one of the caller org's contacts.
DELETE /v1/crm/contacts/{id}Removes one of the caller org's contacts and answers 204.
GET /v1/crm/contactsReturns the caller org's contacts, most recently updated first.
POST /v1/crm/contactsAdds a person to the caller's org and answers 201 with the stored record.
GET /v1/crm/opportunities/{id}Returns one of the caller org's deals.
PUT /v1/crm/opportunities/{id}Replaces one of the caller org's deals.
DELETE /v1/crm/opportunities/{id}Removes one of the caller org's deals and answers 204.
GET /v1/crm/opportunitiesReturns the caller org's deals, most recently updated first.
POST /v1/crm/opportunitiesAdds a deal to the caller's org and answers 201 with the stored record.
GET /v1/crm/summarySummary counts the caller org's CRM records: companies, contacts, opportunities.

All Hanzo APIs · Interactive reference

How is this guide?