Estimatic API docs
Everything you need to generate, refine, and push estimates from any language — plus the MCP server for AI agents. Base URL: https://api.estimatic.ai/v1
Quickstart
Five minutes from zero to your first priced estimate. Pick a language, paste a key, ship.
1. Get a key
Email developers@estimatic.ai or grab one from Settings → Developers inside the app. Keys are scoped to a single organization and prefixed sk_live_ or sk_test_.
2. Curl — first estimate
curl -X POST https://api.estimatic.ai/v1/estimates \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"prompt": "Replace 24 LF of cedar privacy fence, 6ft tall, with 2 gates.",
"trade": "fencing",
"project_zip": "30303"
}'3. Node SDK (preview — request access)
// npm i @estimatic/node
import { Estimatic } from "@estimatic/node";
const estimatic = new Estimatic({ apiKey: process.env.ESTIMATIC_KEY! });
const estimate = await estimatic.estimates.create({
prompt: "Master bath gut remodel, double vanity, heated floors",
trade: "remodel",
project_zip: "30303",
options: { good_better_best: true },
});
console.log(estimate.options.map(o => `${o.tier}: $${o.total_price}`));4. Python SDK (preview — request access)
# pip install estimatic
from estimatic import Estimatic
client = Estimatic(api_key=os.environ["ESTIMATIC_KEY"])
estimate = client.estimates.create(
prompt="Replace 30sq architectural shingle roof, tear-off, ice & water on eaves",
trade="roofing",
project_zip="30303",
)
print(estimate.totals.grand_total)5. MCP — hello world
{
"mcpServers": {
"estimatic": {
"url": "https://api.estimatic.ai/mcp",
"headers": { "Authorization": "Bearer sk_live_..." }
}
}
}Introduction
The Estimatic API turns any system you already run — a CRM, a lead form, an in-house AI agent — into an estimating powerhouse. Same engine, same local pricing, same one-click push that powers the Estimatic app.
Two surfaces, one engine
Use the REST API for traditional integrations. Use the MCP server when you want an AI agent (Claude, Cursor, ChatGPT, your own) to generate, refine, and push estimates as a tool. Both call the same backend, share the same auth, and emit the same webhooks.
Design principles
Predictable resource URLs. Verbs map to HTTP methods. Errors are structured JSON. Every mutating call accepts an Idempotency-Key. List endpoints use cursor pagination. We will never break a /v1/ contract — breaking changes ship behind /v2/.
Status
Currently in private beta. Request a key from your account manager or email developers@estimatic.ai. We onboard new partners weekly.
Authentication
All requests require a Bearer token scoped to a single organization. Generate keys from Settings → Developers. Treat them like passwords — server-side only, never in a mobile app or browser bundle.
Key scopes
Keys can be scoped at creation. estimates:read — list/get only. estimates:write — create, refine, archive. integrations:push — push to connected CRMs. webhooks:manage — create/delete webhooks. clients:write — create/update clients. Combine as needed; default new keys get estimates:read + estimates:write.
Multi-org partners
If you hold a partner key that can act on behalf of multiple orgs, pass X-Estimatic-Org: org_01HX... on every request. Without the header the request is rejected with permission_denied.
Key rotation
Rotate from Settings → Developers. The old key keeps working for 24 hours after rotation, giving you a window to deploy. We email every key holder 7 days before any forced rotation.
/v1/meVerify the current API key and return the org it belongs to.
curl https://api.estimatic.ai/v1/me \
-H "Authorization: Bearer sk_live_..."Errors
All errors return a consistent JSON shape. Read error.type — never parse the message string. HTTP status follows REST conventions.
Error envelope
{
"error": {
"type": "validation_failed",
"message": "project_zip must be a 5-digit US ZIP code.",
"param": "project_zip",
"request_id": "req_01JXYZ..."
}
}Error types
invalid_request (400) — malformed JSON or missing required field. authentication_failed (401) — bad or missing key. permission_denied (403) — key lacks the scope or org. not_found (404) — id doesn't exist or isn't visible to the caller. conflict (409) — resource already exists in a state that blocks the operation. validation_failed (422) — input parsed but failed business rules; check param. rate_limited (429) — slow down, see Retry-After. idempotency_replayed (200, header) — not an error, but the response was served from cache; check Idempotency-Replayed: true. upstream_unavailable (502) — a connected integration timed out; safe to retry. internal_error (500) — ours; we get paged, you can retry with backoff.
Retry policy
Retry 429, 502, 503, and 504 with exponential backoff (start at 1s, double, cap at 30s, max 5 attempts). Always retry with the same Idempotency-Key — never generate a new one for retries.
Rate limits & idempotency
Base URL & versioning
https://api.estimatic.ai/v1 — versioned in the URL. Breaking changes ship behind /v2/. Additive changes never break existing /v1/ clients.
Idempotency
Every POST accepts an Idempotency-Key header (any unique string up to 255 chars — UUID v4 recommended). Re-sending the same key within 24 hours returns the original response with header Idempotency-Replayed: true. Different request body + same key = 409 conflict.
Rate limits
60 req/min per key by default. Burst headroom of 20. Higher limits available on request — email developers@estimatic.ai with your expected volume.
Response headers
X-RateLimit-Limit — your ceiling. X-RateLimit-Remaining — what's left in the current window. X-RateLimit-Reset — Unix seconds until the bucket refills. Retry-After — only on 429, seconds to wait. Idempotency-Replayed — present and true when a cached response is returned. X-Request-Id — include this in any support email.
Estimates
The headline resource. One endpoint takes a plain-English prompt (plus optional files, ZIP, trade) and returns a fully-priced estimate with good/better/best options.
/v1/estimatesIdempotentGenerate a new estimate from a prompt, files, or both.
curl -X POST https://api.estimatic.ai/v1/estimates \
-H "Authorization: Bearer sk_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Master bath gut remodel...",
"project_zip": "30303",
"options": { "good_better_best": true }
}'Request fields
| Field | Type | Description |
|---|---|---|
promptrequired | string | Plain-English scope of work. The more detail, the better the estimate. Include trade, dimensions, materials, and any special conditions. |
trade | string | Hint for trade routing. Inferred from prompt if omitted.remodelroofingfencingelectricalplumbinghvaclandscapinggeneral |
project_address | string | Full street address. Used for permit lookup and accurate local pricing. |
project_zip | string | 5-digit US ZIP. Drives local labor and material pricing. Strongly recommended. |
files | array<File> | Attached plans, photos, LiDAR scans, or reports. Each item is { url, kind }. Upload via POST /v1/files first. |
client_id | string | Attach the estimate to an existing client. Optional. |
pricing_profile_id | string | Override the org default pricing profile (markups, supplier preferences, rounding). |
options.good_better_best | boolean | Generate three priced tiers. Default true. |
options.upsells | boolean | Include suggested add-on line items (heated floors, smart fixtures, etc.). Default false. |
options.local_permits | boolean | Look up and add jurisdictional permit fees. Default true. |
Response fields
| Field | Type | Description |
|---|---|---|
id | string | Unique estimate id. Prefix est_. |
title | string | Auto-generated from the prompt; editable via PATCH. |
scope_summary | string | 1–2 sentence client-presentable summary of the scope. |
groups | array<Group> | Line items grouped by phase (Demolition, Framing, Finishes, etc.). |
options | array<Option> | Good/Better/Best tiers. Each has tier, name, total_price, highlights[]. |
totals | object | Aggregated math: subtotal, labor, materials, permits, overhead, profit, tax, grand_total. |
is_demo | boolean | True for sandbox/demo responses; never invoiceable. |
- Average response time: 8–14 seconds. Use POST /v1/estimates:stream for token-by-token progress.
- Local pricing is resolved from project_zip. Falls back to org default if omitted.
- files[].url must be an Estimatic-hosted URL — upload first via POST /v1/files.
/v1/estimates:streamIdempotentSame as POST /v1/estimates, but streams progress as Server-Sent Events.
curl -N -X POST https://api.estimatic.ai/v1/estimates:stream \
-H "Authorization: Bearer sk_live_..." \
-H "Accept: text/event-stream" \
-H "Content-Type: application/json" \
-d '{"prompt":"Re-roof 30sq architectural","project_zip":"30303"}'- Event types: status, line_item, option, total, done, error.
- Always emits exactly one done or error event. Close the stream after either.
/v1/estimatesList estimates, newest first. Supports cursor pagination.
curl "https://api.estimatic.ai/v1/estimates?limit=20&status=draft" \
-H "Authorization: Bearer sk_live_..."- Query params: limit (1–100, default 20), cursor, status (draft|sent|accepted|declined|archived), client_id, created_after (ISO8601), created_before.
/v1/estimates/{id}Retrieve a single estimate by id.
curl https://api.estimatic.ai/v1/estimates/est_01JXYZ \
-H "Authorization: Bearer sk_live_..."/v1/estimates/{id}Programmatic edits to line items — rename, change qty/unit_price, reorder, add custom lines. Use refine() for natural-language edits.
{
"ops": [
{ "op": "update_item", "group": "Demolition", "item_id": "li_01...", "fields": { "quantity": 16 } },
{ "op": "add_item", "group": "Permits", "item": { "kind": "fee", "name": "City of Atlanta permit", "quantity": 1, "unit_price": 450 } },
{ "op": "remove_item", "item_id": "li_07..." }
]
}- ops are applied in array order, atomically — if any op fails, the whole request rolls back.
- op types: update_item (mutate fields on an existing line), add_item (append a new line to a group), remove_item (delete a line), update_group (rename or reorder a group), set_title, set_status.
- update_item.fields can include: name, description, quantity, unit, unit_cost, unit_price, taxable, notes.
/v1/estimates/{id}/refineIdempotentChat-style refinement. The agent re-prices and returns the updated estimate.
curl -X POST https://api.estimatic.ai/v1/estimates/est_01JXYZ/refine \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"message":"Add a permit line for the city of Atlanta."}'/v1/estimates/{id}/pushIdempotentPush a finished estimate into a connected CRM or accounting system. Same field-mapping the Chrome extension uses.
curl -X POST https://api.estimatic.ai/v1/estimates/est_01JXYZ/push \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"integration":"servicetitan","target":"estimate"}'/v1/estimates/{id}/sendIdempotentEmail or SMS the estimate to the customer with a signable share link.
{
"channel": "email",
"to": "sarah@example.com",
"subject": "Your master bath estimate",
"message": "Hi Sarah — here's the estimate we discussed. Tap to review and sign.",
"require_signature": true
}Request fields
| Field | Type | Description |
|---|---|---|
channelrequired | string | Delivery channel.emailsmsboth |
to | string | Override recipient. Defaults to the attached client's primary contact. |
subject | string | Email subject. Ignored for SMS. |
message | string | Custom message body. A default branded template is used if omitted. |
require_signature | boolean | Require the customer to e-sign before accepting. Default true. |
/v1/estimates/{id}/duplicateIdempotentClone an estimate as a fresh draft. Useful for templating recurring jobs.
{ "title": "Master bath — Greene residence v2", "client_id": "cli_01HZ..." }/v1/estimates/{id}/statusManually transition an estimate's lifecycle status.
{ "status": "accepted", "selected_option": "better", "note": "Signed at site visit." }Request fields
| Field | Type | Description |
|---|---|---|
statusrequired | string | Target status. Must be a valid forward transition.draftsentaccepteddeclinedarchived |
selected_option | string | Which tier the customer chose. Required when status=accepted.goodbetterbest |
note | string | Internal note attached to the status change. |
/v1/estimates/{id}Archive an estimate. Soft-delete — restorable for 30 days from the app.
curl -X DELETE https://api.estimatic.ai/v1/estimates/est_01JXYZ \
-H "Authorization: Bearer sk_live_..."Files
Upload plans, site photos, LiDAR scans, Hover/EagleView reports, or PDFs. The returned URL is what you pass into files[] on POST /v1/estimates.
/v1/filesUpload a file. Multipart/form-data. 50 MB cap per file.
curl -X POST https://api.estimatic.ai/v1/files \
-H "Authorization: Bearer sk_live_..." \
-F "kind=plan" \
-F "file=@./floorplan.pdf"- Returned URL is a signed, single-org URL valid for 24 hours. Attach it to an estimate before it expires — once attached, it's persisted.
- Allowed mime types: application/pdf, image/jpeg, image/png, image/heic, model/gltf-binary, application/octet-stream (LiDAR).
/v1/files/{id}Retrieve file metadata and a fresh signed URL.
Contacts (leads)
Inbound leads — people who filled out a form, called in, or were captured by your CRM but haven't been qualified into a paying client yet. Convert a contact to a client when you're ready to send them an estimate.
/v1/contactsIdempotentCreate a lead. Idempotent on (email|phone) + source within 24h.
{
"name": "Marcus Webb",
"email": "marcus@example.com",
"phone": "+14045550199",
"source": "website_form",
"utm": { "source": "google", "medium": "cpc", "campaign": "bath-remodel-atl" },
"scope_summary": "Wants to gut master bath, asked about heated floors.",
"address": { "line1": "812 Peachtree", "city": "Atlanta", "state": "GA", "postal_code": "30303" },
"custom_fields": { "budget_band": "$25-40k", "timeline": "next 60 days" }
}Request fields
| Field | Type | Description |
|---|---|---|
namerequired | string | Full name. |
email | string | At least one of email or phone is required. |
phone | string | E.164 format preferred (+14045550199). |
source | string | Where the lead came from. Free-form, but common values are conventional.website_formphone_callreferralgoogle_lsafacebookyelpangimanual |
utm | object | UTM tracking object: { source, medium, campaign, term, content }. |
scope_summary | string | What the lead asked for. Free-form notes from the intake call/form. |
address | object | Project address. line1, line2, city, state, postal_code. |
custom_fields | object | Arbitrary key-value bag for org-defined metadata. |
/v1/contactsList contacts with filters and search.
curl "https://api.estimatic.ai/v1/contacts?status=new&source=google_lsa&limit=50" \
-H "Authorization: Bearer sk_live_..."- Query params: status (new|qualified|converted|lost|spam), source, q (search name/email/phone), created_after, created_before, limit (1–100, default 20), cursor.
/v1/contacts/{id}Retrieve a contact by id, including any draft estimates linked to them.
/v1/contacts/{id}Update a contact — most often used to change status or append notes.
{ "status": "qualified", "notes": "Spoke 5/6, scheduling site visit Tuesday." }/v1/contacts/{id}/convertIdempotentPromote a contact to a client. Returns the new client_id; the contact stays in place with status=converted.
{ "create_estimate": true, "estimate_prompt": "Master bath gut remodel, double vanity, heated floors." }- If create_estimate is true, an estimate is generated using the contact's scope_summary as the prompt fallback.
/v1/contacts/{id}Soft-delete a contact. Restorable for 30 days.
Clients
The customer the estimate is for. A converted contact becomes a client. Estimates can also be drafted without a client and attached later.
/v1/clientsIdempotentCreate a client record.
{
"name": "Sarah Greene",
"email": "sarah@example.com",
"phone": "+14045551212",
"address": { "line1": "1428 Elm Street", "city": "Atlanta", "state": "GA", "postal_code": "30303" }
}Request fields
| Field | Type | Description |
|---|---|---|
namerequired | string | Full legal/billing name. |
email | string | Primary email. Used as the default recipient for sent estimates. |
phone | string | Primary phone. E.164 preferred. |
address | object | Billing address: line1, line2, city, state, postal_code, country. |
company | string | Optional company name for B2B clients. |
tax_exempt | boolean | Skip tax on estimates for this client. Default false. |
/v1/clientsList clients with search and filters.
curl "https://api.estimatic.ai/v1/clients?q=greene&limit=20" -H "Authorization: Bearer sk_live_..."- Query params: q (search name/email/phone), created_after, created_before, limit (1–100, default 20), cursor.
/v1/clients/{id}Retrieve a client by id.
/v1/clients/{id}Update any client field. Partial — only send what's changing.
{ "phone": "+14045559999", "tax_exempt": true }/v1/clients/{id}Soft-delete a client. Linked estimates remain but become unattached.
/v1/clients/{id}/estimatesList every estimate for a client, newest first.
curl "https://api.estimatic.ai/v1/clients/cli_01HZ/estimates?status=accepted" \
-H "Authorization: Bearer sk_live_..."Pricing profiles
Your cost book. Markup rules, labor burden, supplier preferences, and rounding. Every estimate uses one — defaults to the org default.
/v1/pricing-profilesList pricing profiles for the org.
/v1/pricing-profilesCreate a new pricing profile.
{
"name": "Premium remodels",
"labor_markup": 1.45,
"material_markup": 1.25,
"preferred_suppliers": ["abc_supply", "qxo"],
"rounding": "nearest_25"
}Integrations
List the CRMs, FSMs, and accounting tools connected to the org. Use these slugs in /estimates/{id}/push.
/v1/integrationsList connected integrations.
{
"object": "list",
"data": [
{ "slug": "jobber", "name": "Jobber", "status": "connected" },
{ "slug": "servicetitan", "name": "ServiceTitan", "status": "connected" },
{ "slug": "housecall_pro", "name": "Housecall Pro", "status": "disconnected" },
{ "slug": "quickbooks", "name": "QuickBooks", "status": "connected" }
]
}Webhooks
Get notified when estimates are created, updated, pushed, sent, viewed by your customer, accepted, or declined. Payloads are HMAC-SHA256 signed.
Event catalog
estimate.created — fires after a successful POST /v1/estimates. estimate.updated — any field change (incl. PATCH). estimate.refined — natural-language refinement applied. estimate.pushed — sent to a connected CRM/accounting tool. estimate.sent — delivered to the customer (email/SMS). estimate.viewed — customer opened the share link. estimate.accepted — customer signed/accepted. estimate.declined — customer declined.
Example payload — estimate.viewed
{
"id": "evt_01JXYZ...",
"type": "estimate.viewed",
"created_at": "2026-05-06T14:22:08Z",
"data": {
"estimate_id": "est_01JXYZ...",
"viewer_ip": "73.12.x.x",
"user_agent": "Mozilla/5.0 ...",
"first_view": true
}
}Example payload — estimate.accepted
{
"id": "evt_01JXYZ...",
"type": "estimate.accepted",
"created_at": "2026-05-06T15:01:22Z",
"data": {
"estimate_id": "est_01JXYZ...",
"selected_option": "better",
"signer_name": "Sarah Greene",
"signed_at": "2026-05-06T15:01:21Z"
}
}Verifying signatures (Node / Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post(
"/hooks/estimatic",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("x-estimatic-signature") ?? "";
const ts = req.header("x-estimatic-timestamp") ?? "";
const expected = crypto
.createHmac("sha256", process.env.ESTIMATIC_WEBHOOK_SECRET!)
.update(`${ts}.${req.body}`)
.digest("hex");
const ok =
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.status(401).end();
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// ... handle event ...
res.status(200).end();
}
);Verifying signatures (Python / Flask)
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["ESTIMATIC_WEBHOOK_SECRET"].encode()
@app.post("/hooks/estimatic")
def hook():
sig = request.headers.get("x-estimatic-signature", "")
ts = request.headers.get("x-estimatic-timestamp", "")
body = request.get_data() # raw bytes — do not parse first
expected = hmac.new(SECRET, f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
if abs(time.time() - int(ts)) > 300:
abort(401)
event = request.get_json()
# ... handle event ...
return "", 200/v1/webhooksSubscribe a URL to one or more events.
{
"url": "https://yourapp.com/hooks/estimatic",
"events": ["estimate.created", "estimate.pushed", "estimate.viewed"],
"description": "Sync estimates to internal pipeline"
}- Save the secret on creation — it's only shown once.
/v1/webhooksList webhook subscriptions.
/v1/webhooks/{id}Unsubscribe a webhook.
curl -X DELETE https://api.estimatic.ai/v1/webhooks/wh_01 \
-H "Authorization: Bearer sk_live_..."MCP server
The Estimatic MCP server exposes the same operations as tools to any MCP-compatible AI client — Claude Desktop, Cursor, ChatGPT (with MCP), or your own agent. Authenticate with the same Bearer token as the REST API. One config block, your AI can now scope, price, and push real jobs.
Endpoint & transport
https://api.estimatic.ai/mcp — Streamable HTTP transport per the MCP spec. Every request must send Authorization: Bearer sk_live_... and Accept: application/json, text/event-stream. The server responds with either JSON or an SSE stream depending on the tool call.
Authentication
Same Bearer key as the REST API, scoped to one org. For multi-org partners, set X-Estimatic-Org on the MCP transport headers — your AI client must support custom headers (Claude Desktop, Cursor, and the OpenAI Agents SDK all do).
Tools — overview
create_estimate, list_estimates, get_estimate, refine_estimate, patch_estimate, archive_estimate, push_estimate, create_client, list_clients, list_integrations, list_pricing_profiles, upload_file. Arguments and return shapes mirror the REST endpoints above.
Tool — create_estimate
{
"name": "create_estimate",
"description": "Generate a fully-priced estimate from a natural-language scope and (optionally) attached files.",
"inputSchema": {
"type": "object",
"required": ["prompt"],
"properties": {
"prompt": { "type": "string" },
"trade": { "type": "string", "enum": ["remodel","roofing","fencing","electrical","plumbing","hvac","landscaping","general"] },
"project_zip": { "type": "string", "pattern": "^[0-9]{5}$" },
"client_id": { "type": "string" },
"file_ids": { "type": "array", "items": { "type": "string" } },
"options": {
"type": "object",
"properties": {
"good_better_best": { "type": "boolean", "default": true },
"upsells": { "type": "boolean", "default": false },
"local_permits": { "type": "boolean", "default": true }
}
}
}
}
}Tool — refine_estimate
{
"name": "refine_estimate",
"description": "Apply a natural-language change to an existing estimate and re-price.",
"inputSchema": {
"type": "object",
"required": ["estimate_id", "message"],
"properties": {
"estimate_id": { "type": "string" },
"message": { "type": "string" }
}
}
}Tool — push_estimate
{
"name": "push_estimate",
"description": "Push a finished estimate into a connected CRM or accounting system.",
"inputSchema": {
"type": "object",
"required": ["estimate_id", "integration"],
"properties": {
"estimate_id": { "type": "string" },
"integration": { "type": "string", "enum": ["jobber","servicetitan","housecall_pro","quickbooks"] },
"target": { "type": "string", "enum": ["quote","estimate","invoice"], "default": "quote" },
"client_id": { "type": "string" }
}
}
}Setup — Claude Desktop
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"estimatic": {
"url": "https://api.estimatic.ai/mcp",
"headers": {
"Authorization": "Bearer sk_live_..."
}
}
}
}Setup — Cursor / VS Code
// .cursor/mcp.json (or VS Code MCP settings)
{
"mcpServers": {
"estimatic": {
"url": "https://api.estimatic.ai/mcp",
"headers": { "Authorization": "Bearer sk_live_..." }
}
}
}Setup — ChatGPT (Custom Connector)
In ChatGPT → Settings → Connectors → Add custom connector. Name: Estimatic. URL: https://api.estimatic.ai/mcp. Auth: Bearer token, paste your sk_live_ key. Save, then enable it in any chat. Ask the model: 'Use the estimatic connector to draft a roof replacement estimate for ZIP 30303, 30sq architectural shingles.'
Setup — OpenAI Agents SDK (Python)
# pip install openai-agents
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
estimatic = MCPServerStreamableHttp(
name="estimatic",
params={
"url": "https://api.estimatic.ai/mcp",
"headers": {"Authorization": f"Bearer {os.environ['ESTIMATIC_KEY']}"},
},
)
agent = Agent(
name="EstimatorBot",
instructions="You draft contractor estimates. Always call estimatic.create_estimate.",
mcp_servers=[estimatic],
)
result = Runner.run_sync(agent, "Quote a 24 LF cedar privacy fence in ZIP 30303.")
print(result.final_output)Prompting tips
1) Be explicit about the tool: 'Use estimatic.create_estimate to scope this bath remodel...' — vague prompts often skip the tool entirely. 2) Always pass project_zip — without it, pricing falls back to org default and accuracy drops 5–15%. 3) Chain refinements: 'Now use estimatic.refine_estimate to add a permit line for the city of Atlanta.' rather than re-creating from scratch.
Changelog
Stability promise
Additive changes (new fields, new endpoints, new event types) are non-breaking and ship continuously to /v1/. Breaking changes ship behind /v2/ — never as a quiet change to /v1/. Deprecations get a 90-day email notice to every key holder.
v1.0 — public beta (target Q3 2026)
First stable release of /v1/estimates, /v1/clients, /v1/files, /v1/pricing-profiles, /v1/integrations, /v1/webhooks, and the MCP server.
Pre-beta
Private partners get early access via direct integration with our team. Endpoints are stable but breaking changes are still possible — we'll email every key holder before any.
Ready to build?
Grab a key, fire your first request, and tell us what you're shipping. We read every email at developers@estimatic.ai.