API documentation
Connect any platform to CRM City.
Your first connection in 5 steps
- Create an API key in Settings → API Keys.
- Copy the secret key (
crm_sec_…). It is shown only once. - Store it in your platform's environment variables (NOT hardcoded in your code).
- Test it with
GET /api/crm/me. - 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
/api/crm/me— Confirms the key works. Accepts pub or sec.Contacts (Pin 1)
/api/crm/contacts?email=...&q=...&limit=50— List / search. Secret./api/crm/contacts— Upsert contact (dedupe by email, fill-blanks-only). Secret./api/crm/contacts/:id— A single contact. Secret.Activities (Pin 2)
/api/crm/contacts/:id/activities— Logs an activity on the timeline. Secret.Invitations (Pin 3)
/api/crm/invitations?status=...&limit=50— List invitations. Secret./api/crm/invitations— Create (+ optionally send email). Secret./api/crm/invitations/:token— Track open + (opt) redirect. Pub OK./api/crm/invitations/:token— Mark as accepted. Pub OK.Groups (Pin 4)
/api/crm/groups— List groups. Secret./api/crm/groups— Create group (with source_type). Secret./api/crm/groups/:id— Group + members with roles. Secret./api/crm/groups/:id— Delete group. Secret./api/crm/groups/:id/members— Members with roles. Secret./api/crm/groups/:id/members— Add member with role. Secret./api/crm/groups/:id/members?contact_id=...— Remove member. Secret.Tags & Segments (Pin 6)
/api/crm/contacts/:id/tags— The contact's tags. Secret./api/crm/contacts/:id/tags— Add tag (auto-created if missing). Secret./api/crm/contacts/:id/tags?tag=name— Remove tag. Secret./api/crm/segments/:slug/contacts— Resolve a segment → list of contacts. Secret.Pastoral alerts (Pin 8)
/api/crm/alerts?open=1— Needs (filter by open). Secret./api/crm/alerts— Raise a pastoral need. Secret./api/crm/alerts/:id— Mark as resolved. Secret.Hierarchy (Pin 13)
/api/crm/hierarchy— List of the organization's levels. Secret./api/crm/hierarchy— FULL replace of the levels. Secret./api/crm/hierarchy/levels— Insert an intermediate level. Secret./api/crm/contacts/:id/hierarchy— Assign contact to a level. Secret.Webhooks OUT (Pin 10) — CRM notifies your platform
/api/crm/webhooks— List subscriptions. Secret./api/crm/webhooks— Subscribe to events. Secret shown only once./api/crm/webhooks/:id— Unsubscribe. Secret.Ingest (Pin 14) — third-party platforms send to CRM
/api/ingest/:source— Receiver for Shopify, Stripe etc. with HMAC verify. Header X-Ingest-Secret./api/ingest/generic— Catch-all for custom sources. Header X-Ingest-Secret.Deals (Pin 7)
/api/crm/deals— List deals. Secret./api/crm/deals— Create deal. Secret./api/crm/deals/:id/stage— Move stage (won/lost auto). Secret.Events (Pin 2.2)
/api/crm/events— Platform event → upsert contact + activity. Secret.Field permissions (Pin 15)
/api/crm/field-permissions— Policies per level. Secret./api/crm/field-permissions— Set a field_group's visibility for a level. Secret.Backup & Export (Pin 16)
/api/crm/export— Full JSON snapshot (all tenant tables). Secret.Companies
/api/crm/companies?q=...&limit=50&offset=0— List / search by name. Pub OK./api/crm/companies— Create company (name required, type optional). Secret.Products
/api/crm/products?active=true&q=...— Product catalog (active filter, name/sku search). Pub OK./api/crm/products— Create product (sku unique per tenant). Secret./api/crm/products/:id— A single product. Pub OK./api/crm/products/:id— Update any subset of fields. Secret./api/crm/products/:id— Delete; if it has order lines → deactivate only. Secret.Orders (e-commerce)
/api/crm/orders?contact_id=...&status=...— List orders with lines. Secret./api/crm/orders— Create order: contact_id or email (upsert), items by product_id/sku/product_name, total computed. Emits order.placed. Secret.Courses & certificates
/api/crm/courses?active=true&q=...— Course catalog. Pub OK./api/crm/courses— Create course (title required). Secret./api/crm/courses/:id/enrollments?status=...— A course's enrollments, with contact. Pub OK./api/crm/courses/:id/enrollments— Enroll contact (contact_id or email → upsert). Duplicate → 409. Secret./api/crm/enrollments/:id— Status/progress; on completed → completed_at + course.completed event. Secret./api/crm/certificates?contact_id=...— Issued certificates, with public verification_url. Secret./api/crm/certificates— Issue a certificate for a completed enrollment (only once). Secret.Invoices (read-only V1)
/api/crm/invoices?status=...&contact_id=...— List invoices. Creation stays in the app. Secret./api/crm/invoices/:id— The invoice with its lines. Secret.Bookings (read-only V1)
/api/crm/bookings?from=...&to=...&status=confirmed— Upcoming 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 contactstag:<name>— contacts with the given tagleaders— contacts with the "leader" role in any grouprole:<role>— contacts with a specific role in any groupgroup:<id>— members of a groupinactive-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 (seefield) - 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