Skip to content
indraft
Start free

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_capacity and 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.