Skip to main content

Activities

One thing that happened, logged by a person or by an integration: a call made, a meeting held, a note taken. An activity hangs off a contact, a company or a deal — at least one — because an activity attached to nothing is a sentence nobody can find from anywhere. It can name all three, and it reads on each of their timelines.

An activity about a deal or a contact rolls up to their company: send no companyId and the activity takes the deal's company, else the contact's, and the response says which. That is what puts a call with a person on their company's timeline, and it is what GET /v1/activities?companyId= lists.

An activity is a log entry, and a log is written once. There is no PATCH: an entry that turns out to be wrong is deleted and logged again, which keeps every row's at and byUid the record of what was logged rather than what somebody later wished had been.

Scopes and the site rule are shared with every CRM resource — see companies.

The activity object​

{
"id": "a_8f21",
"object": "activity",
"kind": "call",
"body": "Walked through the proposal. They want the Q4 volume tier.",
"at": "2026-09-05T16:30:00.000Z",
"byUid": "u_9f1c",
"contactId": "k7d2b9f104",
"companyId": "c_71be",
"dealId": "d_3c9a",
"outcome": "Interested",
"durationMinutes": 25,
"direction": "outbound",
"siteId": "site_a1b2c3",
"created": "2026-09-05T18:23:23.941Z",
"updated": "2026-09-05T18:23:23.941Z"
}
FieldTypeNotes
idstringOpaque activity id.
objectstringAlways "activity".
kindstringcall, email, text, whatsapp, messenger, instagram, linkedin, meeting, note or other. note when not sent.
bodystringRequired. What happened, 5,000 characters.
atstringWhen it happened — which is not when it was logged. ISO 8601 instant; defaults to the moment of the request, for the integration logging a call as it ends.
byUidstringWho did it. Must be a member of your organization; "api" when not sent.
contactId, dealIdstring | nullWho it was with and what it is about. At least one of these or companyId is required, and each must exist.
companyIdstring | nullThe company it is about, or the one it rolls up to. A company you send stands; when you send none it is the deal's company, else the contact's. The contact's company is read through consentSiteId's site, or through the person's primary site for an activity from no site. null when none of them names one.
outcomestring | nullA short label, 120 characters.
durationMinutesinteger | nullA whole number, 0 or more.
directionstring | nullWhich way it went: a call takes outbound, inbound or internal (Salesforce's Call Type); an email and every message kind (text, whatsapp, messenger, instagram, linkedin) take outbound or inbound. Any other kind takes none. Optional.
siteIdstringThe site the activity was logged from. Read-only.
created / updatedstring | nullISO 8601.

Endpoints​

List activities​

GET /v1/activities — scope crm:read. Paginated, ordered by activity id unless updatedAfter is given.

ParamNotes
dealId, contactId, companyIdActivities about that record. companyId includes every activity that rolled up to the company.
kindOne of the five kinds. Anything else is a 400.
updatedAfterActivities updated after this instant — the sync filter. An activity is written once, so this is when it was logged.
limit, cursorStandard pagination.
# the history of one deal
curl "https://app.aglyn.com/api/v1/activities?dealId=d_3c9a" \
-H "Authorization: Bearer aglyn_sk_…"

One filter narrows the query — dealId, then contactId, companyId, kind — and the rest are checked on the page, which can come back short.

Retrieve an activity​

GET /v1/activities/{activityId} — scope crm:read. Returns an activity, or 404 not_found ("No such activity").

Log an activity​

POST /v1/activities — scope crm:write. Accepts an Idempotency-Key, scoped to the organization — send one from a dialer or a mail integration, where a retry after a lost response would otherwise log the call twice. body and at least one of contactId, companyId, dealId are required. consentSiteId names the site the record came from; leave it out for a record from no site, which belongs to the whole organization. It is required only when your organization keeps each site's records apart.

curl -X POST "https://app.aglyn.com/api/v1/activities" \
-H "Authorization: Bearer aglyn_sk_…" \
-H "Idempotency-Key: 9a41f0c2-…" \
-H "Content-Type: application/json" \
-d '{"kind":"call","body":"Walked through the proposal.","at":"2026-09-05T16:30:00Z","byUid":"u_9f1c","dealId":"d_3c9a","durationMinutes":25,"consentSiteId":"site_a1b2c3"}'

Returns 201, or 200 with the original when an Idempotency-Key replays.

Delete an activity​

DELETE /v1/activities/{activityId} — scope crm:write. Accepts an Idempotency-Key, scoped to the organization.

{ "id": "a_8f21", "object": "activity", "deleted": true }

Errors​

StatustypeWhen
400bad_requestcode: "validation_failed" — a missing body, a missing consentSiteId where the organization keeps its sites apart, no contactId, companyId or dealId at all (fields.contactId explains), a kind outside its list, an at that is not an ISO 8601 instant, a byUid who is not a member, a reference that does not exist, a durationMinutes that is not a whole number 0 or more, or a direction the kind does not take. On the list, a ?kind= outside the list or a malformed ?updatedAfter=.
403insufficient_scopeKey lacks crm:read / crm:write.
404not_found"No such activity".
405method_not_allowedAllow: GET, POST on /v1/activities, GET, DELETE on one activity — a PATCH is refused here rather than with a 404 that would read as "no such activity".
409conflictcode: "idempotency_in_progress". code: "activity_log_full" — the record named already carries 5,000 activities, the most one record can hold; the key is given back.

See Conventions → Errors for the shared envelope.

  • Tasks — what is owed, as opposed to what happened.
  • Contacts — the platform's own record of what a person did (a form, an order, a booking) lives on the contact's timeline in the console, not here.