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_…)
Limited, not harmless. Used for: smoke tests, reading the product and course catalogues, and opening or accepting an invitation you hold the link token for, which returns that invitee's name and email. Companies and course enrolments name people, so they need the secret key. It cannot write contacts or create invitations. Anyone who sees the key can do the same.
Secret key (crm_sec_…)
Server-only. Used for: CRUD on contacts, activities, invitations, groups, tags, and every read that names a person: contacts, companies, course enrolments, orders, invoices, bookings and certificates. NEVER expose it in client-side JS.
Send the key in a header: X-CRM-API-Key: crm_sec_…
Some clients — agent builders especially — make you pick the header name from a fixed list. Any of these work and mean the same thing: x-api-key, api-key, apikey, or Authorization: Bearer crm_sec_…
Endpoints
Identity & smoke test
/api/crm/me— Confirms the key works. Accepts pub or sec.Contacts
/api/crm/contacts?email=...&q=...&limit=50&order=id&cursor=...— List / search. Most recently changed first by default. To go through every contact, pass order=id and call again with cursor set to next_cursor until it is null. Secret./api/crm/contacts— Upsert contact (dedupe by email, fill-blanks-only). Secret./api/crm/contacts/:id— A single contact. Secret.Activities
/api/crm/contacts/:id/activities— Logs an activity on the timeline. Secret.Invitations
/api/crm/invitations?status=...&limit=50— List invitations. Secret./api/crm/invitations— Create; returns invite_url. send: true asks for the email — refused today, until your own email account can be connected (send_error says why). Secret./api/crm/invitations/:token— Track open + (opt) redirect; returns the invitee's name and email. Pub OK — the token is what grants it./api/crm/invitations/:token— Mark as accepted. Pub OK.Groups
/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
/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
/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
/api/crm/hierarchy— List of the organisation'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 — 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.Webhooks IN — 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
/api/crm/deals— List deals. Secret./api/crm/deals— Create deal. Secret./api/crm/deals/:id/stage— Move stage (won/lost auto). Secret.Events
/api/crm/events— Platform event → upsert contact + activity. Secret.Field permissions
/api/crm/field-permissions— Policies per level. Secret./api/crm/field-permissions— Set a field_group's visibility for a level. Secret.Backup & Export
/api/crm/export— JSON snapshot of your organisation's records: the same tables and exclusions as Settings → Backup & Export. Secret key only.Companies
/api/crm/companies?q=...&limit=50&offset=0— List / search by name, with notes and addresses. Secret./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 organisation). 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 each person's name and email. Secret./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
Examples use crmcity.app, the domain CRM City is served from — copy one and it runs as written. If your CRM is served from your own domain, swap the host: all endpoints live under https://<your-domain>/api/crm.
# Smoke test
curl https://crmcity.app/api/crm/me \
-H "X-CRM-API-Key: crm_sec_xxx..."
# Upsert contact (dedupe by email)
curl https://crmcity.app/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"}'
# Create an invitation — share its invite_url yourself; the email part is refused
# until your own email account can be connected
curl https://crmcity.app/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":false
}'
# Create company
curl https://crmcity.app/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://crmcity.app/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://crmcity.app/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://crmcity.app/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://crmcity.app/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://crmcity.app/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://crmcity.app/api/crm/invoices?status=paid" \
-H "X-CRM-API-Key: crm_sec_xxx..."
# Bookings for the next 7 days (read-only)
curl "https://crmcity.app/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 - 403
organization_deleted— the key's organisation has been deleted; it works again if the organisation is restored within 30 days - 400
validation_error— invalid field (seefield) - 404
not_found— resource does not exist in the key's organisation - 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