Every change in the CRM — a contact tag added, a lead stage moved, an email opened, a ticket replied to — produces an activity record. Activities are the audit trail and the source of truth for the contact timeline a sales rep sees on the contact detail page. The system records them automatically; you rarely create activities by hand.
Surface
/crm/activity/manageJWT/crm/activity/by-customer/:datatype?/:ownerId?JWT/crm/activity/by-resource/:datatype/:ownerIdJWTThe two read endpoints differ in pivot: by-customer returns activities about a customer (contact, lead, merchant), by-resource returns activities about a non-customer resource (a ticket, a campaign, an order).
A typical contact-timeline call:
const activities = await fetch(
`/api/crm/activity/by-customer/contact/${contactId}?page=1&pageSize=50`,
{ headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` } },
).then(r => r.json());
Auto-logged events
The CRM automatically writes activities for these events:
| Event | Source | Activity type |
|---|---|---|
| Contact created | POST /repository/create/contact | contact_created |
| Contact tagged | DB hook | contact_tagged |
| Lead stage moved | PUT /crm/leads/detail/:id | lead_stage_changed |
| Email sent | broadcast send | email_sent |
| Email opened | tracking pixel | email_opened |
| Email clicked | tracking redirect | email_clicked |
| Page visited | analytics ingest | page_visited |
| Form submitted | POST /crm/contact-form/post/:app/:name | form_submitted |
| Ticket created | POST /crm/tickets/create | ticket_created |
| Ticket replied | POST /crm/tickets/reply/:ticketId | ticket_replied |
| Reservation booked | POST /crm/reservations/create | reservation_booked |
| Reservation cancelled | DELETE /crm/reservations/cancel/... | reservation_cancelled |
| Order placed | storefront checkout | order_placed |
Activities flow into the Sync notification processor which evaluates automation triggers (e.g. "if email_opened on contact tagged 'vip', notify rep"). They also feed the lead-scoring engine.
Manual activities
Use manage to add a custom activity — a logged call, an in-person meeting, a note pinned to a contact:
await fetch('/api/crm/activity/manage', {
method: 'POST',
headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
body: JSON.stringify({
type: 'call_logged',
datatype: 'contact',
ownerId: 'contact-abc',
summary: '15min discovery call. Interested in enterprise plan.',
durationSeconds: 900,
metadata: { phone: '+15551234567', outcome: 'positive' },
}),
});
Comments
Comments are first-class on activities — internal-only threaded discussion attached to a record (a contact, a lead, a deal):
/crm/activity/comment/createJWT/crm/activity/comment/get/:datatype/:ownerIdJWT/crm/activity/comment/delete/:idJWT// POST /crm/activity/comment/create
{
"datatype": "lead",
"ownerId": "lead-xyz",
"text": "Procurement asked about SOC2 — need to send report.",
"mentions": ["user-jane"]
}
Mentions create notifications via the Sync notification processor. Comments are visible only to staff (User principals) — never returned to customer-facing endpoints.
Audit reads
For broader audit (who edited what, when, from which IP), use the data layer's history endpoints rather than the activity log:
GET /repository/history/{collection}/:idreturns the version history of a record.- The system fields on every
BaseModel\<T\>(createdate,modifydate,createuser,modifyuser,version) carry the basic audit info inline.
The activity log is for narrative timeline; the data history is for forensic audit.
Activities are append-only by design. There's no edit endpoint — if a wrong activity is created, soft-delete it via the data layer. This keeps the audit trail tamper-evident.
Performance
Activities are scoped per ownerId, so reading a single contact's timeline is cheap. Reading "all activities for the org" is not — there's no global feed endpoint, and you should not build one against the raw data layer either; route through the Analytics module which materialises rollups.