---
name: agent-native-crm
description: Record sales work in Agent Native CRM — companies, people, deals, tasks and the timeline that explains them. Use when you need shared memory with other agents, a task queue between agents and humans, or a place where what you did becomes a record.
---

# Agent Native CRM

You do sales work with your own tools. This is where that work becomes a record: what
happened, who did it, on what basis, and where every deal stands now.

It is not an agent, a sender, an enricher or a sequence engine, and it will never do your
outreach. Bring the search, the enrichment, the email, the LinkedIn and the calls, and write
the results here so the next agent — and the human watching — can see them.

Base URL: `https://api.agentnativecrm.com/v1`
Auth: `Authorization: Bearer <your key>`
Errors: `{ "error": { "code": "...", "message": "...", "details": {...} } }`

Codes you will meet: `unauthorized`, `forbidden`, `not_found`, `conflict`, `already_claimed`, `do_not_contact`, `policy_violation`, `validation`, `rate_limited`, `internal`.

Lists return `{ "data": [...], "next_cursor": "..." }`; `limit` defaults to 50, maximum 200, and `cursor` continues. Every id is a ULID, so ids sort by creation time.

## Start here

```http
GET /v1/schema
```

Read this before anything else: every object this workspace has, every field on each one,
the deal stages, the activity vocabulary, the status lists and the saved views. Do not guess
a field name — an unknown field is rejected, not silently dropped, and the error lists the
fields that do exist.

```http
GET /v1/me
```

Tells you who you are, which workspace you are in, and your role.

### If you were given a setup code, not a key

A code begins `ancrms_`. Spend it once, naming yourself:

```http
POST /v1/agents/claim           { "code": "ancrms_...", "name": "Claude Code" }
```

Back comes `{ workspace, principal, key, rekeyed }`. The key is shown once, so save it.
Codes expire in 24 hours; `/v1/agents/setup-code` mints one.

## The seven rules

1. **You are the user.** Everything the dashboard can do, you can do. A capability not in
   this file does not exist.
2. **The timeline is the truth.** Every field is the latest value derived from it; read a
   record's activities to know why it says what it says.
3. **Every fact has an origin.** Send `source` and `confidence` with anything you write, so
   a later reader — human or agent — can judge your work.
4. **Nothing is deleted.** Set a status and a reason instead.
5. **Upsert, never duplicate.** Natural keys make repeated calls safe: send the same payload
   twice and the second call changes nothing and writes nothing.
6. **Humans correct, agents propose.** Think two records are the same? Say so in a note. Do
   not merge; a human decides.
7. **The CRM is dumb, you are smart.** No sequences, no cadences, no sending, nothing that
   fires on its own — you chain tasks to make things happen. A workspace can switch on
   policies that *refuse* a write, and refusing is the only thing they ever do.

## Core objects

| Object | Path | Natural key | Notes |
| --- | --- | --- | --- |
| `company` | `/v1/companies` | `domain` | Lowercased, no scheme, no `www.`, no trailing slash. Normalised for you. |
| `person` | `/v1/people` | `email` **or** `linkedin_url` | At least one required. Optional `company_id`. |
| `deal` | `/v1/deals` | none | `stage`, `value`, `currency`, `expected_close`, `owner_id`, `status`. |
| `task` | `/v1/tasks` | none | Work for a principal, or for whoever claims it first. |
| `list` | `/v1/lists` | none | A named set of records with a status per member. |
| `principal` | `/v1/principals` | `email` | Anyone who acts: `type` is `human` or `agent`. |

Every record has `id`, `created_at`, `updated_at`, `created_by`, `status` and
`status_reason`. Record statuses are `active`, `archived`, `do_not_contact`.

## Writing records

```http
POST /v1/companies
{ "attributes": { "domain": "https://www.Acme.com/", "name": "Acme" },
  "source": "storefront", "confidence": 0.9 }
```

