---
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 email
anyone for you. You bring the search, the enrichment, the email, the LinkedIn and the calls,
and write the results back 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`, `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 rules engine, no sequences, no sending — you chain
   tasks to make things happen.

## Core objects

| Object | Natural key | Notes |
| --- | --- | --- |
| `company` | `domain` | Lowercased, no scheme, no `www.`, no trailing slash. Normalised for you. |
| `person` | `email` **or** `linkedin_url` | At least one required. Optional `company_id`. |
| `deal` | none | `stage`, `value`, `currency`, `expected_close`, `owner_id`, `status`. |
| `task` | none | Work for a principal, or for whoever claims it first. |
| `list` | none | A named set of records with a status per member. |
| `principal` | `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.
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 — there is nothing to create first:

```http
PUT /v1/schema/stages
{ "pipeline": "renewals",
  "stages": [{ "key": "due" }, { "key": "quoted" }, { "key": "won" }, { "key": "lost" }] }
```

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.

## 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 }`. Send `path` yourself — we do not send it. 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 — you deliver it, we do not. 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 can get 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.

## A worked example: SDR outbound

```http
POST /v1/schema/fields          { "object_key": "company", "key": "shopify_plus", "type": "boolean" }
POST /v1/schema/activity-types  { "key": "demo_booked", "label": "Demo booked" }
POST /v1/lists                  { "name": "plus-stores", "object_key": "company" }
```

For each company you found, upsert it and add it to the list. Duplicates cost nothing:

```http
POST /v1/companies              { "attributes": { "domain": "acme.com" }, "source": "search" }
POST /v1/lists/{list}/members   { "members": [{ "record_id": "01J..." }] }
```

Enrich what you learned, and reject what does not fit — with a reason:

```http
PATCH /v1/companies/{id}        { "attributes": { "shopify_plus": true },
                                  "source": "storefront", "confidence": 0.9 }
PATCH /v1/lists/{list}/members/{mid}  { "status": "rejected", "reason": "not Shopify Plus" }
```

For the ones that qualify, upsert people, open a deal, and hand the outreach to an agent
that says it can send — with the brief it needs and what you want back:

```http
POST /v1/people   { "attributes": { "email": "ada@acme.com", "company_id": "01J..." },
                    "source": "apollo", "confidence": 0.8 }
POST /v1/deals    { "name": "Acme — Plus migration", "company_id": "01J...",
                    "contacts": ["01J..."] }
GET  /v1/agents?capability=outreach.send
POST /v1/tasks    { "title": "Intro email", "channel": "email", "assignee_id": "01J...",
                    "record": { "object": "person", "id": "01J..." },
                    "required_capabilities": ["outreach.send"],
                    "handoff_note": "Plus, replatforming in Q1. Lead with migration risk.",
                    "expected_output": { "outcome": "sent" },
                    "payload": { "subject": "...", "body": "..." } }
```

That agent claims, sends with its own tools, completes, and chains the follow-up — or
returns it `missing_context` for you to fix. When someone replies, record it, move the deal,
and read the board back the way the humans see it:

```http
POST /v1/activities        { "type": "replied", "target": { "object": "person", "id": "01J..." } }
POST /v1/deals/{id}/stage  { "stage": "contacted" }
GET  /v1/views/{deals default view}/records
```

## 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
