API documentation
Relm is API-first. Everything you can do in the dashboard, an agent can do over REST or the native MCP server - with the same bearer key.
Base URL & auth
The API lives at https://api.relmcrm.com. Authenticate every request with a workspace-scoped bearer key. Keys are shown once, SHA-256 hashed at rest, and come in live and test variants.
Authorization: Bearer relm_live_...
Mint keys in the dashboard. relm_test_ keys write to an isolated test dataset that is free and invisible to billing and the dashboard - build against them freely. Test mode is a sandbox, not storage: test records are automatically deleted 7 days after they are created.
Quickstart
Create your first contact in one call:
curl https://api.relmcrm.com/v1/contacts \
-H "Authorization: Bearer relm_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "first_name": "Ada", "last_name": "Lovelace" }'
Response:
{
"id": "con_x8f3k2m9q2",
"object": "contact",
"email": "[email protected]",
"first_name": "Ada",
"last_name": "Lovelace",
"created_at": "2026-07-09T12:00:00.000Z"
}
TypeScript SDK
Prefer types over curl? The official SDK is a zero-dependency wrapper over the same API.
npm i relmcrm
import { Relm } from "relmcrm";
const relm = new Relm(process.env.RELM_KEY!);
const ada = await relm.contacts.create({ email: "[email protected]", first_name: "Ada" });
await relm.deals.create({ title: "Acme - annual", stage: "lead" });
// page through everything
for await (const c of relm.contacts.all()) console.log(c.id, c.email);
Failed calls throw a RelmError you can read to self-correct: e.status, e.validOptions, e.hint. Works in Node 18+, Bun, Deno and the browser. On npm: npmjs.com/package/relmcrm.
Conventions
- Prefixed IDs -
con_contacts,cmp_companies,deal_deals,act_activities. Self-describing and copy-paste safe. - Envelope - list endpoints return
{ "object": "list", "data": [...], "has_more": true, "next_cursor": "..." }. - Cursor pagination - pass
?limit=100&cursor=.... Keyset over(created_at, id), stable under writes.limitdefaults to25and is capped at100(larger values clamp). - Search & filter - list endpoints take
?q=for a case-insensitive substring match (contacts: name, email, phone, LinkedIn; companies: name, domain; deals: title), plus exact filters like?company_id=,?stage=,?pipeline=(alias?pipeline_id=). An unrecognized filter is rejected with400+valid_options, so you never silently get unfiltered results. - Idempotency - send an
Idempotency-Keyheader on a create; a retry returns the original record instead of duplicating. - Optimistic concurrency - every record carries a
version; sendIf-Matchto guard against lost updates. A stale write returns412 version_conflict. - Modes - the key decides
testvslive; data never crosses.
Core objects
Contacts, companies, deals and activities have full CRUD at /v1/<object> with matching MCP tools (relm_create, relm_list, relm_get, relm_update, relm_delete). The remaining surfaces have purpose-built endpoints and tools.
| Object | Endpoint | What it is |
|---|---|---|
| Contact | /v1/contacts | People. Email-optional - phone or LinkedIn-only leads are fine. |
| Company | /v1/companies | Accounts. Contacts and deals link to them. |
| Deal | /v1/deals | Opportunities in a pipeline + stage. Carries an amount. |
| Activity | /v1/activities | Notes, calls, emails, meetings. Backdatable via occurred_at. |
| Pipeline | /v1/pipelines | Named pipelines, each with ordered stages. |
| Automation | /v1/automations | Event-triggered "when X then Y" rules. |
| Sequence | /v1/sequences | Multi-step drip sequences with auto-enroll and exit conditions. |
| Template | /v1/templates | Reusable email templates referenced by automations and sequences. |
| Webhook | /v1/webhooks | Subscribe an https endpoint to events. HMAC-signed, retried, dead-lettered. |
| Search | /v1/search | Cross-object search over contacts, companies and deals. |
| Schema | /v1/schema | Live, self-describing object + field + enum registry. |
Read the schema first
Before writing, an agent should GET /v1/schema to learn what objects, fields and enum values exist. If it then sends an unknown value, the error tells it exactly what is valid:
POST /v1/contacts { "type": "prospect" }
422 Unprocessable Entity (application/problem+json)
{
"type": "https://relmcrm.com/errors/unknown_value",
"title": "Unknown Value",
"status": 422,
"detail": "'prospect' is not a valid contact type.",
"code": "unknown_value",
"field": "contact type",
"valid_options": ["lead", "customer"]
}
This is the "never confused" contract: agents self-correct from the error instead of failing blind or hallucinating a field. Need a new value? Create it - relm_create_enum_value, relm_create_field, relm_create_type.
Batch writes
Import many records in one round-trip with POST /v1/batch (or the relm_batch MCP tool). Each operation is metered individually - batching saves round-trips, not quota.
POST /v1/batch
{ "operations": [
{ "method": "create", "object": "contact", "data": { "email": "[email protected]" } },
{ "method": "create", "object": "deal", "data": { "title": "Acme" } }
]}
Connect via MCP
Relm ships a native Model Context Protocol server at https://api.relmcrm.com/mcp (Streamable HTTP, request/response). Every CRM operation is a typed MCP tool. No key is needed to initialize or tools/list - the catalog is public so clients and directories can discover it; tools/call needs a credential.
Two ways to connect. If your client supports OAuth (most chat clients do), just point it at the server URL and it will walk you through signing in - the client registers itself, you approve in a browser, and you never handle a secret. If you would rather paste a key, or you are wiring up a server or a CI job, use an API key in a header:
{
"mcpServers": {
"relm": {
"type": "http",
"url": "https://api.relmcrm.com/mcp",
"headers": { "Authorization": "Bearer relm_live_..." }
}
}
}
Then talk to it in plain language: "add these five leads and open a deal for each in the sales pipeline." The agent calls relm_describe_schema, then batches the writes. A single MCP POST can carry an array of tool calls; each is metered per call.
OAuth 2.1
For clients that authorize with OAuth, everything is discoverable - there is nothing to register by hand:
- Protected-resource metadata:
https://api.relmcrm.com/.well-known/oauth-protected-resource - Authorization-server metadata:
https://api.relmcrm.com/.well-known/oauth-authorization-server - Dynamic client registration:
POST https://api.relmcrm.com/oauth/register(RFC 7591) - Authorization code + PKCE (
S256required) and refresh tokens with rotation. Scope:crm.
An unauthenticated tools/call returns 401 with a WWW-Authenticate header pointing at that metadata, which is how a client knows to offer you a Connect button. OAuth grants act on live data; test mode stays API-key only.
Connect via A2A
Relm also speaks Agent2Agent at https://api.relmcrm.com/a2a (JSON-RPC 2.0). Send a message/send whose data part is {"tool":"relm_...","arguments":{...}}; Relm runs it synchronously against the same validated tools and returns a terminal Task. The agent card is at /.well-known/agent-card.json.
OpenAPI
The full REST surface is described by a machine-readable OpenAPI 3.1 spec - import it into your client generator, Postman, or an agent toolchain.
Webhooks
Subscribe an https endpoint to CRM events. Register with POST /v1/webhooks (or relm_create_webhook) - the response returns a signing secret once. Events: contact.created, contact.updated, deal.created, deal.updated, deal.stage_changed (or ["*"] for all).
curl https://api.relmcrm.com/v1/webhooks \
-H "Authorization: Bearer relm_live_..." -H "Content-Type: application/json" \
-d '{ "url": "https://your.app/relm", "events": ["deal.stage_changed"] }'
# -> { "id": "wh_...", "secret": "whsec_...", ... } (store the secret; shown once)
Each delivery is a JSON POST with headers Relm-Event, Relm-Delivery and Relm-Signature: t=<unix>,v1=<hmac>. Verify it by recomputing HMAC-SHA256 of "<t>.<raw body>" with your secret:
import crypto from "node:crypto";
function verify(secret, rawBody, header) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
Non-2xx or timeout retries with backoff (1m, 5m, 30m, 2h, 6h) and dead-letters after 6 attempts. Inspect recent attempts at GET /v1/webhooks/{id}/deliveries. Live-mode URLs must be public https (private, loopback and link-local hosts are rejected); test-mode keys may point at localhost.
Errors
Every error is RFC-9457 problem+JSON. type is a stable URI (https://relmcrm.com/errors/<code>) that resolves to a short reference page, with a human title, a machine code, and - where useful - valid_options and a suggestion.
| Status | code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed JSON or bad parameter. |
| 401 | unauthorized | Missing or invalid API key. |
| 403 | plan_limit / forbidden | Plan cap reached (Free allows 2 automations / 1 sequence) or action not permitted for this key. |
| 404 | not_found | No such record in this workspace/mode. |
| 409 | conflict / idempotency_key_reused | Duplicate (e.g. email - returns the existing record), or a reused idempotency key. |
| 412 | version_conflict | Record changed since you read it - re-fetch and reapply. |
| 422 | unknown_value / unknown_field / validation_failed / invalid_reference | Unprocessable input - see valid_options and suggestion. |
| 429 | rate_limited / quota_exceeded / spend_cap_reached | Slow down, out of monthly quota, or spend cap hit. |
Rate limits & quotas
Requests are rate-limited per workspace per minute and counted against a monthly quota. Responses carry X-RateLimit-* and X-Quota-* headers.
| Plan | Monthly requests | Over the cap |
|---|---|---|
| Free | 1,000 | Hard-stops (429) |
| Pro - $29/mo | 100,000 | Metered overage, $0.0001/req |
| Scale - $249/mo | 2,000,000 | Metered overage, $0.0001/req |
On paid plans you can set a hard spend cap; at $0 it behaves like Free and stops at the quota instead of billing overage. Test mode never counts.