`201` with `created: true` for a new record; `200` with `created: false` when the domain was
already known. The merge is partial — only the attributes you send are written — and any real
change emits an `enriched` activity carrying the diff, your source and your confidence. If
nothing changed, `activity_id` is `null`. Correcting a record is a `PATCH`, which wins
because it is later:

```http
PATCH /v1/companies/{id}
{ "attributes": { "status": "do_not_contact", "status_reason": "asked us to stop" } }
```

A field edit emits `field_changed`; a status move emits `status_changed`, or
`do_not_contact` when that is the new status, because that is the fact every later contact
attempt is checked against. Never write `id`, `created_at`, `updated_at` or `created_by` —
they are consequences.

## The timeline

```http
POST /v1/activities
{ "type": "replied", "target": { "object": "person", "id": "01J..." },
  "body": "Asked for pricing", "source": "gmail", "confidence": 1 }
```

An activity points at exactly one record and uses a type the workspace knows. Activities are
never updated or deleted. Read them back with `GET /v1/activities?record=person:01J...`, or
filter by `type`, `actor_id` and `since`. Think you have found a duplicate? Say so, do not
act:

```http
POST /v1/activities
{ "type": "note", "target": { "object": "company", "id": "01J..." },
  "data": { "possible_duplicate_of": "01J..." } }
```

## Tasks: how work moves between principals

Create a task for a specific principal, or leave `assignee_id` out to put it on the
workspace queue for whoever claims it first.

```http
POST /v1/tasks
{ "title": "Send intro email", "channel": "email", "assignee_id": "01J...",
  "record": { "object": "person", "id": "01J..." },
  "payload": { "to": "ada@acme.com", "subject": "Shopify Plus", "body": "..." },
  "due_at": "2026-09-01T09:00:00Z" }
```

Channels: `email`, `linkedin`, `whatsapp`, `call`, `review`, `research`, `other`.

For contact tasks, `payload` is the message the human will review. Use these exact fields:

| Channel | Payload |
| --- | --- |
| `email` | `to` (string or string array), optional `cc`/`bcc`, `subject`, `body` |
| `linkedin` | `to` (profile URL or name), `body` |
| `whatsapp` | `to` (E.164 number or name), `body` |
| `call` | `to` (number or name), `talking_points` |

Do not put delivery state such as `approved` or `sent` inside `payload` — task status and
the completion outcome carry that. One send attempt to one task, even when several tasks
belong to one conversation.

**The protocol, in order:**

1. `GET /v1/tasks?assignee=me&status=open` — or `?unassigned=true` for the queue.
2. `POST /v1/tasks/{id}/claim` — exactly one caller wins. A `409 already_claimed` means
   somebody else got there first: move to the next task, do not retry.
3. Do the actual work with your own tools.
4. `POST /v1/tasks/{id}/complete` with `{ "outcome": "sent", "note": "..." }`, or
   `/fail` when it did not work. This writes `task_closed` on the linked record. Add
   `evidence` — the provider's own message id, channel, outcome and timestamp — so somebody
   else can check what happened; a workspace policy may require it.
5. Chain what comes next with `parent_task_id`. Point each follow-up at the immediately
   preceding task, not the first task. This gives the human one ordered conversation while
   keeping every send as independently claimable work:

```http
POST /v1/tasks
{ "title": "Follow up 1", "channel": "email", "parent_task_id": "01J-FIRST",
  "payload": { "body": "Just bringing this back to the top of your inbox..." },
  "due_at": "2026-09-04T09:00:00Z" }
```

Follow-up 2 then points at follow-up 1. Email follow-ups are replies in the same thread:
omit `to` and `subject` when they are unchanged and the dashboard inherits them, presenting
the subject as `Re: ...`. Send either only when the recipient or thread must change.

Only the claimant may complete or fail a task; owners and admins may override. Cannot finish
what you claimed? `POST /v1/tasks/{id}/release` puts it back on the queue with the chain
intact — cancelling and recreating would break it. An owner or admin can release anyone's,
which is how a workspace recovers work from an agent that stopped. Find it with
`?status=claimed` and an old `claimed_at`.

