API documentation

Connect any platform to CRM City.

Your first connection in 5 steps

  1. Create an API key in Settings → API Keys.
  2. Copy the secret key (crm_sec_…). It is shown only once.
  3. Store it in your platform's environment variables (NOT hardcoded in your code).
  4. Test it with GET /api/crm/me.
  5. Start syncing contacts with POST /api/crm/contacts.

API keys

Public key (crm_pub_…)

Safe for frontend / browser use. Used for: smoke tests, tracking invite opens, accepting invitations. It cannot write contacts or create new invitations.

Secret key (crm_sec_…)

Server-only. Used for: CRUD on contacts, activities, invitations, groups, tags. NEVER expose it in client-side JS.

Send the key in a header: X-CRM-API-Key: crm_sec_…

Endpoints

Identity & smoke test

GET/api/crm/meConfirms the key works. Accepts pub or sec.

Contacts (Pin 1)

GET/api/crm/contacts?email=...&q=...&limit=50List / search. Secret.
POST/api/crm/contactsUpsert contact (dedupe by email, fill-blanks-only). Secret.
GET/api/crm/contacts/:idA single contact. Secret.

Activities (Pin 2)

POST/api/crm/contacts/:id/activitiesLogs an activity on the timeline. Secret.

Invitations (Pin 3)

GET/api/crm/invitations?status=...&limit=50List invitations. Secret.
POST/api/crm/invitationsCreate (+ optionally send email). Secret.
GET/api/crm/invitations/:tokenTrack open + (opt) redirect. Pub OK.
PATCH/api/crm/invitations/:tokenMark as accepted. Pub OK.

Groups (Pin 4)

GET/api/crm/groupsList groups. Secret.
POST/api/crm/groupsCreate group (with source_type). Secret.
GET/api/crm/groups/:idGroup + members with roles. Secret.
DELETE/api/crm/groups/:idDelete group. Secret.
GET/api/crm/groups/:id/membersMembers with roles. Secret.
POST/api/crm/groups/:id/membersAdd member with role. Secret.
DELETE/api/crm/groups/:id/members?contact_id=...Remove member. Secret.

Tags & Segments (Pin 6)

GET/api/crm/contacts/:id/tagsThe contact's tags. Secret.
POST/api/crm/contacts/:id/tagsAdd tag (auto-created if missing). Secret.
DELETE/api/crm/contacts/:id/tags?tag=nameRemove tag. Secret.
GET/api/crm/segments/:slug/contactsResolve a segment → list of contacts. Secret.

Pastoral alerts (Pin 8)

GET/api/crm/alerts?open=1Needs (filter by open). Secret.
POST/api/crm/alertsRaise a pastoral need. Secret.
PATCH/api/crm/alerts/:idMark as resolved. Secret.

Hierarchy (Pin 13)

GET/api/crm/hierarchyList of the organization's levels. Secret.
POST/api/crm/hierarchyFULL replace of the levels. Secret.
POST/api/crm/hierarchy/levelsInsert an intermediate level. Secret.
PATCH/api/crm/contacts/:id/hierarchyAssign contact to a level. Secret.

Webhooks OUT (Pin 10) — CRM notifies your platform

GET/api/crm/webhooksList subscriptions. Secret.
POST/api/crm/webhooksSubscribe to events. Secret shown only once.
DELETE/api/crm/webhooks/:idUnsubscribe. Secret.

Ingest (Pin 14) — third-party platforms send to CRM

POST/api/ingest/:sourceReceiver for Shopify, Stripe etc. with HMAC verify. Header X-Ingest-Secret.
POST/api/ingest/genericCatch-all for custom sources. Header X-Ingest-Secret.

Deals (Pin 7)

GET/api/crm/dealsList deals. Secret.
POST/api/crm/dealsCreate deal. Secret.
PATCH/api/crm/deals/:id/stageMove stage (won/lost auto). Secret.

Events (Pin 2.2)

POST/api/crm/eventsPlatform event → upsert contact + activity. Secret.

Field permissions (Pin 15)

GET/api/crm/field-permissionsPolicies per level. Secret.
PATCH/api/crm/field-permissionsSet a field_group's visibility for a level. Secret.

Backup & Export (Pin 16)

GET/api/crm/exportFull JSON snapshot (all tenant tables). Secret.

Companies

GET/api/crm/companies?q=...&limit=50&offset=0List / search by name. Pub OK.
POST/api/crm/companiesCreate company (name required, type optional). Secret.

Products

GET/api/crm/products?active=true&q=...Product catalog (active filter, name/sku search). Pub OK.
POST/api/crm/productsCreate product (sku unique per tenant). Secret.
GET/api/crm/products/:idA single product. Pub OK.
PATCH/api/crm/products/:idUpdate any subset of fields. Secret.
DELETE/api/crm/products/:idDelete; if it has order lines → deactivate only. Secret.

Orders (e-commerce)

GET/api/crm/orders?contact_id=...&status=...List orders with lines. Secret.
POST/api/crm/ordersCreate order: contact_id or email (upsert), items by product_id/sku/product_name, total computed. Emits order.placed. Secret.

Courses & certificates

