HTTP API
Base URL: https://undocrm.com/v1. JSON in, JSON out. Auth: Authorization: Bearer <key>.
The agent endpoints have the same semantics as the MCP tools, but return structured JSON — use them from backends, queues and agent frameworks without MCP.
Errors look like:
{ "error": { "code": "invalid_input", "message": "Could not read \"@jane\" as a person. …" } }
| Status | Code |
|---|---|
| 400 | invalid_input |
| 401 | unauthorized — missing, invalid or revoked key |
| 403 | forbidden — e.g. an agent key on an owner endpoint |
| 404 | not_found |
| 409 | conflict — e.g. undoing something twice |
Pagination
Lists page with keyset cursors, so every page is as fast as the first, even at millions of rows. Pass limit (default 50, max 200); the response includes next_cursor, which you send back unchanged as ?cursor= for the next page. next_cursor: null means you've reached the end.
curl "https://undocrm.com/v1/contacts?limit=100" -H "Authorization: Bearer $KEY"
# → { "contacts": [...], "next_cursor": "eyJ0IjoiMjAyNi0xMC0xNyAwMjo1NDozMi44NjQ3MjMrMDAiLCJpZCI6ImN0X..." }
curl "https://undocrm.com/v1/contacts?limit=100&cursor=eyJ0Ijoi..." -H "Authorization: Bearer $KEY"
| List | Sorted by | Extra filters |
|---|---|---|
GET /contacts |
last activity, newest first | lifecyclestage (or none) |
GET /contacts?q= |
relevance (cursor is an offset; results are capped at a few hundred) | |
GET /follow-ups |
due date, soonest first | due, mine, person |
GET /deals/column |
last updated, newest first | pipeline, dealstage (required) |
GET /contacts/:id/timeline |
newest first: interactions older than the cursor from GET /contacts/:id, with the events in the same window |
|
GET /audit |
newest first; pages with before=<id> |
contact_id, agent_id |
Totals for headers come from cheap count endpoints: GET /contacts/groups (contacts per lifecycle stage), GET /follow-ups/counts (overdue / today / upcoming), and the totals in GET /deals/board.
Authentication
| Caller | Header | Reaches |
|---|---|---|
| Agent | Authorization: Bearer ucrm_agt_… |
Workspace endpoints (as that agent), MCP |
| Backend, one workspace | Authorization: Bearer ucrm_own_… |
Workspace endpoints, including administration |
| Backend, organization | Authorization: Bearer ucrm_org_… |
POST/GET /workspaces |
| Signed-in person | Authorization: Bearer <session token> and X-Workspace-Id: ws_… |
Account endpoints; workspace endpoints for workspaces in their organizations, by role |
Workspaces for your customers — org key
POST /workspaces
curl -X POST https://undocrm.com/v1/workspaces \
-H "Authorization: Bearer $ORG_KEY" -H "content-type: application/json" \
-d '{"name": "Acme Recruiting"}'
{ "workspace": { "id": "ws_cd7b82dcfdcc4ed0", "name": "Acme Recruiting", "org_id": "org_…", "created_at": "…" },
"owner_key": "ucrm_own_…" }
The owner key is shown once; use it to create that workspace's agents. GET /workspaces lists your organization's workspaces.
Accounts — signed-in people
These power the web app. All take a signed-in session token; none take API keys.
| Method | Path | Body | |
|---|---|---|---|
| GET | /me |
You, your organizations (with role and workspaces), invites waiting for you | |
| PATCH | /me |
{name} |
|
| POST | /orgs |
{name, workspace_name?} |
New organization with a first workspace; you're its owner |
| PATCH | /orgs/:org |
{name} |
Admin |
| GET | /orgs/:org/members |
Members and pending invites | |
| PATCH | /orgs/:org/members/:user |
{role} |
Admin; only owners change owners |
| DELETE | /orgs/:org/members/:user |
Admin, or yourself to leave. The last owner can't leave | |
| POST | /orgs/:org/invites |
{email, role} |
Admin. Returns {url, emailed} |
| DELETE | /orgs/:org/invites/:id |
Admin | |
| GET | /invites/:token |
Public preview: organization, inviter, masked email | |
| POST | /invites/:token/accept |
Must be signed in as the invited email | |
| GET/POST | /orgs/:org/workspaces |
{name} |
List; create (admin) |
| PATCH | /orgs/:org/workspaces/:ws |
{name} |
Admin |
| GET/POST/DELETE | /orgs/:org/keys[/:id] |
{label} |
Org API keys (admin) |
| GET | /orgs/:org/audit |
?before |
Organization-level changes (admin) |
Workspace administration — owner key or admin
| Method | Path | Body | |
|---|---|---|---|
| GET | /workspace |
Name, your role, counts. Works with any key. | |
| GET | /agents |
Agents with key prefixes, last use, activity | |
| POST | /agents |
{name, description?} |
Returns {agent, api_key}; key shown once |
| POST | /agents/:id/keys |
Another key for an agent (rotation) | |
| POST | /owner-keys |
{label?} |
Another owner key |
| DELETE | /keys/:id |
Revoke; effective within 30s | |
| GET | /audit |
?limit&before&contact_id&agent_id |
Newest first; page with before=<id> |
| POST | /audit/:id/undo |
Reverse one action | |
| DELETE | /contacts/:id/do-not-contact/:blockId |
Lift a block (humans only) |
People — any key
| Method | Path | Body / query | |
|---|---|---|---|
| GET | /contacts |
?q&limit |
Search; without q, most recently active |
| POST | /contacts |
{person?, identities?, name?, title?, company?, facts?} |
Create/update/merge. 201 if created |
| GET | /contacts/:person |
Full record: identities, interactions, facts (with history), follow-ups, blocks, claim, audit | |
| POST | /contacts/:person/do-not-contact |
{reason, channel?} |
:person accepts any person string, URL-encoded (/contacts/jane%40acme.com).
Deals and pipelines — any key
| Method | Path | Body / query | |
|---|---|---|---|
| GET | /pipelines |
Pipelines with ordered stages (key, label, probability, is_closed) |
|
| POST | /pipelines |
{label, stages: ["Applied", …]} |
|
| GET | /deals |
?pipeline&dealstage&person&open=true |
|
| POST | /deals |
{person?, dealname, pipeline?, dealstage?, amount?, closedate?, currency?, company?} |
|
| GET | /deals/:id |
||
| PATCH | /deals/:id |
any of the above | Stage key or label |
| DELETE | /deals/:id |
Archive (undoable) | |
| GET | /properties |
The property registry, with usage counts | |
| GET | /interactions/search |
?q= |
Full-text search over every conversation |
| GET | /overview |
Owner key: 14-day activity, agents, pipeline funnel (powers the web home page) |
Agent operations — agent key (owner keys can read)
GET /brief?person=&purpose=&max_tokens=
{ "text": "# Jane Doe — VP Engineering, Acme …", "contact_id": "ct_…", "tokens": 172,
"omitted": { "facts": 0, "interactions": 0, "follow_ups": 0 } }
POST /can-contact
{ "person": "jane@acme.com", "channel": "email", "purpose": "send SOC2 report" }
{ "allowed": true, "reason": "ok", "contact_id": "ct_…", "channel": "email", "block": null,
"claim": { "id": "cl_…", "agent_id": "ag_…", "agent_name": "sdr-1", "purpose": "send SOC2 report", "expires_at": "…" },
"last_outbound": { "channel": "email", "occurred_at": "…", "agent_name": "sdr-1" } }
reason is one of ok, do_not_contact, claimed_by_other_agent.
POST /release
{ "person": "jane@acme.com" } → { "contact_id": "ct_…", "released": true }
POST /interactions
Body as in log_interaction, plus optional source (default agent; sync integrations set their own, e.g. gmail). Returns 201:
{ "duplicate": false, "interaction_id": "ix_…", "contact_id": "ct_…", "created_contact": false,
"facts": [{ "id": "fa_…", "key": "budget", "value": "$80k/yr", "previous": "$50k/yr" }],
"follow_ups": [{ "id": "fu_…", "action": "Send the SOC2 report", "due_at": "…" }],
"completed_follow_ups": [], "warning": null }
A repeated external_id (per source) returns { "duplicate": true, … } with 200.
GET /follow-ups?due=&mine=&person=&limit=
due: overdue, today (default), week, all. mine defaults to true for agent keys.
POST /follow-ups/:id/complete
{ "status": "done" | "cancelled", "outcome": "…" }