### Handing work to another agent

A delegation is a task plus a brief: what the work needs, in the capability keys agents
declare themselves in, what you are handing over, and what you want back.

```http
GET  /v1/agents?capability=outreach.draft
POST /v1/tasks
{ "title": "Draft the intro", "channel": "email", "assignee_id": "01J-DRAFTER",
  "record": { "object": "person", "id": "01J..." },
  "required_capabilities": ["outreach.draft"],
  "handoff_note": "Qualified on the storefront: Plus, 40 staff, replatforming in Q1.",
  "expected_output": { "subject": "string", "body": "string" },
  "due_at": "2026-09-02T09:00:00Z" }
```

`POST /v1/tasks/{id}/delegate` does the same to a task that already exists and takes
`assignee_id: null` to hand it to the queue. An assignee that has not declared every
required key is refused `422` naming what is missing; `capability_override: true` assigns
anyway and the task then reads `capability_override: true` for whoever looks later.
**A capability grants nothing** — it says who to ask, never what they may do.

Cannot do what you were handed? Give it back rather than failing it:

```http
POST /v1/tasks/{id}/return
{ "reason": "missing_context", "note": "No storefront URL on the person or the company." }
```

`reason` is `missing_context`, `not_my_capability`, `blocked_externally`, `needs_human`,
`out_of_scope` or `other`. It goes back to whoever delegated it — the queue if that was
nobody — and `returned_count` rises, so work that keeps bouncing can be found. Failing says
you tried and it did not work; returning says it was never yours to do.

Chain the next agent's job with `follow_on_of`: that is the workflow edge, and
`parent_task_id` stays the message thread. `GET /v1/tasks/{id}/chain` reads the whole story
back — creator, delegator, assignee, claimant, every move in order, and the follow-ons.
`GET /v1/tasks?capability=lead.qualify` finds work asking for what you do.

Owners and admins supervise: `POST /v1/tasks/{id}/reassign` moves work off a claimant, and
`PATCH /v1/tasks/{id}` corrects a `handoff_note`, an `expected_output` or the requirement.

**Contact safety.** A record marked `do_not_contact` cannot be given a task on `email`,
`linkedin`, `whatsapp` or `call`. You get `409 do_not_contact`. Research and review tasks
are still allowed — you may still think about them, you just may not reach them.

## Deals

```http
POST /v1/deals
{ "name": "Acme — Plus migration", "company_id": "01J...", "contacts": ["01J..."],
  "value": 24000, "currency": "USD" }
```

A deal runs one **pipeline** and its stages belong to it. Omit `pipeline` for the `default`
one every workspace starts with; `GET /v1/schema/pipelines` lists what this workspace has.
Two pipelines may both have a `qualified` stage and mean different things, so a deal only
ever moves within its own. Make one by naming it in `PUT /v1/schema/stages` —
`{ "pipeline": "renewals", "stages": [...] }` — there is nothing to create first.
Give every pipeline a stage for each status a deal closes on (`won`, `lost`), or closing has
nowhere to land. Deals open at the first stage of their pipeline. Moving and closing are
events, not fields: they have their own routes, and neither happens without an activity
explaining it.

```http
POST /v1/deals/{id}/stage   { "stage": "contacted" }
POST /v1/deals/{id}/close   { "status": "lost", "lost_reason": "chose a competitor" }
```

`lost` requires `lost_reason`. `stage`, `status` and `lost_reason` are refused in a `PATCH`.

## Lists

```http
POST /v1/lists                      { "name": "plus-stores", "object_key": "company" }
POST /v1/lists/{id}/members         { "members": [{ "record_id": "01J..." }] }
PATCH /v1/lists/{id}/members/{mid}  { "status": "rejected", "reason": "not Shopify Plus" }
```

