REST API
Product semantics, not table semantics
There is no generic row endpoint, because a generic row endpoint pushes every business rule out to the caller and leaves the two of us disagreeing about what a won opportunity means.
Conventions
Decided once, so you never have to ask
83 endpoints over the same objects the MCP tools reach, with the same rules and the same error codes. One handler covers both.
- Identifiers
- Opaque and prefixed, so a value tells you what it is.
- Time
- UTC everywhere. A workspace carries a display timezone; storage never converts.
- Money
- Integer minor units plus an ISO currency code. No floating point, ever.
- Updates
- Name exactly the fields they mean. An unknown field is refused, not ignored.
- Lists
- Cursor paginated and bounded. No endpoint can return an unbounded set.
- The spec
- OpenAPI generated from the same contracts the API validates against.
Failure
Every error carries a recovery, not just a status
The code set is fixed, and REST and MCP return the same codes with the same hints. These are the ones that come up in practice.
| Code | What happened | What to do |
|---|---|---|
ambiguous_identity | Upsert matched multiple plausible records. Nothing was committed. | Inspect candidates and choose one explicitly by id, or set createDistinct to create a separate record. |
duplicate_identity | A strong unique identity already belongs to another record. | Use the existing record, correct the identity, or merge the two records. |
stale_version | expectedVersion does not match the current record. Nothing was overwritten. | Re-read the current record and apply the intended change again. |
invalid_stage_transition | Target stage does not belong to this record's current pipeline. | Read the pipeline schema and choose a stage from the same pipeline. |
idempotency_conflict | The same idempotency key was reused for materially different input. | Use the original result, or a new key for a new intent. |
not_reversible | This change cannot be safely compensated. | Apply an explicit corrective mutation. Creations are undone by archiving, and a merge is undone by unmerge. |
rate_limited | Infrastructure protection tripped. This is not a product limit. | Retry after the supplied delay. |
quota_exceeded | The workspace reached an entitlement quota for the current period. | Open billing to raise the plan, or wait for the period to reset. Retrying the same request will not succeed. |
Two walls, two meanings
A rate limit and a quota are not the same refusal
A rate limit protects infrastructure: 1,200 requests a minute per workspace, bursting to 30 a second, and 600 a minute per credential. Size your scheduler against those. A bulk population run fits inside them, so hitting one means something is wrong rather than that you are a good customer.
A quota is what your plan includes. They return different codes because the recoveries differ: one means wait, the other means upgrade, and retrying it unchanged can never succeed.
You do not have to be refused to find out where you are. Every response
carries ratelimit-remaining and ratelimit-reset,
and a refusal adds retry-after in seconds.
If you would rather be told than ask, Indraft posts signed webhooks for every change and serves a cursor-paginated change feed for jobs that prefer to poll.
rate_limited- Retry after the delay we hand you. Nothing is wrong with the request.
- Workspace ceiling
-
One workspace is one database with a hard ceiling of
9.0 GB, which measures out to
roughly 3,500,000 companies and
10,000,000 records including their contacts,
opportunities, activity, and complete change history. At the ceiling writes are
refused with
workspace_capacityand everything already there stays readable, exportable, and deletable. We tell you at 80 percent, not at 100. quota_exceeded- The plan is full for this period. Retrying the same call unchanged can never succeed, so it is marked non-retryable and names the metric that ran out.
Next
Build against it
Sign up and connect over MCP, which authorizes your own service the same way it authorizes an agent. The generated reference lists every endpoint, and safe writes covers the mutation envelope every write accepts.
A credential never outranks its author
Every credential carries the intersection of its scopes with the current role of the person behind it, read live at request time. Demote that person and its power falls on the very next call rather than whenever a token happens to expire.