# Working with agents (/dev/agents)
Connect an AI assistant to Estii and it can work with the same commercial model
your team uses: deals, scope, rates, resources, milestones, approvals, comments,
versions, forecasts, and your library of previously sold work.
You don't need to talk in ids. Use the names your team uses. Ask follow-up
questions. The best results usually come from a short conversation rather than a
single perfect prompt.
If the assistant is new to your space, ask it to get its bearings before it
writes or changes anything. It can inspect the structure of a deal, understand
the labels you use for phases and sections, see which resources an estimate is
priced against, and tell you when Estii refuses a change.
*These examples assume you have connected an assistant through Estii's MCP
server. The assistant works through Estii's CLI, so it can read live Estii data
and, where your permissions allow, change it. Review proposed changes before
applying them, especially on active deals.*
## First, ask what is there [#first-ask-what-is-there]
Start with the same questions you would ask a teammate before a call.
> Give me a summary of the ACME Customer Portal deal.
You get the deal value, cost, margin, owner, stage, dates, and account context.
> How is that split across phases?
The assistant breaks the deal down by phase, with duration, people, price, cost,
and margin contribution.
> Anything unusual in there?
It looks for where the commercial weight sits: a phase carrying most of the
value, a section running at a thinner margin than the rest, or a large payment
milestone that changes the cash shape of the deal.
> When did anyone last touch it?
It returns the last change date and current stage, so you know whether you are
looking at fresh work or something that has been sitting for a while.
## Understand the price [#understand-the-price]
When the number is already on the table, ask what is driving it.
> Break down the ACME Customer Portal price by category.
You get each category and section, with scope, effort, cost, price, and margin.
> Which parts are dragging the margin down?
The assistant shows the lower-margin parts and the size of the drag.
> What is the resourcing behind the build phase?
It shows who is on the phase, how much of each resource is allocated, and which
rates are being used.
> How much senior engineering is in this deal in total?
It reads the estimates themselves rather than guessing from a phase-level
summary.
## Get a picture of a deal to put in front of someone [#get-a-picture-of-a-deal-to-put-in-front-of-someone]
The same answers work as pictures. A chart is what goes in a board pack, a
quarterly review, or an email to a client, and asking for one is no different
from asking for the number.
> Chart the ACME Customer Portal price by category.
You get the deal's own groupings drawn as a division of the total, largest first.
Ask for it by phase, by section, or by whichever grouping you use instead.
> Which work is big but not worth much? Plot effort against price.
Every piece of work in the deal, placed by the effort behind it against what it
sells for. The ones carrying days and very little price are where your margin is
going.
> Show me how the deal is resourced across its phases.
Each phase on the dates it actually runs, with the people it calls for. Phases
that sit apart, or that are pinned to a fixed date, appear where they really are
rather than end to end.
> And what the money looks like month by month.
The payment milestones as they fall, so you can see when cash arrives rather than
what the deal adds up to.
A picture reads as complete in a way a table does not, so it is worth knowing
what these leave out. Work you have taken out of scope keeps its price in the
underlying numbers, so a division of the price can add up to slightly more than
the deal you are quoting; ask for the scoped figures if that difference matters.
And work priced without effort behind it, a recurring charge or a fixed cost, has
no size to plot: the assistant names it rather than drawing it as nothing.
An artifact takes your own appearance. Unless you say otherwise it uses your
space's theme, the colours and fonts your customers already see on your
proposals, so something built for your client carries your brand rather than
Estii's. Anything you ask for yourself wins over that. It also carries a line
saying which deal it was built from, when, and where to find it, so whoever reads
it later can trace a figure back. Ask for that line to be left off and it is.
## Check capacity and revenue across the pipeline [#check-capacity-and-revenue-across-the-pipeline]
Some questions are not about one deal. They are about what all open work adds up
to over the next few months.
> What does our capacity look like over the next six months?
You get week-by-week demand in full-time equivalents across three bands:
* won work
* won work plus open deals weighted by probability
* won work plus every open deal at full weight
Those bands give you a floor, a likely picture, and a ceiling.
> Which roles is that concentrated in, and when does it peak?
The same demand is split by role, with the peak week named for each role. This is
demand, not availability. Estii does not hold your headcount, so a peak tells you
where work piles up, not whether you have enough people.
> Which deals are driving that peak?
The assistant splits the same window by deal, so the few deals causing the spike
come to the top.
> What does revenue recognition look like over the next twelve months?
Revenue is reported month by month from payment milestones, not from deal totals.
Longer windows use months rather than weeks.
> Only the approved work, and only the delivery team.
Filters narrow the same picture by deal, role, resource tag, or stage.
> That March peak is too high. What if Northwind started a month later?
The assistant previews the new start date, what would move with it, and whether a
version will be saved first. A deal past draft is versioned before its dates
change, so the approved shape stays recoverable.
Forecast answers say when they were calculated. If a deal has changed since the
forecast was calculated, the assistant tells you rather than presenting stale
figures as current.
## Rescope a deal [#rescope-a-deal]
Shaping a deal is deciding what stays in, what comes out, and what the change
does to the commercial shape.
> In the ACME Customer Portal, which items could come out to save $40K?
The assistant values each item against what is currently in scope. If something
is already partly out, it is described that way rather than counted twice.
> Take the reporting module out of phase one.
The change is previewed first. The line items under the module move with it,
instead of being left behind.
> What is the deal worth now?
You get the new price, cost, margin, and schedule.
> Put it back.
The same scope returns, including any line items that were already out before
this change started.
For bigger edits, save a version before you begin.
> Save a version of ACME Customer Portal first, call it "before rescope".
That gives you a restore point.
> Drop the second training workshop, and add five days of senior dev to
> discovery.
Each change is previewed, then applied once you approve it.
> What did that do to the margin?
The assistant compares the new totals with where the deal started.
> And the delivery dates?
Changing effort moves the schedule, so the assistant reports the new end date.
> Actually, revert all of it.
The deal is restored from the version you saved.
## Close a gap between your price and the customer's budget [#close-a-gap-between-your-price-and-the-customers-budget]
This is often the reason to open a deal at all.
> They have $280K. We are at $340K. What are the options?
The assistant works from your live rates. It can compare discounts, descoping,
resource swaps, and the margin impact of each option.
> What if the discovery work went to a mid-level engineer rather than a senior?
It prices the swap against your rate card without touching the deal.
> Try a 10% discount instead.
The discount is previewed before it is added. Once approved, the deal reports
both numbers: the price the scope adds up to and the price the customer sees
after discount, each with its own margin.
> That takes us under 35% margin. Take it off and drop the second training
> workshop instead.
The discount comes off. The scope change is previewed the same way, so you can
compare the two options on real numbers.
## Start a deal [#start-a-deal]
When a new opportunity has no scope yet, start from something that already has a
shape.
> What can we start a new deal from?
The assistant lists your templates and what each one creates: categories,
sections, and the work seeded under them.
> Start one for Northwind off the Delivery template. Call it "Northwind Phase 1".
The deal is created with its first phase, category, and section, so there is
somewhere to put work straight away. If the account does not exist yet, Estii
creates it and tells you.
> Actually base it on the ACME Customer Portal deal instead, but leave the
> numbers behind.
The structure is copied across and the estimates are stripped out, so you keep
the shape and re-estimate it for the new customer.
## Turn a proposal into a deal [#turn-a-proposal-into-a-deal]
If the scope already exists in a spreadsheet, proposal, RFP, or SOW, hand the
file over instead of typing it back in.
> Here is the RFP from Northwind. Build me a deal from it.
The file is uploaded and composition starts. Composition takes a few minutes, so
the assistant tells you it is running and comes back when it is done. You get the
same deal Estii would build from the app.
> Tell it the optional phases should be priced too.
You can give the assistant the same steer the app asks for during import. The
instruction is carried into composition rather than applied afterwards.
> How far has it got?
The assistant shows each step and what it produced. If the composition went
somewhere unexpected, you can see where. If you stop it, the uploaded source is
discarded. If it fails, the source is kept so you can inspect the failure.
> Actually, we are at our deal limit on this plan.
Estii refuses before anything runs. The same is true if composing is switched off
for the space, or if the space has already used its hourly composition allowance.
## Build scope line by line [#build-scope-line-by-line]
You can also build directly inside an existing deal.
> Set up the Discovery phase on ACME Customer Portal: a workshop line item, a
> requirements one, and five days of senior dev on each.
The assistant creates the phase, sections, line items, and estimates in order.
Effort is stored in the unit you used, so five days is not quietly stored as a
number with no unit attached.
> Now move the requirements one above the workshop, and copy the whole phase for
> phase two.
Reordering is treated as a move. Copying a phase takes its contents with it, so
phase two starts as a full copy and can be edited down.
## Use work you have already sold [#use-work-you-have-already-sold]
Your library holds reusable work, named and grouped, with low, typical, and high
estimates drawn from deals you have won.
> What have we got in the library for taking card payments?
The assistant returns matching items, the confidence of each match, and why it
matched. Each item includes its price range at your current rates, plus a grade
for how proven and current it is.
> Tell me what the Stripe one actually covers.
You get the tasks in order. Each task shows low, typical, and high amounts, and
how many deals stand behind them. If an amount was typed by hand rather than
observed from past deals, it is marked that way.
> Put it into ACME Customer Portal under Integrations, at the high end.
The line item, tasks, and estimates are previewed before they land. If the
library item knows where it belongs, the assistant tells you where it chose. If
it does not, it asks.
> One of those lines wants a Cloud Engineer and we do not have one on this deal.
Nothing is written. The assistant names the line and shows the resources the deal
does hold, so you can map it to the right one or leave it out.
> That Stripe estimate is out of date, it is nearer five days now.
Library corrections are previewed before they land. The amount must include a
unit, so a figure meant as days is not stored as hours. A corrected amount is
marked as typed by hand, which stops the next won deal from averaging it back out.
> Anything in the library waiting on me?
The assistant shows items waiting for review, with a link to each one. Accepting,
declining, and deleting library work stays in the app because those actions are
judgements about evidence. Deleting archives an item rather than removing it
outright.
## Build a library from old proposals [#build-a-library-from-old-proposals]
If your library is thin, your old proposals are often the best source material.
> Here are twenty SOWs we have delivered. Work out what we sell and put it up for
> me to look at.
Nothing is added to the live library straight away. Estii creates review items:
each piece of work it found, named, with tasks and line estimates, priced at your
rates so you can inspect it before accepting it.
Until you accept an item, it cannot be found, matched, or added to a deal. That
makes it safe to run against a library your team already uses.
> Where does it think all this belongs?
Each review item shows where it wants to be filed. If it needs a group you do not
have yet, the group is not created until you approve it. If it looks like work
you already have, Estii shows the matching item and offers to fold the new
evidence into it.
> Fine, take the lot, but put the reporting ones together.
You can accept items one at a time, choose which tasks and lines to keep, and
decide where each goes. Or you can accept a selection in one go and send them all
to the same place. Anything declined is discarded and will not come back.
## Inspect a line before changing it [#inspect-a-line-before-changing-it]
For smaller changes, ask what an item is worth before you edit it.
> Which line items on ACME Customer Portal carry the most value?
The assistant lists line items in deal order, with units and value.
> What is actually in the integrations one?
It shows that line item on its own, including everything under it and what it
comes to.
> Some of those have a plus next to the days. What does that mean?
The number covers most of what sits under the line, but not everything. For
example, licences may sit beside effort. Estii says what was left out instead of
folding unlike units into one figure.
> Now drop it to five days.
The change is previewed against the current value, then applied once you approve
it.
## Set up or fix the things you price from [#set-up-or-fix-the-things-you-price-from]
Deals are priced from roles, products, streams, rate cards, and tags. You can ask
the assistant to inspect or change those too.
> Add a Senior Developer role, $700 a day to us and $1,400 to the customer.
The role is added to every rate card you hold. A card you have not named takes
the rate implied by its own margin band. If a figure belongs to a specific card,
say which one.
> Premium Support should run at 45% margin, and cheaper above fifty seats.
The assistant previews the product's volume table before and after, including the
tier that takes over above fifty seats and the price movement it causes.
> What is the Delivery Pod made of?
You get the roles it allocates and how much of each. A stream has no rate of its
own. Its cost and price come from the allocations, so changing an allocation
moves both.
> We do not sell Junior Developer any more. Take it out.
Estii will not delete a resource while other work is priced against it. The
assistant names the streams that would be recomposed and asks what should replace
the role. Once you choose, it repoints those streams and removes the role.
Changing a resource needs the manager role. It does not reprice existing deals on
its own. Each deal holds its own copy and takes the update only when someone
decides it should.
## Push a rate change through open deals [#push-a-rate-change-through-open-deals]
Putting a rate up does not quietly reprice work you have already quoted. Estii
shows you what would change first.
> Put the senior engineer day rate up to $1,400.
The assistant shows the current rate and the new rate before applying the change.
If you use more than one rate card, say which card it lands on. Also say whether
the amount is your cost or the customer price.
> Which of our open deals are still on the old rate?
You get the deals that are behind and how long they have been behind.
> What would updating ACME Customer Portal do to it?
The assistant previews every change and the movement in total price and margin.
> Do it for all of them.
Each deal is updated in turn, with changes shown first. Only draft deals can take
the update. Deals already out for approval or signature are reported as not
updated, so quoted work does not move without someone noticing.
> Anything you could not update?
If a change needs judgement, such as mapping a retired resource to a replacement,
the assistant stops on that deal and gives you a link to sort it out in the app.
## Run a pipeline review [#run-a-pipeline-review]
Use these questions when you want to know where deals stand. For demand over
time, use capacity and revenue forecasts.
> What is in the pipeline right now, by value and probability?
Every open deal, with stage, value, and margin.
> What have we won this quarter, and at what margin?
The same view, filtered to closed-won.
> Which deals have not been touched in two weeks?
The stale deals, oldest first.
> Which ones are waiting on someone to approve them?
The deals sitting with an approver, and who they are sitting with.
> Send ACME Customer Portal to Sam for approval.
The assistant confirms the move first, then submits it and notifies Sam.
> Close the Northwind deal as won. We signed the SOW yesterday.
It confirms the stage change and records the reason.
## Pick up comment threads [#pick-up-comment-threads]
> What has Sam commented on in the ACME deal?
Sam's comments, with the thing each comment is attached to.
> Anything still unresolved?
The open threads.
> What was the pricing one about?
The comment and replies under it.
> Reply saying we have re-scoped it and the number holds, and mark it resolved.
The assistant previews the reply before posting it, then closes the thread.
## Compare versions and export a deal [#compare-versions-and-export-a-deal]
> What versions do we have of ACME Customer Portal?
Every saved version, with who saved it, when, and what the deal was worth at the
time.
> Show me the deal as it was in "sent to legal".
The assistant shows the deal at that point, read-only.
> What is different between then and now?
You get the movement in value, cost, and margin between the two versions.
> Export the current one to a spreadsheet for me.
The workbook is written where you ask for it, with its size reported so you know
it arrived whole.
> Now give me the proposal itself as a PDF.
The same document the proposal's download button produces, because it is the
same render: the assistant opens the proposal in a browser on your machine and
saves what comes back. It needs a browser installed to do that, and it tells you
the address to open yourself if there is none. Ask for a saved version and you
get that version's proposal instead of the current one.
## When Estii refuses a request [#when-estii-refuses-a-request]
Sometimes Estii will not carry out a request. The assistant tells you why and
does not save a partial change.
Common reasons:
* Your role does not allow it. The assistant has the same access you do. Editing
a deal needs editor access or above. Approving, closing, saving a version, and
changing the library or resources need manager access.
* Your plan does not allow it. Most reads work on every plan. Pipeline forecasts
and changes made from outside the app need a paid plan or active trial. The
app itself is unaffected.
* The deal is no longer in draft. Scope is editable while a deal is in draft.
Once it is approved, commercial fields stay open but scope content does not.
Returning it to draft reopens the scope.
* The change would leave the data in a state Estii does not support. For example,
a rate card with its floor above its ceiling, two roles with the same name, an
estimate measured in the wrong unit, or a line pointing at something being
removed.
If existing data already has a problem, Estii tells you about it. That does not
stop unrelated work. A new invalid change is refused before anything is saved.
## Getting connected [#getting-connected]
Setup is one line and takes a minute. See the [MCP server](/dev/mcp) page for
Claude Code, Codex, Cursor, and other clients.
If you would rather work from a terminal or script, the same surface is available
from the [CLI](/dev/cli).
# Overview (/dev)
Estii is programmable. Read and update your deals and the data behind them (spaces,
rate cards, roles, products, streams, templates, price estimates, and the deal
lifecycle) from your own tools, the terminal, or an AI agent.
Three ways in, all over the same contract:
* **[MCP server](/dev/mcp)** — connect an AI agent like Claude Code or Codex and
work in plain language. The quickest way to start, and [Working with
agents](/dev/agents) covers what to hand it once you are connected.
* **[Estii CLI](/dev/cli)** — the official `estii` command, published on npm as
`@estii.com/cli`, for running any operation from your terminal.
* **[API reference](/dev/api)** — a JSON REST API at `https://api.estii.com/v1`,
for building directly against Estii.
However you connect, you authenticate. Build against the API with a bearer token:
```bash
curl https://api.estii.com/v1/spaces \
-H "Authorization: Bearer estp_your_token"
```
The CLI and MCP server take the same token, or you can just run `estii auth login`
to sign in through your browser. See [Authentication](/dev/api/authentication) for
tokens and scopes.
## Learn more [#learn-more]
# Estii MCP Server (/dev/mcp)
Connect Estii to Claude Code, Codex, Cursor, or any other AI agent, so it can read
and update your deals in plain language. Setup is one line, with nothing to
install.
## Set it up [#set-it-up]
The quickest path needs only Node. `npx` fetches and runs the server, so there is
nothing to install first.
### Claude Code [#claude-code]
```bash
claude mcp add estii -- npx -y @estii.com/cli mcp serve
```
Then open `/mcp` in Claude Code to confirm estii is connected.
### Codex [#codex]
Add a block to `~/.codex/config.toml`:
```toml
[mcp_servers.estii]
command = "npx"
args = ["-y", "@estii.com/cli", "mcp", "serve"]
```
### Other clients [#other-clients]
Any MCP client works. Point its command at `npx` with args `-y @estii.com/cli mcp
serve`. If you have the [CLI](/dev/cli) installed, point it at `estii` with args
`mcp serve` instead.
## Sign in [#sign-in]
The server uses your Estii identity, so signing in once is enough:
```bash
npx @estii.com/cli auth login
```
For headless or shared setups, pass a token in the server's environment instead of
signing in. In Claude Code:
```bash
claude mcp add estii --env ESTII_TOKEN=estp_your_token -- npx -y @estii.com/cli mcp serve
```
In Codex, add an `[mcp_servers.estii.env]` block that sets `ESTII_TOKEN`. See [API
keys](/docs/settings/api-keys) to create a token. Browsing works with no
credential; reading and updating your deals needs one.
After you change the server (new token, new version), restart the connection:
toggle estii off and on in your client, or restart it.
## Talk to your agent [#talk-to-your-agent]
Once connected, just ask in plain language. Talk in names, not ids, and the agent
looks things up. Not sure where to start? Ask it what it can do:
> What can you do with my Estii deals?
Reads are safe and changes are explicit: the agent confirms before it writes
anything, and you can ask for a dry run to preview a change first.
For what to hand it once you are connected, see [Working with
agents](/dev/agents): worked conversations for pricing a deal, closing a gap to a
customer's budget, rescoping, rolling a rate change through the pipeline, and
reviewing where everything stands.
## Before you connected [#before-you-connected]
Estii’s MCP server lets an AI assistant work with Estii through our CLI. That means the assistant can read and, where your access allows it, change parts of your space: deals, resources, rate cards, library items, comments, versions, and related content.
Reading is safe. Writing is real. If a change is applied, it lands on your live Estii data.
The CLI is designed to preview changes and ask for confirmation before it writes. Agents are also told to show you what they are about to do before applying it. But the AI client you use is not controlled by Estii, so we cannot guarantee exactly how carefully it will behave in every case.
Use the same judgement you would with any person or tool that has your access. Check what it proposes, make larger changes deliberately, and save a version before a major rescope.
Some actions are not available through the assistant at all. When that happens, Estii sends you back into the app instead.
## How it works [#how-it-works]
`estii mcp serve` runs the [CLI](/dev/cli) as a [Model Context
Protocol](https://modelcontextprotocol.io) server. The agent gets three tools:
* **command reference** — what the CLI can currently do, read straight from the
binary. No sign-in needed.
* **read** — anything that only looks: your spaces and their libraries, deals and
what is inside them, and pricing worked out from an expression.
* **change** — anything that alters state: edits to a deal and its contents,
edits to the space library, and the transitions that move a deal through its
lifecycle.
You never name the tools yourself; the agent picks the right one. Read and change
together cover the CLI's entire surface, and the reference tool tells the agent
what that surface is at the moment it asks, which is why asking the agent what it
can do just works.
The server stays connected for as long as your client keeps it open, so it holds
what it has read and is told when anything changes. Asking the same question
twice costs nothing, an edit somebody makes in the app reaches the next answer
without you saying so, and a change your agent makes lands and is ordered the way
an edit made in the browser is. If the connection cannot be made, or drops, every
tool call still works: the server checks before it answers instead.
## Learn more [#learn-more]
# Authentication (/dev/api/authentication)
Every request outside the device-flow endpoints needs a bearer credential. Send
it in the `Authorization` header:
```bash
curl https://api.estii.com/v1/spaces \
-H "Authorization: Bearer estp_your_token"
```
The `estii` CLI reads the same token from the `ESTII_TOKEN` environment
variable, or you can run `estii auth login` for the interactive device flow.
## Verify your token [#verify-your-token]
`GET /v1/spaces` lists the spaces your credential can reach, so it doubles as a
quick check that a token works. A valid token returns `200` with the list:
```bash
curl https://api.estii.com/v1/spaces \
-H "Authorization: Bearer estp_your_token"
```
```json
{
"items": [
{
"id": "sp_abc",
"name": "Acme",
"role": "owner",
"is_member": true,
"plan": "pro",
"updated_at": 1738368000000
}
],
"next_cursor": null
}
```
A missing or invalid token returns `401` instead:
```json
{ "message": "invalid or expired token" }
```
You can also call `GET /v1/meta` without a credential to read the current
contract version before you build against it.
## Credential types [#credential-types]
There are two kinds of credential:
* **Personal access tokens** (`estp_`) act as a user across the spaces that user
can reach. Create them in **Account settings**. Best for the CLI and personal
scripts.
* **Space API keys** (`ests_`) act as a single space and keep working after the
person who created them leaves. Create them in **Space settings → API keys**
(owners and admins). Best for durable server-to-server integrations.
## Scopes [#scopes]
Each credential carries a scope: `read` (read-only) or `read_write` (read plus
mutating calls). A `read` credential is rejected on any mutating endpoint with a
`403` and the message `token scope does not permit writes`.
## Rate limits [#rate-limits]
Each credential is limited to **120 requests per 60-second window** across the
whole `/v1` surface, expensive exports included. Every response advertises that
quota as `RateLimit-Policy: 120;w=60`. The limiter runs at the edge and reports
only whether a request was allowed, so there is no running remaining count to
read between requests. When you exceed the limit the API responds with `429`,
carrying `RateLimit-Remaining: 0` and a `Retry-After` giving the authoritative
number of seconds to wait. Back off for that long, then retry.
## Errors [#errors]
Errors return the matching HTTP status with a JSON body:
```json
{ "message": "bearer token required" }
```
A request that fails schema validation is the one exception, answering `400`
with the failing fields instead:
```json
{
"success": false,
"data": { "limit": "99999" },
"error": [{ "path": ["limit"], "message": "..." }]
}
```
| Status | Meaning |
| ------ | -------------------------------------------------------------- |
| `401` | Missing, malformed, or expired bearer credential. |
| `403` | Credential is valid but its `read` scope forbids the mutation. |
| `404` | Resource or endpoint not found. |
| `409` | Request conflicts with the resource's current state. |
| `429` | Rate limit exceeded. Honour the `Retry-After` header. |
| `500` | Internal error. Retry only if the request is idempotent. |
Create, scope, and revoke tokens in [API keys](/docs/settings/api-keys), or drive the API from the
[estii CLI](/dev/cli).
# Cancel a run (/dev/api/deleteV1SpaceByIdRunsByRunId)
`DELETE /v1/space/{id}/runs/{runId}`
Requires `read_write` scope.
Stops the work, records the run as cancelled, and discards what it uploaded, because cancelling states the caller does not want the result. A run that has already completed or failed is refused rather than silently accepted, and a failed run keeps its artefacts so it stays diagnosable.
## Path parameters
- `id` (string, required)
- `runId` (string, required)
## Responses
### 200 — Success
- `run_id` (string, required)
- `status` (string, required)
- `error` (string)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List deals (/dev/api/getV1Deal)
`GET /v1/deal`
Requires `read` scope.
## Query parameters
- `limit` (integer): range 1–200, default `50`
- `cursor` (string)
- `space` (string, required): length ≥ 1
- `status` (string): one of `active`, `archived`, `all`, `draft`, `approved`, `progressed`, `won`, `lost`, `abandoned`, default `active`
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `next_cursor` (string | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Get a deal (/dev/api/getV1DealById)
`GET /v1/deal/{id}`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `space` (string, required): length ≥ 1
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
- `overview` (object | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Export a deal (/dev/api/getV1DealByIdExport)
`GET /v1/deal/{id}/export`
Requires `read` scope.
Returns an xlsx workbook by default, a JSON payload with `?format=json`, or the proposal deck as markdown with `?format=md` (Business plans and trials; a free space is refused with `plan_required`). `include` selects deck, appendix, or all sections for markdown.
## Path parameters
- `id` (string, required)
## Query parameters
- `space` (string, required): length ≥ 1
- `format` (string): one of `xlsx`, `json`, `md`, default `xlsx`
- `include` (string): one of `deck`, `appendix`, `all`, default `all`
## Responses
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List deal versions (/dev/api/getV1DealByIdVersions)
`GET /v1/deal/{id}/versions`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `space` (string, required): length ≥ 1
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `description` (string, required)
- `status` (string, required)
- `price` (number, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Service metadata and contract version (/dev/api/getV1Meta)
`GET /v1/meta`
## Responses
### 200 — Success
- `contract` (string, required)
- `limit_version` (string, required)
### 400 — Invalid request
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Get a space (/dev/api/getV1SpaceById)
`GET /v1/space/{id}`
Requires `read` scope.
`members=true` returns the whole member directory. Every member receives each member's id, name and role; the `email` field is present only for `owner` or `admin` in the space. `member_ids` is a comma-separated list of member ids and returns those members alone, with addresses, to any member of the space; at most 50 ids per call. The two parameters are mutually exclusive.
## Path parameters
- `id` (string, required)
## Query parameters
- `members` (boolean)
- `member_ids` (string)
## Responses
### 200 — Success
- `id` (string, required)
- `name` (string, required)
- `plan` (string, required)
- `trial` (boolean)
- `role` (string | null, required): one of `owner`, `admin`, `manager`, `editor`, `guest`, `null`
- `member_id` (string | null)
- `is_member` (boolean, required)
- `currency` (string, required)
- `work_unit` (string, required): one of `day`, `hour`
- `member_count` (number, required)
- `members` (object[])
- `id` (string, required)
- `email` (string)
- `name` (string, required)
- `role` (string, required): one of `owner`, `admin`, `manager`, `editor`, `guest`
- `created_at` (number, required)
- `updated_at` (number, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List rate cards (/dev/api/getV1SpaceByIdCards)
`GET /v1/space/{id}/cards`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `is_default` (boolean, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List library items (/dev/api/getV1SpaceByIdLibrary)
`GET /v1/space/{id}/library`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `limit` (integer): range 1–200, default `50`
- `cursor` (string)
- `collection` (string)
- `group` (string)
- `status` (string): one of `proposed`, `active`, `archived`, default `active`
- `card` (string)
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required): one of `proposed`, `active`, `archived`
- `group` (string, required)
- `collection` (string, required)
- `tasks` (number, required)
- `sources` (number, required)
- `uses` (number, required)
- `grade` (string, required): one of `A+`, `A`, `B`, `C`, `n/a`
- `price_low` (number | null, required)
- `price_high` (number | null, required)
- `currency` (string, required)
- `updated_at` (number, required)
- `next_cursor` (string | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Export the library (/dev/api/getV1SpaceByIdLibraryExport)
`GET /v1/space/{id}/library/export`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `status` (string): one of `proposed`, `active`, `archived`, default `active`
- `card` (string)
## Responses
### 200 — Success
- `space_id` (string, required)
- `currency` (string, required)
- `collections` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `description` (string, required)
- `groups` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required): one of `proposed`, `active`, `archived`
- `group` (string, required)
- `collection` (string, required)
- `tasks` (number, required)
- `sources` (number, required)
- `uses` (number, required)
- `grade` (string, required): one of `A+`, `A`, `B`, `C`, `n/a`
- `price_low` (number | null, required)
- `price_high` (number | null, required)
- `currency` (string, required)
- `updated_at` (number, required)
- `aliases` (string[], required)
- `keywords` (string[], required)
- `tags` (string[], required)
- `last_seen` (number, required)
- `last_used` (number, required)
- `task_list` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `description` (string, required)
- `priority` (number | null, required)
- `risk` (number | null, required)
- `tags` (string[], required)
- `grade` (string, required): one of `A+`, `A`, `B`, `C`, `n/a`
- `price_low` (number | null, required)
- `price_high` (number | null, required)
- `estimates` (object[], required)
- `resource` (string, required)
- `quantity` (string, required)
- `unit` (string, required)
- `period` (string | null, required)
- `low` (number, required)
- `typical` (number, required)
- `high` (number, required)
- `low_text` (string, required)
- `typical_text` (string, required)
- `high_text` (string, required)
- `evidence` (string, required): one of `observed`, `manual`
- `sample_count` (number, required)
- `source_count` (number, required)
- `price_low` (number | null, required)
- `price_high` (number | null, required)
- `ungrouped` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required): one of `proposed`, `active`, `archived`
- `group` (string, required)
- `collection` (string, required)
- `tasks` (number, required)
- `sources` (number, required)
- `uses` (number, required)
- `grade` (string, required): one of `A+`, `A`, `B`, `C`, `n/a`
- `price_low` (number | null, required)
- `price_high` (number | null, required)
- `currency` (string, required)
- `updated_at` (number, required)
- `aliases` (string[], required)
- `keywords` (string[], required)
- `tags` (string[], required)
- `last_seen` (number, required)
- `last_used` (number, required)
- `task_list` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `description` (string, required)
- `priority` (number | null, required)
- `risk` (number | null, required)
- `tags` (string[], required)
- `grade` (string, required): one of `A+`, `A`, `B`, `C`, `n/a`
- `price_low` (number | null, required)
- `price_high` (number | null, required)
- `estimates` (object[], required)
- `resource` (string, required)
- `quantity` (string, required)
- `unit` (string, required)
- `period` (string | null, required)
- `low` (number, required)
- `typical` (number, required)
- `high` (number, required)
- `low_text` (string, required)
- `typical_text` (string, required)
- `high_text` (string, required)
- `evidence` (string, required): one of `observed`, `manual`
- `sample_count` (number, required)
- `source_count` (number, required)
- `price_low` (number | null, required)
- `price_high` (number | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Read capacity or revenue over a window (/dev/api/getV1SpaceByIdPipelineByKind)
`GET /v1/space/{id}/pipeline/{kind}`
Requires `read` scope.
Filters are include-only; there is no exclusion form of any of them. The window resolves rather than refusing: a mid-week `from` snaps to its Monday and `months` clamps to 3-12, and the response states what it covered. Capacity is demand in full-time equivalents, not availability against headcount. Pipeline forecasting is a Business feature; a free space is refused with `plan_required`.
## Path parameters
- `id` (string, required)
- `kind` (string, required)
## Query parameters
- `from` (integer)
- `months` (integer)
- `period` (string): one of `week`, `month`
- `group` (string): one of `role`, `deal`
- `deals` (string)
- `roles` (string)
- `tags` (string)
- `status` (string)
## Responses
### 200 — Success
- `kind` (string, required): one of `capacity`, `revenue`
- `currency` (string, required)
- `window` (object, required)
- `start` (number, required)
- `end` (number, required)
- `months` (number, required)
- `period` (string, required): one of `week`, `month`
- `rule` (string, required)
- `group` (string | null, required): one of `role`, `deal`, `null`
- `series` (object[], required)
- `group` (object | null, required)
- `entries` (object[], required)
- `period` (string, required)
- `start` (number, required)
- `end` (number, required)
- `average` (object, required)
- `confirmed` (number, required)
- `forecast` (number, required)
- `exposure` (number, required)
- `peak` (object, required)
- `confirmed` (number, required)
- `forecast` (number, required)
- `exposure` (number, required)
- `bands` (object, required)
- `confirmed` (object, required)
- `peak` (number, required)
- `peak_period` (string | null, required)
- `total` (number, required)
- `forecast` (object, required)
- `peak` (number, required)
- `peak_period` (string | null, required)
- `total` (number, required)
- `exposure` (object, required)
- `peak` (number, required)
- `peak_period` (string | null, required)
- `total` (number, required)
- `as_of` (number, required)
- `stale` (boolean, required)
- `source` (string, required): one of `rollup`, `deals`
- `deals` (number, required)
- `empty` (number, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Summarise the pipeline by deal status (/dev/api/getV1SpaceByIdPipelineSummary)
`GET /v1/space/{id}/pipeline/summary`
Requires `read` scope.
Pipeline forecasting is a Business feature; a free space is refused with `plan_required`.
## Path parameters
- `id` (string, required)
## Responses
### 200 — Success
- `currency` (string, required)
- `statuses` (object[], required)
- `status` (string, required)
- `deals` (number, required)
- `value` (number, required)
- `weighted_value` (number, required)
- `margin` (number, required)
- `totals` (object, required)
- `deals` (number, required)
- `value` (number, required)
- `weighted_value` (number, required)
- `open_value` (number, required)
- `win_rate` (object, required)
- `rate` (number, required)
- `won` (number, required)
- `lost` (number, required)
- `abandoned` (number, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List products (/dev/api/getV1SpaceByIdProducts)
`GET /v1/space/{id}/products`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `limit` (integer): range 1–200, default `50`
- `cursor` (string)
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `kind` (string, required)
- `unit` (string | null, required)
- `period` (string | null, required)
- `quantity` (string, required)
- `currency` (string, required)
- `model` (string | null, required)
- `updated_at` (number, required)
- `tag_id` (string | null, required)
- `tag_name` (string | null, required)
- `rate_count` (number, required)
- `cost_min` (number, required)
- `cost_max` (number, required)
- `price_min` (number, required)
- `price_max` (number, required)
- `margin` (number, required)
- `rates` (object[], required)
- `max_units` (number | null, required)
- `cost` (number, required)
- `price` (number, required)
- `cost_set` (boolean)
- `price_set` (boolean)
- `next_cursor` (string | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Export products (/dev/api/getV1SpaceByIdProductsExport)
`GET /v1/space/{id}/products/export`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Responses
### 200 — Success
- `meta` (object, required)
- `exported_at` (number, required)
- `space_id` (string, required)
- `space_name` (string, required)
- `tags` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `products` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `unit` (string | null, required)
- `period` (string | null, required)
- `quantity` (string, required)
- `currency` (string, required)
- `model` (string | null, required)
- `updated_at` (number, required)
- `tag_id` (string | null, required)
- `rates` (object[], required)
- `max_units` (number | null, required)
- `cost` (number, required)
- `price` (number, required)
- `cost_set` (boolean)
- `price_set` (boolean)
- `margin` (number)
- `external_id` (string)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List roles (/dev/api/getV1SpaceByIdRoles)
`GET /v1/space/{id}/roles`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `limit` (integer): range 1–200, default `50`
- `cursor` (string)
- `card` (string)
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `kind` (string, required)
- `unit` (string | null, required)
- `period` (string | null, required)
- `quantity` (string, required)
- `currency` (string, required)
- `updated_at` (number, required)
- `tag_id` (string | null, required)
- `tag_name` (string | null, required)
- `card_id` (string, required)
- `card_name` (string, required)
- `cost` (number, required)
- `price` (number, required)
- `margin` (number, required)
- `next_cursor` (string | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Export roles (/dev/api/getV1SpaceByIdRolesExport)
`GET /v1/space/{id}/roles/export`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `card` (string)
## Responses
### 200 — Success
- `meta` (object, required)
- `exported_at` (number, required)
- `space_id` (string, required)
- `space_name` (string, required)
- `tags` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `cards` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `roles` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `unit` (string | null, required)
- `period` (string | null, required)
- `currency` (string, required)
- `updated_at` (number, required)
- `tag_id` (string | null, required)
- `cards` (object[], required)
- `card_id` (string, required)
- `cost` (number, required)
- `price` (number, required)
- `margin` (number, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List streams (/dev/api/getV1SpaceByIdStreams)
`GET /v1/space/{id}/streams`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `limit` (integer): range 1–200, default `50`
- `cursor` (string)
- `card` (string)
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `kind` (string, required)
- `unit` (string | null, required)
- `period` (string | null, required)
- `quantity` (string, required)
- `currency` (string, required)
- `updated_at` (number, required)
- `tag_id` (string | null, required)
- `tag_name` (string | null, required)
- `card_id` (string, required)
- `card_name` (string, required)
- `allocation_count` (number, required)
- `role_equivalents` (number, required)
- `cost_rollup` (number, required)
- `price_rollup` (number, required)
- `margin` (number, required)
- `allocations` (object[], required)
- `role_id` (string)
- `role_name` (string | null, required)
- `amount` (number, required)
- `dedicated` (boolean)
- `next_cursor` (string | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Export streams (/dev/api/getV1SpaceByIdStreamsExport)
`GET /v1/space/{id}/streams/export`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `card` (string)
## Responses
### 200 — Success
- `meta` (object, required)
- `exported_at` (number, required)
- `space_id` (string, required)
- `space_name` (string, required)
- `tags` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `cards` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `streams` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `unit` (string | null, required)
- `period` (string | null, required)
- `currency` (string, required)
- `updated_at` (number, required)
- `tag_id` (string | null, required)
- `roles` (number, required)
- `cards` (object[], required)
- `card_id` (string, required)
- `cost` (number, required)
- `price` (number, required)
- `margin` (number, required)
- `allocations` (object[], required)
- `role_id` (string)
- `role_name` (string | null, required)
- `amount` (number, required)
- `dedicated` (boolean)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List templates (/dev/api/getV1SpaceByIdTemplates)
`GET /v1/space/{id}/templates`
Requires `read` scope.
## Path parameters
- `id` (string, required)
## Query parameters
- `limit` (integer): range 1–200, default `50`
- `cursor` (string)
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `kind` (string, required): one of `deal`, `phase`
- `name` (string, required)
- `description` (string | null, required)
- `is_default` (boolean, required)
- `currency` (string | null, required)
- `price` (number | null, required)
- `margin` (number | null, required)
- `updated_at` (number, required)
- `next_cursor` (string | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# List spaces (/dev/api/getV1Spaces)
`GET /v1/spaces`
Requires `read` scope.
## Query parameters
- `limit` (integer): range 1–200, default `50`
- `cursor` (string)
## Responses
### 200 — Success
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `role` (string | null, required): one of `owner`, `admin`, `manager`, `editor`, `guest`, `null`
- `is_member` (boolean, required)
- `plan` (string, required)
- `updated_at` (number, required)
- `next_cursor` (string | null, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# API reference (/dev/api)
The Estii API is JSON over HTTPS. Start with [Authentication](/dev/api/authentication), then browse the endpoints below.
## Spaces [#spaces]
## Resources [#resources]
## Deals [#deals]
# Update a deal (/dev/api/patchV1DealById)
`PATCH /v1/deal/{id}`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
- `set` (object, required)
- `probability` (number): range 0–1
- `name` (string): length 1–32
- `description` (string): length ≤ 2048
- `target_price` (number): range 0–1000000000
- `target_margin` (number): range 0–1
- `due` (integer): range ≥ 0
- `start` (integer): range ≥ 0
- `owner` (string | null): format email
- `account` (string | null): length ≤ 32
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Approve a deal (/dev/api/postV1DealByIdApprove)
`POST /v1/deal/{id}/approve`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
- `probability` (number): range 0–1
- `date` (integer)
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Archive a deal (/dev/api/postV1DealByIdArchive)
`POST /v1/deal/{id}/archive`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Close a deal (/dev/api/postV1DealByIdClose)
`POST /v1/deal/{id}/close`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
- `outcome` (string, required): one of `won`, `lost`, `abandoned`
- `reason` (string): length ≤ 500
- `date` (integer)
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Progress a deal (/dev/api/postV1DealByIdProgress)
`POST /v1/deal/{id}/progress`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
- `probability` (number): range 0–1
- `date` (integer)
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Redraft a deal (/dev/api/postV1DealByIdRedraft)
`POST /v1/deal/{id}/redraft`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
- `reason` (string): length ≤ 500
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Request approval for a deal (/dev/api/postV1DealByIdRequestApproval)
`POST /v1/deal/{id}/request-approval`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
- `requested_to` (string): format email
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Move a deal's dates (/dev/api/postV1DealByIdReschedule)
`POST /v1/deal/{id}/reschedule`
Requires `read_write` scope.
Accepts a signed relative offset (`+2w`, `-1m`, `+10d`) or an absolute `YYYY-MM-DD` date, and resolves either to a Monday. A deal past draft is snapshotted first, and phases pinned to a fixed date move by the same offset. A shift landing on the deal's current start succeeds without writing.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
- `shift` (string, required): length 1–32
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
- `start` (number, required)
- `previous_start` (number, required)
- `version_created` (boolean, required)
- `phases_moved` (object[], required)
- `id` (string, required)
- `name` (string, required)
- `changed` (boolean, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Unarchive a deal (/dev/api/postV1DealByIdUnarchive)
`POST /v1/deal/{id}/unarchive`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Save a deal version (/dev/api/postV1DealByIdVersions)
`POST /v1/deal/{id}/versions`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
- `name` (string, required): length 1–200
- `description` (string): length ≤ 2000
## Responses
### 201 — Success
- `id` (string, required)
- `name` (string, required)
- `description` (string, required)
- `status` (string, required)
- `price` (number, required)
- `created_at` (number, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Restore a deal version (/dev/api/postV1DealByIdVersionsByVersionId)
`POST /v1/deal/{id}/versions/{versionId}`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
- `versionId` (string, required)
## Request body (required)
- `space` (string, required): length ≥ 1
## Responses
### 200 — Success
- `success` (boolean, required)
- `deal` (string, required)
- `version` (string, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Create a deal (/dev/api/postV1SpaceByIdDeals)
`POST /v1/space/{id}/deals`
Requires `read_write` scope.
## Path parameters
- `id` (string, required)
## Request body (required)
- `name` (string): length 1–32
- `clone_of` (string): length ≥ 1
- `phase_template_id` (string): length ≥ 1
- `account` (string | null): length ≤ 32
- `target_margin` (number | null): range 0–1
- `target_price` (number): range 0–1000000000
- `due` (integer): range ≥ 0
- `start` (integer): range ≥ 0
- `currency` (string): length ≥ 1
- `card_id` (string): length ≥ 1
- `order` (number)
- `template` (boolean)
- `options` (object)
- `estimates` (boolean)
- `priorities` (boolean)
- `risks` (boolean)
- `tags` (boolean)
- `owners` (boolean)
- `comments` (boolean)
## Responses
### 200 — Success
- `deal` (object, required)
- `id` (string, required)
- `name` (string, required)
- `status` (string, required)
- `updated_at` (number, required)
- `space_id` (string, required)
- `currency` (string, required)
- `price` (number, required)
- `margin` (number, required)
- `probability` (number, required)
- `archived` (boolean, required)
- `created_at` (number, required)
- `owner_email` (string | null, required)
- `description` (string, required)
- `target_price` (number, required)
- `target_margin` (number, required)
- `start` (number, required)
- `due` (number, required)
- `closed_reason` (string | null, required)
- `pending_at` (number, required)
- `requested_by_email` (string | null, required)
- `requested_to_email` (string | null, required)
- `account` (string | null)
- `nodes` (object[], required)
- `id` (string, required)
- `type` (string, required)
- `requested` (string, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Compose a deal from a file (/dev/api/postV1SpaceByIdImportsDeal)
`POST /v1/space/{id}/imports/deal`
Requires `read_write` scope.
Send `multipart/form-data` with a `file` part and an optional `prompt` part steering how the file is read. `currency` and `work_unit` set what the imported deal is priced and estimated in; unset, each falls back to the space setting. Returns as soon as the run is accepted, because an import takes minutes; read its progress from the run nodes the space sync carries, and stop it with the cancel route. Refused before the file is stored when the space has Import Intelligence switched off (409), has spent its hourly import capacity (429), or is at its active-deal ceiling (400).
## Path parameters
- `id` (string, required)
## Responses
### 200 — Success
- `run_id` (string, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# Match library items to a request (/dev/api/postV1SpaceByIdLibraryMatch)
`POST /v1/space/{id}/library/match`
Requires `read` scope.
Meters as AI usage against the space credit measure. Refused when the space's library setting is off, and when its plan does not permit it.
## Path parameters
- `id` (string, required)
## Request body (required)
- `prompt` (string, required): length 1–2000
- `context` (object): default `[object Object]`
- `phase` (string): length ≤ 32
- `categories` (string[])
- `sections` (string[])
- `placingUnder` (string): length ≤ 60
## Responses
### 200 — Success
- `groups` (object[], required)
- `label` (string, required)
- `summary` (string, required)
- `items` (object[], required)
- `id` (string, required)
- `name` (string, required)
### 400 — Invalid request
- `message` (string, required)
### 401 — Missing or invalid credential
- `message` (string, required)
### 402 — Target space is read-only on its plan (`plan_required`)
- `message` (string, required)
### 403 — Insufficient scope or access
- `message` (string, required)
### 429 — Rate limit exceeded
- `message` (string, required)
### 500 — Internal server error
- `message` (string, required)
# How commands work (/dev/cli/commands)
Enough to follow every workflow in this section. Every command's exact arguments
and options are in its own `--help`.
## Shape [#shape]
A command is a noun, a verb, and what they act on:
```bash
estii [[] [] [=...] [flags]
```
The nouns that address your data are `context`, `space`, `deal`, `pipeline`,
`library`, `role`, `stream`, `product`, `tag`, `card`, `template`, and `run`.
Alongside them sit `auth`, `init`, `self`, `cache`, `mcp`, `doctor`, `version`,
and `commands`. So
`estii deal add nd_abc123 tasks/tk_9/estimates resource="Senior Dev" amount=5d`
reads as: on the deal `nd_abc123`, under the estimates of task `tk_9`, add one
with these two values.
## `estii context` says what to build [#estii-context-says-what-to-build]
Everything else here is how to phrase a command; that one is what a deal in your
space is made of, what the space already holds to start from and reuse, which
resource an estimate binds to and in what unit, and what a change is refused for.
It answers in your space's own words, so if you have renamed a level it uses your
name for it. Add a deal reference and it reports that deal's own shape and what
may be changed on it now. Worth reading first, and worth handing to an assistant
before it writes anything.
## Paths address what lives inside a deal [#paths-address-what-lives-inside-a-deal]
Phases, categories, sections, line items, tasks, estimates, milestones,
adjustments, comments, and versions exist only within a deal, so they are
addressed as a path on it rather than as nouns of their own. A path names either a
collection to create in or one node to act on. Omit the path and the command means
the deal itself.
## References may be an id or a name [#references-may-be-an-id-or-a-name]
`estii deal show "ACME Customer Portal"` works as well as the id. Names are matched
loosely, ignoring case and punctuation. Ids are exact, so prefer them in a script;
an ambiguous name exits non-zero and lists the candidates.
## Ids come from `deal find` [#ids-come-from-deal-find]
A deal's own nodes each have an id, and `estii deal find ][` lists them with the
exact path each write verb takes, alongside the units and price the app shows for
each. It is how you get from "the discovery estimate" to `estimates/es_4`, and it
ranks them at the same time. Anything you create also reports its own id, so a
script that is building a deal never has to look one up.
## Three reads, one question each [#three-reads-one-question-each]
`deal find` says where something is, `deal inspect` says what one part of the deal
is, and `deal show` says what the whole deal is worth. `inspect` takes an address
inside the deal, `show` does not.
## One space is active at a time [#one-space-is-active-at-a-time]
Set it once with `estii space use acme`, or override it per call with
`--space acme`, or set `ESTII_SPACE`. With no space active and none passed, a
command exits rather than picking one.
## Values go in as pairs or as JSON [#values-go-in-as-pairs-or-as-json]
`key=value` suits a single field. `--data ''` takes a whole body, and
`--data @file` or `--data -` reads it from a file or standard input, which is what
to use when a value carries quotes or line breaks. They set the same fields;
passing both at once is an error.
## Changes preview, then apply [#changes-preview-then-apply]
At a terminal, a command that writes shows you the diff and asks before it applies.
A script has no one to ask, so it passes `--yes` to apply or `--dry-run` to see the
diff and stop; without either, it exits.
## Output [#output]
Text by default, for reading. Add `--json` for one document or `-o ndjson` for a
row per line. Those two are the stable contract.
## Exit codes [#exit-codes]
Exit codes carry the outcome, so a script branches on the code rather than matching
on message text: `0` success, `2` bad usage, `3` not authorised, `4` not found, `5`
conflict or ambiguous, `6` temporarily unavailable and worth retrying, `7` client
too old, `130` cancelled. Waiting on work that takes minutes adds two: `8` the work
failed, `9` it is still going and you can wait again.
The line between `2` and `5` is where the refusal comes from. A command that cannot
be understood — an unknown field, a value the field will not take — exits `2`. A
command that is understood but would leave your data in a state Estii does not mean
exits `5`: a rate card whose floor sits above its ceiling, a name two roles share,
an estimate measured in a unit its resource does not count, a line pointing at
something the same change removes. Every such refusal is checked before anything is
sent, so nothing is half applied, and `--json` carries each one as its own entry
with the field to change and the values that would satisfy it. State that was
already there does not block you: it is reported and the change goes through.
## Ask the binary [#ask-the-binary]
These pages cover the paths worth walking end to end. For the exact surface:
```bash
estii commands # every command, with its arguments, options, and examples
estii commands deal # just one noun's verbs
estii commands deal set # one command's entry on its own
estii deal set --help # one command in full
```
`estii commands` prints a table at a terminal and JSON when piped.
# Build and edit a deal (/dev/cli/deals)
Every command here follows the grammar in [how commands work](/dev/cli/commands):
a preview first, `--yes` to apply.
## Find what a deal's money is in, before touching it [#find-what-a-deals-money-is-in-before-touching-it]
The index prices every level of the deal, so one call tells you which line item
carries the value and the next tells you what is inside it.
```bash
estii deal find nd_abc123 --kind feature --json
estii deal inspect nd_abc123 features/ft_7 --json
estii deal find nd_abc123 --kind estimate --json
estii deal set nd_abc123 estimates/es_4 amount=5d --dry-run
```
Units follow whatever the work is measured in, so a line item reads as days, or as
licences, or as a count of fixed prices where nothing beneath it carries a unit.
Where a line item mixes them, the figure covers the dominant part and is marked
with a `+`; under `--json`, `total.omitted` says what it left out, so a script can
act on the qualifier. A deal whose structure is incomplete still lists every id,
with the totals left off rather than reported as zero.
## Correct an estimate [#correct-an-estimate]
Someone quoted three days where it should have been five.
```bash
estii deal list --status draft
estii deal find nd_abc123 --kind estimate
estii deal set nd_abc123 estimates/es_4 amount=5d --dry-run
estii deal set nd_abc123 estimates/es_4 amount=5d --yes
```
`find` gives you the path and what the line is currently worth. An amount names its
own unit, so `5d` is five days and `4h` is four hours. A bare number against a
role, a stream, or anything else measured in time or data is rejected, and the
message names the units it takes. Resources that count whole things, like licences
or a fixed fee, take a bare number. Recurrence is its own field: `period=week`.
## Turn a proposal you already have into a deal [#turn-a-proposal-you-already-have-into-a-deal]
If the scope is already written down — a pricing spreadsheet, a proposal, an RFP —
hand the file over instead of typing it back in.
```bash
estii deal import proposal.xlsx --yes --json
estii run wait rn_abc123 --json
estii deal show nd_new123
```
Composing takes minutes, so the first command returns as soon as the work starts
and gives you its run id. `run wait` blocks until it finishes, and stops after
ninety seconds: exit `0` means it finished and the result names the deal it built,
`9` means it is still going and you can wait again, `8` means it failed and says
why.
```bash
estii run list
estii run show rn_abc123
estii run cancel rn_abc123 --yes
```
`run list` shows what is running in the space and `run show` walks the steps one by
one with what each produced, which is where to look if a composition went somewhere
you did not expect. `run cancel` stops one and throws away what it uploaded; a run
that failed keeps everything, so you can still see what happened.
Spreadsheets, PDF, Word, HTML, plain text, markdown, CSV, JSON and YAML are all
read, up to 20MB. `--prompt "price the optional phases too"` tells the import how to
read the file, the same steer the app's import dialog takes. The deal you get is the
deal the app would build from the same file.
## Build a deal's scope from nothing [#build-a-deals-scope-from-nothing]
Start with the deal itself. Each create reports the nodes it made, so the id for the
next call comes from the last one rather than a lookup.
```bash
estii deal create name="Acme Phase 2" account="Acme Corp" --yes --json
estii deal add nd_abc123 sections/sc_2/features name="Onboarding" --yes --json
estii deal add nd_abc123 features/ft_7/tasks name="Workshop" --yes --json
estii deal add nd_abc123 tasks/tk_9/estimates resource="Senior Developer" amount=5d --yes --json
estii deal show nd_abc123
```
The deal arrives usable: creating one also creates its first phase, that phase's
category and the category's section, and the response lists all four with
`requested` naming the deal. Read the section's id from that response and the next
call has somewhere to put a line item. The account is a name rather than an id, and
one the space does not hold is created and reported.
`--from` says what the deal starts from, and takes one reference for every kind of
start: a phase template, a deal template, an existing deal to copy, or `blank` for
an empty one. Leave it off and the space's default applies. When copying,
`--without estimates` leaves the numbers behind and keeps the structure.
A phase arrives usable the same way: creating one also creates the category and
section it needs, and the response lists all three with `requested` naming the
phase. To start from a template instead of an empty phase, pass
`template="Delivery"`; a name the space does not hold is rejected and the message
lists the ones it has.
`estii template list` names the templates the space holds and
`estii template show "Delivery"` reports what one of them creates: its categories,
the sections under each, and the items each section is seeded with. Worth a look
before building the same structure by hand. It reads a deal template too, which is a
deal, so it answers exactly as `deal show` would.
## Reorganise a deal's scope [#reorganise-a-deals-scope]
Scope rarely lands in the right shape first time.
```bash
estii deal find nd_abc123 --kind feature
estii deal move nd_abc123 features/ft_7 --before ft_3 --yes
estii deal move nd_abc123 features/ft_7 --to sections/sc_5 --yes
estii deal copy nd_abc123 phases/ph_1 --yes --json
estii deal remove nd_abc123 features/ft_9 --yes
```
`move` either repositions a node among its siblings or hands it to a new parent,
never both in one call: moving it elsewhere puts it last, so ordering it is a second
move. `copy` takes everything beneath the node with it. `remove` takes everything
beneath it too, so removing a line item removes its tasks and their estimates.
## Rescope with a way back [#rescope-with-a-way-back]
Before pulling scope around, leave yourself somewhere to return to.
```bash
estii deal snapshot nd_abc123 --name "Before rescope"
estii deal remove nd_abc123 estimates/es_4 --yes
estii deal add nd_abc123 tasks/tk_9/estimates resource="Senior Dev" amount=5d --yes
estii deal show nd_abc123
estii deal restore nd_abc123 "Before rescope" --yes
```
The version comes first, before anything changes. `restore` takes the version by
name or id, and saves the current state on its way back if the deal has left draft.
## Price a change before making it [#price-a-change-before-making-it]
```bash
estii role estimate "Senior Developer" 10d
estii role estimate "Developer" 10d
estii deal add nd_abc123 adjustments kind=discount method=percent value=10% reason="Launch" --dry-run
estii deal add nd_abc123 adjustments kind=discount method=percent value=10% reason="Launch" --yes
estii deal show nd_abc123
```
The two estimates price a resource swap against your live rate card without touching
the deal. Once an adjustment is on the deal, `deal show` reports the price your scope
adds up to and the price after the discount, each with its own margin.
# Reporting and documents (/dev/cli/export)
## Report across the pipeline [#report-across-the-pipeline]
```bash
estii deal list --status all --all --fields id,name,status,price,margin,probability,updated_at -o ndjson
estii deal list --status won --all -o ndjson | jq -s 'map(.price) | add'
estii deal export nd_abc123 --format json --out - | jq .resources
estii deal export nd_abc123 --out acme.xlsx
estii space theme --json
```
`--all` follows every page. `--fields` projects the columns you want, and NDJSON
gives one deal per line for whatever comes next in the pipe. The JSON export carries
the scope tree and the per-resource lines behind the totals. `space theme` reports
the colours and fonts this space's own proposals carry, for whatever renders the
numbers.
## Get the proposal document itself [#get-the-proposal-document-itself]
```bash
estii deal export nd_abc123 --format pdf --out acme.pdf
estii deal export nd_abc123@ver_1 --format pdf --out acme-v1.pdf
```
This is the same PDF the download button in the proposal produces, because it is the
same render: the CLI opens the deal's proposal in a browser on your own machine and
collects what it downloads.
That means a browser has to be there. Chrome, Chromium or Edge, already installed;
set `ESTII_BROWSER` if yours lives somewhere unusual. With none, the command tells
you the address to open by hand instead. The command blocks until the file is
written, which for a long deck is a minute or so, and it needs a personal token that
can write: a browser session carries your whole account, so it is not issued against
a read-only token or a space key.
Add `@ver_1` to export a saved version instead of the deal as it stands.
# Estii CLI (/dev/cli)
The Estii CLI is the official `estii` command, published on npm as
[`@estii.com/cli`](https://www.npmjs.com/package/@estii.com/cli). It keeps a local
copy of your space, prices and validates a change against the same rules the app
applies, and sends the result the same way the app sends an edit, so a deal built
from a script is a deal built in Estii. Reads and reporting are also available as
plain HTTP on the [API](/dev/api).
## Install [#install]
Run it without installing anything (needs Node):
```bash
npx @estii.com/cli commands
```
or with bun
```bash
bunx @estii.com/cli commands
```
Or install it so `estii` is on your `PATH`:
```bash
npm i -g @estii.com/cli
# or
bun add -g @estii.com/cli
```
On macOS and Linux you can also use the install script:
```bash
curl -fsSL https://dl.estii.com/install.sh | sh
```
Confirm it with `estii version`. The examples throughout these pages assume `estii`
is installed; if you are running through `npx`, prefix each command with
`npx @estii.com/cli`.
## Sign in [#sign-in]
Sign in once and the CLI keeps your session:
```bash
estii auth login
```
This opens your browser to confirm the sign-in, then stores the session locally.
Check it any time with `estii auth status`, and sign out with `estii auth logout`.
## Use a token instead [#use-a-token-instead]
If you would rather use a token, set `ESTII_TOKEN` to a personal access token and
the CLI uses it without signing in:
```bash
export ESTII_TOKEN=estp_your_token
```
See [API keys](/docs/settings/api-keys) to create one.
## Start here [#start-here]
`estii context` is the first command worth running. It reports what a deal in your
space is made of, what the space already holds to start from and reuse, which
resource an estimate binds to and in what unit, and what a change is refused for,
in your space's own words. Add a deal reference and it reports that deal's own
shape and what may be changed on it now.
```bash
estii space use acme
estii context
estii deal list --status draft
```
Then [how commands work](/dev/cli/commands) covers the grammar every command
shares, and the pages after it walk the workflows end to end.
## Rather not script it [#rather-not-script-it]
Every operation here can be done by asking an AI assistant in plain language
instead. [MCP server](/dev/mcp) connects one to your Estii data, and [Working with
agents](/dev/agents) covers what to hand it.
# Reuse and curate scope (/dev/cli/library)
## Reuse scope you have already priced [#reuse-scope-you-have-already-priced]
Your library holds the work you sell, named, with a low/typical/high estimate range
behind each line drawn from deals you have already won. Reach for it before writing
scope from scratch.
```bash
estii library find "take card payments"
estii library inspect lib_blk_stripe
estii deal fill nd_abc123 lib_blk_stripe --into "Integrations" --dry-run
estii deal fill nd_abc123 lib_blk_stripe --into "Integrations" --level high --yes
```
`find` matches your phrase against the catalogue and reports how strong each match
is and what caused it, so you can tell a name hit from a loose topical one. It
matches on words, including a word your query only starts (`auth` reaches
`authentication`), and a nothing-found result says what to try instead. Every row
also carries what the item is worth at your rates, as a low-to-high range, and a
grade from `A+` to `C` for how well attested and how current it is: a `B` or a `C` is
work you have sold before but not lately.
`inspect` opens one item: its tasks in order, each with its own price and grade, and
for each estimate line the low, typical, and high amounts with the number of deals
behind them and what that line costs. A line marked `manual` was typed by someone
rather than observed.
`fill` creates the line item, its tasks, and an estimate per line, taking the
`--level` end of each range. Leave `--into` off and it lands where the item's own
placement hints point, reporting that it derived the destination; when nothing points
anywhere it stops and asks you to name one.
An estimate line names a resource, and a deal holds its own copy of your resources,
so a line can name one this deal does not have. When that happens the fill writes
nothing and reports each unresolved line with the deal resources it could map to. Say
which:
```bash
estii deal fill nd_abc123 lib_blk_hosting --into "Platform" --map "Cloud Engineer=Specialist" --yes
```
Or pass `--drop-unresolved` to fill without those lines, which names every one it
dropped. Unresolved lines are always reported, never dropped silently.
## Keep the library right [#keep-the-library-right]
The catalogue drifts: a name stops matching how people ask for the work, an estimate
someone typed by hand is out of date, an item is filed where nobody looks. Correcting
it is the same shape as every other write here, so a diff comes first and nothing
lands until you say so.
```bash
estii library list --updates
estii library set lib_blk_hosting keywords="Infrastructure, Hosting, Environments" --dry-run
estii library set lib_blk_hosting keywords="Infrastructure, Hosting, Environments" --yes
estii library set lib_tsk_host_provision estimate."Specialist"=3-5d --yes
estii library move lib_blk_hosting --to "Support & run" --yes
```
Curating needs the manager role, the same one a rate change needs.
`estii space members` lists who holds which role here, so a refusal names someone to
ask. It reports email addresses to owners and admins only, the same way the app gates
its members page.
An estimate amount names its unit — `4d`, `3-5d`, `10±2d` — and a bare number is
refused with the units that line accepts. The same write marks the line as typed by
hand, so the next deal you win does not average your correction away.
```bash
estii library add item name="Managed backup" group="Support & run" --yes
estii library add task item="Managed backup" name="Restore drill" estimate."Specialist"=2d --yes
estii library export --out library.json
```
`add` puts new work in the catalogue directly, active and ready to fill from.
`export` writes the whole tree — collections, groups, items, tasks and their lines —
which is the read worth doing before a round of edits.
If whatever is reading the catalogue is not running the binary, the same two reads
are plain HTTP: `GET /v1/space/:id/library` for the rows and
`GET /v1/space/:id/library/export` for the tree. Both answer what an item is worth at
your rates and how well attested it is. Neither carries your cost, your margin, or
the matcher guidance a curator wrote.
## Add at scale [#add-at-scale]
Adding at scale is a different safety class, so it has its own verb.
```bash
estii library propose name="Managed backup" group="Support & run" tasks="Restore drill" --yes
estii library propose --into "Managed hosting" tasks="Patch window" --yes
estii library propose --data @library.json --yes
estii library list --status proposed
estii library open
```
`propose` puts items up for review instead of into the catalogue. Until someone
accepts one, it is excluded from `find`, from matching and from `estii deal fill`, so
proposing is safe to run against a library people are already using where a run of
`add` is not. `--data` takes the same document `export` writes, which is what turns
twenty items into one call. A `group=` the space holds is recorded on the proposal;
one it does not hold is carried as a hint and the group is created only if the review
accepts it. A proposal whose name matches something you already have is marked as a
likely duplicate, so the review opens on a merge.
## Three things are done in the app [#three-things-are-done-in-the-app]
An item leaves the catalogue by archival
(`estii library set ][ status=archived`) rather than deletion, because the
evidence behind it outlives the item. A waiting review — new evidence that would move
a line, or a suggested match — is accepted or declined in the app:
`estii library list --updates` names the items with one, and
`estii library open ][` opens the item on the page where you action it: the
catalogue scrolls to it and marks it briefly, and the item's own menu in the app
copies that same link back. And a proposal is accepted, merged or discarded there
too: `estii library open` opens the page, where the Proposals control carries the
count.
# Scope, schedule, and close (/dev/cli/pipeline)
## Cut a deal to a number [#cut-a-deal-to-a-number]
The customer has a budget. `deal scope` says where the money is, one breakdown in
full and every other one named in a line, so you can see whether the weight sits in
the work, in a role, in a stream, or in recurring cost. Then take the group out and
read the deal back.
```bash
estii deal scope nd_abc123
estii deal scope nd_abc123 --by role,stream --json
estii deal descope nd_abc123 ft_7 --dry-run
estii deal descope nd_abc123 ft_7 --yes
estii deal show nd_abc123
estii deal rescope nd_abc123 ft_7 --yes
```
Each group reports what it is worth and what the phase's current scope holds of it,
so a group already partly out reads as `partial`. Descoping takes the line items
under an item with it and clears any exclusion they held on their own, which is what
makes `rescope` put back exactly what came out. Where a group id is in more than one
phase, name the phase with `--phase`, or act on all of them at once with
`--phase all`.
## Read the delivery side [#read-the-delivery-side]
```bash
estii deal schedule nd_abc123
```
The dates each phase sits on, its duration, how many resources it calls for, and how
each role and overhead is allocated across it. The dates are the computed ones, so a
phase that starts after a gap or is pinned to its own date reports where it really
is rather than where chaining the durations would put it.
## See where capacity and revenue land [#see-where-capacity-and-revenue-land]
`deal list` says where each deal stands. `pipeline` says what all of them add up to
over a window: demand in full-time equivalents, and revenue recognition from the
milestones on each deal.
```bash
estii pipeline summary
estii pipeline capacity
estii pipeline capacity --group role
estii pipeline capacity --group deal --from 2026-03-02 --months 3
estii pipeline revenue --months 12 -o json
```
Every series carries three nested bands: `confirmed` is won work, `forecast` adds
your open deals weighted by probability, and `exposure` adds them all at full weight.
Each reports its own peak, the period the peak falls in, and its total, so a caller
and the forecasts page cannot disagree about where the peak sits.
The window resolves rather than refusing. `--from` snaps back to that week's Monday,
`--months` clamps to 3–12, and the period defaults to weekly at six months or under
and monthly above. Every result states the window it actually covered, the rule it
bucketed by, when the forecast was built, and whether a deal has changed since.
`--deals`, `--roles`, `--tags` and `--status` narrow the picture to what they name.
There is no exclusion form of any of them.
Capacity here is demand. Estii holds no headcount, so a peak is a concentration of
scheduled work rather than a shortfall against a roster.
Pipeline forecasting is a Business feature. On the free plan these commands refuse
with the plan required and where to upgrade.
## Move a deal [#move-a-deal]
Once you know which deal drives a peak:
```bash
estii deal reschedule nd_abc123 +2w --dry-run
estii deal reschedule nd_abc123 +2w --yes
estii pipeline capacity --group deal
```
`reschedule` takes a signed offset (`+2w`, `-1m`, `+10d`) or an absolute
`YYYY-MM-DD` date, and resolves either to a Monday. It is not `deal set start=`: a
deal past draft is saved as a version before its dates change, and phases pinned to a
fixed date move by the same offset. It reports the new start, whether a version was
saved, and which phases moved. An archived or closed deal is refused; restore or
redraft it first.
## Check a deal before you send it up [#check-a-deal-before-you-send-it-up]
```bash
estii deal audit nd_abc123
estii deal audit nd_abc123 --all --json
```
What the deal's own numbers imply, ranked by how likely each finding is to be asked
about at approval: cost running ahead of billing and the day the gap peaks, a phase
running longer than its work needs and the recurring cost sitting in that window,
risk classified onto the thinnest-margin scope, a final payment large enough to fund
a dispute. Each finding carries the figures it rests on and the part of the deal to
open.
Most deals produce nothing, and that is the point rather than a failure: a review
that always finds something is not worth reading by the fourth deal. A quiet deal
says so and exits 0, as does one carrying too little to review.
The report always says how many checks passed; `--all` lists them with the value
each outcome rests on. Seeing what passed is what makes what failed worth acting
on. The JSON carries every check either way.
## See what has moved since you last shared it [#see-what-has-moved-since-you-last-shared-it]
```bash
estii deal versions nd_abc123
estii deal audit nd_abc123 --since v_xyz789
estii deal audit nd_abc123@v_xyz789
```
`--since` reports the deal against a save point: what the price, margin and duration
did, then the rules that explain them — a phase that arrived, work added inside the
phases already there, the same work priced differently, a discount that moved, the
dates sliding under a length that did not change. A move against an automatic
snapshot says so, because that is the rate card arriving rather than anyone touching
the deal.
`][@` is the other question: the deal as it stood at that save point,
audited on its own terms. Asking both at once is refused rather than guessed at.
Nothing here is stored or scheduled. The findings are recomputed from the deal each
time you ask, so they move when it moves, and the app's review panel answers with
the same figures in the same order.
## Move a deal through approval [#move-a-deal-through-approval]
```bash
estii deal request-approval nd_abc123 --to sam@acme.com --yes
estii deal approve nd_abc123 --probability 80% --yes
estii deal progress nd_abc123 --yes
estii deal close nd_abc123 won --reason "signed SOW" --yes
```
These are one-way. Re-running a step the deal has already taken is an error rather
than a no-op, and amending a close means `redraft` then closing again. Approving and
closing need the manager role. `estii deal --help` prints the legal transitions from
every state.
# Compose a space's pricing (/dev/cli/pricing)
## Compose a space's library [#compose-a-spaces-library]
A space prices from its library: the roles, products and streams work is estimated
against, the rate cards those are priced on, and the tags that group them. Standing
one up — a new space, a trial account, a rate card that arrived as a spreadsheet — is
the same write shape as everything else, one part at a time. The names below assume a
space that does not hold them yet: a name already taken is refused rather than added a
second time, because a name is how you address the part on the next command.
```bash
estii tag add name="Delivery" --yes
estii role add name="Senior Developer" cost=700 price=1400 tag=Delivery --yes
estii role add name="Junior Developer" cost=350 price=700 tag=Delivery --yes
estii product add name="Premium Support" margin=0.45 unit=seat --yes
estii stream add --data '{"name": "Delivery Pod", "allocations": [{"role": "Senior Developer", "amount": 2}, {"role": "Junior Developer", "amount": 1}]}' --yes
estii card add name="Rush" margin_med=0.6 --yes
estii role set "Senior Developer" --card Rush price=1800 --yes
```
A new role carries a rate on every card in the space, derived from that card's margin
band, so it prices the moment it exists. A card works the other way round: adding one
puts a derived rate on every role you already hold, and `--card` then names which of
them a cost or price lands on. Every part is named at creation, because a part with no
name is one you cannot address on the next command.
## Take something out [#take-something-out]
```bash
estii role remove "Junior Developer" --dry-run
estii role remove "Junior Developer" --map-to "Senior Developer" --yes
estii stream remove "Delivery Pod" "allocations/Senior Developer" --yes
estii role move "Senior Developer" --to top --yes
```
A role that a stream is composed of is refused, naming the streams that allocate it:
deleting it would silently recompose them. `--map-to` repoints those allocations onto
another resource and removes the role as one change, which is the choice the app
offers in the same situation. A stream's own lines are addressed inside it —
`allocations/][`, by allocation id or by the name of the role it targets — so
dropping one role from a pod does not mean restating the pod. Removing the stream
takes its lines with it. `move` positions a part among the parts of its own kind and
prices nothing.
Deals hold their own copy of all of this, so composing a library strands no deal: each
takes the change when `estii deal update` runs on it. That also means restructuring
has a price. Every deal still built on the old shape needs reconciling, and a resource
that is gone needs a mapping decision on each deal that used it, so change the library
for a reason you can name. `estii context` states that rule in the space's own terms,
and `estii deal list --updates` reports what is outstanding.
## Roll a library rate change into deals [#roll-a-library-rate-change-into-deals]
Deals hold their own copy of your rates, so a rate change does not reprice work you
have already quoted. Each deal takes it when you decide.
```bash
estii role set "Senior Developer" price=1400 --dry-run
estii role set "Senior Developer" price=1400 --yes
estii deal list --updates
estii deal update nd_abc123 --dry-run
estii deal update nd_abc123 --yes
```
`--updates` reports which deals are behind and for how long. `deal update` previews
every change and what it does to total price and margin before it applies. If a
resource needs mapping onto another, it writes nothing and hands back the address of
the panel where you decide.
## Fix pricing that is not a single rate [#fix-pricing-that-is-not-a-single-rate]
A role has one cost and one price on each card, so `role set` corrects it in place. A
product and a stream do not. A product prices from its own margin over a table of
volume tiers, and a stream has no rate at all: its cost and price are whatever the
roles it is made of add up to. Both are tables, so you read the current one out,
change what is wrong, and set the whole thing back.
```bash
estii product export --out - | jq '.products[] | select(.name == "Premium Support")' > support.json
estii product set "Premium Support" --data @support.json --dry-run
estii product set "Premium Support" --data @support.json --yes
estii stream set "Delivery Pod" --data '{"allocations": [{"role": "Senior Developer", "amount": 2}, {"role": "Designer", "amount": 1}]}' --dry-run
```
The exported entry is exactly what the write takes, so a round trip through an editor
or a script changes only what you touched. That includes which tier prices itself and
which one follows the margin: set a tier's price and it holds it, leave it alone and
it keeps moving with the product's margin, the way it does in the app.
Setting a table replaces it. A tier or an allocation you leave out is removed, so the
diff lists every removal by name before you confirm. A stream naming a role that does
not exist refuses the write whole rather than dropping that allocation.
There is no card to name on either. A product is not priced per card, and a stream
prices through the roles under it. Deals still hold their own copy, so this moves
nothing until `estii deal update` runs.
]