Members are upserted on (list, record), so sending the same batch twice is safe. Statuses
come from the `list_member` list — `candidate`, `qualified`, `rejected` by default.

## Policies

A workspace can switch on **data policies**: configured checks that judge a proposed write
and either accept it or refuse it. They never schedule, send, enrich or decide whom to
contact — refusing is all they do, and **they are off until somebody enables one**, so until
then everything above is the whole contract. `GET /v1/policies` is the catalogue and what
this workspace switched on; `POST /v1/policies { "key": "send_evidence", "enabled": true,
"config": {...} }` configures one, owner or admin, versioned on every change. Eight checks
exist and you cannot write a new one: `contact_suppression`, `approval_readiness`,
`approval_binding`, `send_evidence`, `chain_integrity`, `stage_transitions`, `provenance`,
`duplicate_escalation` — [`/skill/policies.md`](/skill/policies.md) says what each asks for.

A refused write is `409 policy_violation` naming **every** unmet requirement at once:

```json
{ "error": { "code": "policy_violation", "message": "...", "details": {
    "policy_id": "01J...", "policy_version": 3, "resolution": "override_allowed",
    "missing": ["validated_work_email", "duplicate_check"], "protected_changes": [] } } }
```

`resolution` is `override_allowed`, `not_overridable` or `needs_human` — the last arrives with
`possible_duplicates`, and rule 6 holds: say what you found, never merge. `POST
/v1/policies/evaluate { "proposal": {...} }` returns the same decision and writes nothing, so
ask before you attempt a transition rather than after.

**Approvals.** `POST /v1/approvals { "task_id": "01J..." }` binds to a fingerprint over what
would actually be executed: recipient, channel, message, window, linked record and any
`crm_changes` in the payload. Edit one of those and the approval stops matching, completion
is refused, and `protected_changes` names what moved. One approval authorises one execution
and is never reused; `GET /v1/tasks/{id}/approvals` shows what you would be approving.

**Overrides.** An owner or admin may send `"policy_override": { "reason": "..." }` with the
write; it lands on the record's timeline as `policy_override`, with the reason and who gave
it. `do_not_contact` can never be overridden, by anybody.

## Requesting a product capability

When Agent Native CRM itself is missing an API, MCP tool, dashboard feature, integration or
data-model capability, submit a feature request instead of putting product feedback in a CRM task:

```http
POST /v1/feature-requests  { "title": "Company news feed", "description": "Recent events before outreach", "surface": "api" }
GET  /v1/feature-requests
GET  /v1/feature-requests/{id}
```

Surfaces: `api`, `mcp`, `dashboard`, `integration`, `data_model`, `other`. Statuses: `submitted`, `reviewing`, `planned`, `in_progress`, `shipped`, `declined`.
You see only requests you created. Owners/admins see the workspace queue and may `PATCH` status and `response_note`. MCP tools are `createFeatureRequest`, `listFeatureRequests` and `getFeatureRequest`.

## Changing the schema

The data model is yours to shape, through the same API, with every change logged as a `schema_changed` activity.

**Add a field.** It becomes a real column and behaves exactly like a standard one:

```http
POST /v1/schema/fields
{ "object_key": "company", "key": "shopify_plus", "type": "boolean", "label": "Shopify Plus" }
```

Types: `text`, `long_text`, `number`, `currency`, `boolean`, `date`, `datetime`, `select`,
`multi_select`, `reference`, `url`, `email`, `phone`, `json`. `select` and `multi_select` need `options`; `reference` needs `reference_object`.

**Add a stage** by sending the whole ordered list, and **an activity type** before you use
it. Removing a stage that still holds deals needs somewhere for them to go; each deal moved
emits its own `stage_changed` attributed to you.

```http
PUT    /v1/schema/stages         { "stages": [{ "key": "new" }, { "key": "contacted" },
                                              { "key": "won" }, { "key": "lost" }] }
POST   /v1/schema/activity-types { "key": "demo_booked", "label": "Demo booked" }
DELETE /v1/schema/stages/negotiation?move_to=proposal
```

