Keeping your CRM in sync
A company that keeps HubSpot, Zoho or another CRM next to Majali syncs it through server keys and webhooks. Everything here needs a server key with the operation’s scope; a publishable key is refused, and these routes answer no browser.
The operations
| Operation | Scope | What it does |
|---|---|---|
GET /api/v1/contacts | read:contacts | People: name, email, phone, language, roles, their leads’ references, your ids |
POST /api/v1/contacts | write:contacts | Add or match a person, by your id, then email, then phone; fills, never overwrites |
GET /api/v1/contacts/{reference} | read:contacts | One person |
PATCH /api/v1/contacts/{reference} | write:contacts | Change the name or language, or keep your id |
GET /api/v1/leads | read:leads | Leads, with person, assignee, department, next action and first-answer times |
GET /api/v1/leads/{reference} | read:leads | One lead |
PATCH /api/v1/leads/{reference} | write:leads | Status, assignee, your id |
GET /api/v1/activities | read:followups | Timeline entries |
POST /api/v1/activities | write:followups | Log a note, call, meeting, message or email |
GET /api/v1/tasks | read:followups | Tasks |
POST /api/v1/tasks | write:followups | Add a task |
GET /api/v1/tasks/{task_id} | read:followups | One task |
PATCH /api/v1/tasks/{task_id} | write:followups | Done, open again, title, due date, assignee |
GET /api/v1/members | read:members | Team members, to map your users: use their id as assignee or by |
GET /api/v1/departments | read:members | Departments, by key |
The loop
- Subscribe a webhook endpoint (the company’s Webhooks screen) to the events you mirror:
lead.created,lead.assigned,lead.status_changed,lead.escalated,contact.created,contact.updated,activity.created,task.created,task.done. Events carry ids and references only. - On an event, read the record (
GET /api/v1/leads/{reference},GET /api/v1/contacts/{reference}, or the lists withupdated_since). A nightly pull withupdated_sincecatches anything a missed event left behind. - Upsert it in your CRM, then write your id back:
PATCHthe lead or the person with{"external": {"source": "hubspot", "id": "88213"}}. From then on?external=hubspot:88213finds it. - Changes made in your CRM come back through
PATCH(status, assignee) andPOST /api/v1/activities(a call logged in the CRM lands on the lead’s timeline and counts as its first answer).
No echoes
Every event carries caused_by: admin (someone on a screen), system (scheduled work), website (a publishable key) or api:<key prefix>. Ignore events whose caused_by is your own key’s prefix: they are the echo of your own writes.
Sending twice changes nothing
POST /api/v1/contacts with the same external id, and POST /api/v1/activities or POST /api/v1/tasks with the same source and external_id, return the first record (200 instead of 201). Retry freely after a timeout.
Erasure
A person erased under a privacy request or retention comes back with "erased": true and empty personal fields (a lead the same way). Erase your copy too: the company answers for both.
HubSpot
A private app with the contacts and deals scopes; map Majali people to contacts (source hubspot, the contact’s id) and leads to deals or tickets, with the status as the pipeline stage. Log HubSpot calls and meetings with POST /api/v1/activities, source hubspot and the engagement’s id as external_id.
Zoho CRM
A server-based client; map people to Contacts or Leads and Majali leads to Deals, source zoho. Zoho’s workflow webhooks call your own small service, which writes to Majali with the server key; never give Zoho the key itself.
What the company sees
Every read is recorded in the company’s audit log with the key’s prefix; revoking the key stops the sync at once.