GET/api/crm/courses?active=true&q=...Course catalog. Pub OK.
POST/api/crm/coursesCreate course (title required). Secret.
GET/api/crm/courses/:id/enrollments?status=...A course's enrollments, with contact. Pub OK.
POST/api/crm/courses/:id/enrollmentsEnroll contact (contact_id or email → upsert). Duplicate → 409. Secret.
PATCH/api/crm/enrollments/:idStatus/progress; on completed → completed_at + course.completed event. Secret.
GET/api/crm/certificates?contact_id=...Issued certificates, with public verification_url. Secret.
POST/api/crm/certificatesIssue a certificate for a completed enrollment (only once). Secret.

Invoices (read-only V1)

GET/api/crm/invoices?status=...&contact_id=...List invoices. Creation stays in the app. Secret.
GET/api/crm/invoices/:idThe invoice with its lines. Secret.

Bookings (read-only V1)

GET/api/crm/bookings?from=...&to=...&status=confirmedUpcoming bookings (default: from now). Booking happens on the public page /book/:slug. Secret.

Rate limiting

Per key, default tier free:

  • 300 reads/min (GETs)
  • 60 writes/min (POST/PATCH/DELETE)
  • Exceeding it → HTTP 429 with header Retry-After: <seconds>

Tiers pro: 1500/300. enterprise: configurable. Per-key tier changes are made directly in the DB for now; your account plan (and its limits) is managed in Settings → Billing.

Webhook signature (verification at destination)

# How to verify on your server that the request comes from CRM:
# Header: X-CRM-Signature: sha256=<hex>
# Secret: the one returned by POST /api/crm/webhooks (whs_...)

import hmac, hashlib

def verify(body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

Predefined segments

/api/crm/segments/:slug/contacts accepts standard slugs:

  • all — all active contacts
  • tag:<name> — contacts with the given tag
  • leaders — contacts with the "leader" role in any group
  • role:<role> — contacts with a specific role in any group
  • group:<id> — members of a group
  • inactive-30 — contacts with no activity for 30+ days

curl examples

# Smoke test
curl https://crm.claudiu.site/api/crm/me \
  -H "X-CRM-API-Key: crm_sec_xxx..."

# Upsert contact (dedupe by email)
curl https://crm.claudiu.site/api/crm/contacts \
  -X POST \
  -H "X-CRM-API-Key: crm_sec_xxx..." \
  -H "Content-Type: application/json" \
  -d '{"email":"ion@example.com","first_name":"Ion","phone":"+40712345678"}'

# Send invitation
curl https://crm.claudiu.site/api/crm/invitations \
  -X POST \
  -H "X-CRM-API-Key: crm_sec_xxx..." \
  -H "Content-Type: application/json" \
  -d '{
    "invitee_email":"vasile@example.com",
    "invitee_name":"Vasile",
    "target_type":"general",
    "target_label":"Sunday meeting",
    "message":"Come at 10:00 to the church.",
    "send":true
  }'

# Create company
curl https://crm.claudiu.site/api/crm/companies \
  -X POST \
  -H "X-CRM-API-Key: crm_sec_xxx..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Firma SRL","type":"client","city":"Cluj-Napoca"}'

# Create product
curl https://crm.claudiu.site/api/crm/products \
  -X POST \
  -H "X-CRM-API-Key: crm_sec_xxx..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Abonament Pro","sku":"PRO-1","price":49.99,"currency":"GBP"}'

# Place order (contact by email — created if missing; product by sku)
curl https://crm.claudiu.site/api/crm/orders \
  -X POST \
  -H "X-CRM-API-Key: crm_sec_xxx..." \
  -H "Content-Type: application/json" \
  -d '{
    "email":"ion@example.com",
    "first_name":"Ion",
    "items":[{"sku":"PRO-1","quantity":2}]
  }'

# Enroll contact in a course (by email)
curl https://crm.claudiu.site/api/crm/courses/COURSE_ID/enrollments \
  -X POST \
  -H "X-CRM-API-Key: crm_sec_xxx..." \
  -H "Content-Type: application/json" \
  -d '{"email":"ion@example.com","first_name":"Ion"}'

# Mark completion (the key flow for external course platforms)
curl https://crm.claudiu.site/api/crm/enrollments/ENROLLMENT_ID \
  -X PATCH \
  -H "X-CRM-API-Key: crm_sec_xxx..." \
  -H "Content-Type: application/json" \
  -d '{"status":"completed"}'

# Issue the certificate (response contains a public verification_url)
curl https://crm.claudiu.site/api/crm/certificates \
  -X POST \
  -H "X-CRM-API-Key: crm_sec_xxx..." \
  -H "Content-Type: application/json" \
  -d '{"enrollment_id":"ENROLLMENT_ID"}'

# Paid invoices (read-only)
curl "https://crm.claudiu.site/api/crm/invoices?status=paid" \
  -H "X-CRM-API-Key: crm_sec_xxx..."

# Bookings for the next 7 days (read-only)
curl "https://crm.claudiu.site/api/crm/bookings?from=2026-07-02T00:00:00Z&to=2026-07-09T00:00:00Z" \
  -H "X-CRM-API-Key: crm_sec_xxx..."

Standard errors

All errors have the form:

{ "error": "code_string", "message": "Human-readable.", "field": "optional" }
  • 401 auth_error — key missing or invalid
  • 403 key_level_error — requires a secret key
  • 400 validation_error — invalid field (see field)
  • 404 not_found — resource does not exist in the key's tenant
  • 409 conflict — duplicate (e.g. existing enrollment, duplicated sku, certificate already issued)
  • 429 rate_limit_exceeded — too many requests (see Rate limiting)
  • 500 db_error — internal error