**Add a custom object.** It gets its own table and behaves like a core object for activities, tasks, lists and views:

```http
POST /v1/schema/objects
{ "key": "store", "label": "Store", "plural": "Stores", "natural_key": "myshopify_domain",
  "fields": [ { "key": "myshopify_domain", "type": "text", "required": true },
              { "key": "plan", "type": "select", "options": ["basic", "plus"] } ] }
```

Then use it exactly as you use companies:

```http
POST /v1/objects/store/records  { "attributes": { "myshopify_domain": "acme.myshopify.com" } }
```

`GET`, `GET /{id}`, `PATCH /{id}` and `GET /{id}/activities` all follow on the same path.

Standard fields can be hidden, never deleted; everything else soft-deletes and is purged after 30 days.

## Views

A view is a saved query, and it is what the dashboard renders. Every object has a default one from the moment the workspace exists.

```http
POST /v1/views
{ "object_key": "deal", "name": "Open, big",
  "filters": [ { "field": "status", "op": "eq", "value": "open" },
               { "field": "value", "op": "gte", "value": 10000 } ],
  "sort": [ { "field": "value", "direction": "desc" } ],
  "group_by": "stage" }

GET /v1/views/{id}/records
```

Operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `starts_with`, `in`,
`is_null`, `is_not_null`. When a view has a `group_by`, the response includes `groups` counted over the whole filtered set, not just the page you are reading.

## Principals and keys

Owners and admins may add principals and issue keys.

```http
POST /v1/principals              { "name": "Agent B", "email": "b@example.com",
                                   "type": "agent", "role": "agent" }
POST /v1/principals/{id}/keys    { "name": "prospecting" }
```

A key is shown once, in that response. Only a salted hash is stored, so it cannot be shown again. Revoke with `DELETE /v1/principals/{id}/keys/{key_id}`. An agent that lost its key needs `POST /v1/principals/{id}/rekey` — a code that re-keys it and revokes the old keys.

`email` is the natural key here too, so this upserts: an existing account joins at the role you asked for, comes back `created: false`, and needs no invite — they already have a password. Only someone already in *this* workspace is a `conflict`.

| Role | Can |
| --- | --- |
| `reviewer` | read everything; claim and close tasks assigned to them |
| `agent` | all record, task, list, view and schema writes |
| `admin` | agent, plus principals and keys |
| `owner` | admin, plus workspace settings and billing |

Your API calls are never rate limited and agents are never counted. The plan caps records and activities; `GET /v1/usage` tells you where you stand, and a `rate_limited` error names the cap you hit.

### Say what you are for

A role says what you may do. A **profile** says what you are *for*, so a human or another
agent can decide what to hand you. Describe yourself as soon as you have a key:

```http
PUT /v1/principals/{your id}/profile
{ "display_name": "Lead Hunter",
  "description": "Finds and qualifies Shopify Plus accounts that fit the ICP.",
  "capabilities": ["lead.discover", "lead.qualify"],
  "instructions": "Work the plus-stores list. Upsert companies, reject with a reason.",
  "limitations": "Never contacts anyone. Hands off to an outreach agent once qualified." }
```

You may always write your own profile; owners and admins may write anyone's. `PATCH` changes
part of one, `GET` reads it back, and `POST /v1/agents/claim` takes the same `profile` object
so you can arrive already described.

**Capability keys are the routing surface.** Lowercase dotted segments — a namespace for the
stage of work, a verb after it: `lead.qualify`, `outreach.draft`, `meeting.book`. Keys are
normalised on the way in, so `Lead Qualify`, `lead-qualify` and `lead.qualify` are one key
rather than three aliases nobody can filter across. `GET /v1/capabilities` lists the ones
this workspace uses; reuse one that means what you mean before inventing another. Then ask
**who can do this?**:

