API Reference

Base URL: https://relmcrm.com

Authentication

All requests require an API key. Live keys start with sk_live_; test keys with sk_test_. Create both on the API Keys page.

Authorization: Bearer sk_live_...

Idempotency

Send an Idempotency-Key header (any string up to 255 chars) on any POST. Retries with the same key + same body return the original response; a different body returns 409 idempotency_conflict. Keys are scoped per workspace and stored for 24 hours.

curl -X POST $BASE/v1/contacts \
  -H "Authorization: Bearer $RELM_KEY" \
  -H "Idempotency-Key: create-ada-2026-07-02" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com"}'

Pagination

Lists return { object: "list", data, has_more, next_cursor }. Pass the cursor back as ?cursor=… to page forward. Default limit 50, max 200.

Filters

Use ?filter[field][op]=value. Operators: eq, neq, gt, gte, lt, lte, ilike, in. Shorthand ?email=ada@example.com maps to eq.

# Deals worth ≥ $10k in the "won" stage
curl "$BASE/v1/deals?filter[stage]=won&filter[value_cents][gte]=1000000" \
  -H "Authorization: Bearer $RELM_KEY"

Bulk import

POST /v1/contacts/batch takes up to 500 contacts in one call. Pass dedup_on: ["email"] to upsert on email instead of failing on duplicates.

curl -X POST $BASE/v1/contacts/batch \
  -H "Authorization: Bearer $RELM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {"email":"ada@example.com","first_name":"Ada"},
      {"email":"grace@example.com","first_name":"Grace"}
    ],
    "dedup_on": ["email"]
  }'

Rate limits

Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds). The API version is echoed as Relm-Version.

Quickstart

Create a contact in your language of choice.

curl -X POST $BASE/v1/contacts \
  -H "Authorization: Bearer $RELM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ada@example.com",
    "first_name": "Ada",
    "last_name": "Lovelace"
  }'

Custom fields

Define workspace-scoped custom fields on any core object; values live in metadata and are validated on every write. Types: text | number | date | bool | select.

# Define a required "plan" select field on contacts
curl -X POST $BASE/v1/schema/fields \
  -H "Authorization: Bearer $RELM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "object_type": "contact",
    "key": "plan",
    "label": "Plan",
    "type": "select",
    "required": true,
    "options": { "choices": ["free", "pro", "enterprise"] }
  }'

# Now every contact write must include a valid plan
curl -X POST $BASE/v1/contacts \
  -H "Authorization: Bearer $RELM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","metadata":{"plan":"pro"}}'

Manage fields visually on the Schema page.

Associations

First-class polymorphic edges between any two records — contact↔company, contact↔deal, deal↔activity, etc.

curl -X POST $BASE/v1/associations \
  -H "Authorization: Bearer $RELM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from_type": "contact", "from_id": "con_...",
    "to_type": "deal",     "to_id":   "deal_...",
    "label": "champion"
  }'

# List everything linked from a contact
curl "$BASE/v1/associations?from_type=contact&from_id=con_..." \
  -H "Authorization: Bearer $RELM_KEY"

Webhooks

Subscribe an endpoint and Relm POSTs a signed payload for every matching event. Payloads are signed with HMAC-SHA256 using the endpoint's whsec_… secret; verify the relm-signature header before trusting the body. Failed deliveries retry with exponential backoff up to 8 attempts.

curl -X POST $BASE/v1/webhooks \
  -H "Authorization: Bearer $RELM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/hooks/relm",
    "event_types": ["contact.created", "deal.updated"]
  }'
# → { "id": "...", "secret": "whsec_…", "url": "...", ... }

Signature header format:

relm-signature: t=1751500000,v1=<hex>
# v1 = HMAC_SHA256(secret, `${t}.${rawBody}`)

Event types: contact|company|deal|activity.created|updated|deleted. Subscribe to "*" to receive all events.

Endpoints

POST/v1/contactsCreate a contact
POST/v1/contacts/batchBulk create/upsert (dedup_on: email)
GET/v1/contactsList contacts
GET/v1/contacts/:idRetrieve a contact
PATCH/v1/contacts/:idUpdate a contact
DELETE/v1/contacts/:idDelete a contact
POST/v1/companiesCreate a company
GET/v1/companiesList companies
GET/v1/companies/:idRetrieve a company
PATCH/v1/companies/:idUpdate a company
DELETE/v1/companies/:idDelete a company
POST/v1/dealsCreate a deal
GET/v1/dealsList deals
GET/v1/deals/:idRetrieve a deal
PATCH/v1/deals/:idUpdate a deal
DELETE/v1/deals/:idDelete a deal
POST/v1/activitiesCreate an activity
GET/v1/activitiesList activities
GET/v1/activities/:idRetrieve an activity
PATCH/v1/activities/:idUpdate an activity
DELETE/v1/activities/:idDelete an activity
GET/v1/search?q=...Search across entities
GET/v1/schema/fieldsList custom fields (?object_type=contact)
POST/v1/schema/fieldsDefine a custom field
DELETE/v1/schema/fields/:idRemove a custom field
GET/v1/associationsList edges (filter by from_/to_ type+id)
POST/v1/associationsLink two records
DELETE/v1/associations/:idUnlink
GET/v1/webhooksList webhook endpoints
POST/v1/webhooksSubscribe an endpoint (returns signing secret)
PATCH/v1/webhooks/:idUpdate an endpoint
DELETE/v1/webhooks/:idDelete an endpoint
POST/v1/webhooks/:id/replay/:deliveryIdReplay a delivery

Response shape

Lists return { data, has_more, next_cursor }. Errors return { error: { code, message } } with the appropriate HTTP status.

Objects

Contact
id: con_
emailfirst_namelast_namephonecompany_idmetadata
Company
id: cmp_
namedomainmetadata
Deal
id: deal_
titlevalue_centscurrencystageclose_datecompany_idprimary_contact_idmetadata
Activity
id: act_
typesubjectbodycontact_iddeal_idcompany_idoccurred_atmetadata