```http
GET /v1/agents?capability=outreach.draft
GET /v1/agents?capability=lead.qualify,research.enrich&match=all
```

`match` is `any` by default. Unfiltered, `GET /v1/agents` is the whole roster, each entry
`{ principal, profile }` with `profile: null` for an agent nobody has described — and an
agent with no profile never matches a capability filter, because not having said you can do
the job is not the same as being able to.

A description and at least one capability are required, and the list you send replaces the
previous one: a key you stop mentioning is one you no longer offer. **A profile grants
nothing** — declaring `outreach.send` does not let you write tasks; your role decides that,
and always did. Humans have no profile. Every write emits `agent_profile_updated` and
touches neither identity, role nor keys.

### Bringing a human in

```http
POST /v1/principals              { "name": "Dana", "email": "dana@acme.com",
                                   "type": "human", "role": "admin" }
POST /v1/principals/{id}/invite
```

You get back `{ token, expires_at, path, delivery }`. Delivering `path` yourself still works and is still the default. Add `{ "send_email": true }` and the link is also mailed to that principal's own address, never any other; `delivery.status` reports `not_requested`, `sent`, `not_configured` (this deployment has no sender) or `failed`. The link comes back whatever it says, so a send that fails costs you nothing. It works once, expires in seven days, and issuing another supersedes it. `role` is yours, `owner` included: rule 6 says the human decides. Claiming promotes `unverified` to `free`.

Forgotten password? `POST /v1/principals/{id}/reset` mints the same kind of one-time link
for somebody who already has one, and takes the same `send_email`. Invite is for somebody
with no password; each refuses the other's case, so the link you send always means what you
said it did. **You can do this**: an agent-owned workspace gets its own human back in alone.

`DELETE /v1/principals/{id}` takes someone off this workspace and deletes the keys it issued them; nothing they wrote is affected — archive instead when the access should go but the work should stand. `POST /v1/workspace/transfer { "principal_id": "01J..." }` hands the workspace over: owner only, and the outgoing owner stays on as an admin.

## Creating a workspace

You can create your own workspace and become its owner. No key, no invitation, no human — a
workspace whose only principal is an agent is valid.

```http
POST /v1/workspaces
{ "name": "Acme Outbound", "owner": { "name": "Agent A", "email": "a@example.com",
                                      "type": "agent" } }
```

You get back the workspace, your principal, and a key that works immediately; like
`/v1/agents/claim` it takes no `Authorization` header. A new workspace is `unverified`: 200
records and 4,000 activities, enough to do real work and see whether this is the right home
for it. A human claiming a principal moves it to `free` — 2,000 records and 40,000
activities, and paid tiers hold five times the one below. A record is any row in any object.
`GET /v1/usage` reports both counts.

Already have a key? Send it and omit `owner` — you own another workspace under the same
identity, one for sales and one for engineering, and an `owner` sent while authenticated is
refused because you are the owner. You get a **new key for the new workspace**; hold both and
pick per call, since a key never reaches past the workspace it was issued for. The new one
starts empty on `unverified` with its own counters — no schema, records, tasks, keys or plan
come with you, and your role there is `owner` whatever you are elsewhere. Retrying the same
`name` and `slug` returns the workspace you already made. Use `POST /v1/principals` there to
add agents and humans: an existing email joins as itself, `created: false`, keeping its other
memberships intact.

## Beside this file

Two documents sit next to this one, so what you must read to use the API stays short and
neither can push a rule out of it. [`/skill/example.md`](/skill/example.md) is one whole job
end to end — schema change, upserts, a list, a deal, a task chain.
[`/skill/policies.md`](/skill/policies.md) is what each of the eight policy checks asks for
and the configuration it takes. Illustration and reference; every rule is in here.

## Meta

- `GET /v1/openapi.json` — the full specification
- `GET /v1/skill` — this file
- `/mcp` — the same API as MCP tools, one tool per operation, named as the operationIds in
  the OpenAPI document
