Events
Send server-side events for anything the browser cannot see: OAuth callbacks, payment webhooks, queue workers. Endpoints, parameters, and schemas.
| Endpoint | Method | Path | Permission |
|---|---|---|---|
| Send events Send server-side events | POST | /v1/events | events:write |
Base URL https://app.sourceloop.ai/api/v1
Send events
Send server-side events
POST /v1/events
Requires events:write
For anything the browser cannot see: OAuth callbacks, payment webhooks, queue workers. Events are validated and classified with exactly the same rules the browser tracker applies, so a server-side conversion attributes identically.
Each event needs an email, a phone, or the visitor id to be attributable. The visitor id is the strongest match: it ties the event to the exact visit and its marketing. Read it from the `_sl_aid` cookie, or from the hidden `sl_aid` field the script fills in your forms, and send it as `anonymous_id` (`sl_aid` and `visitor_id` are accepted too). Email and phone match the person when there is no visitor id.
Send your own id for the event (a CRM lead id, an order number) as `external_id`: sending the same one again never counts twice, so retries are safe. The response lists the `event_id` each accepted event got.
Events up to 90 days old. For older history or offline sales, use an offline data source (`POST /imports/{id}/rows`). Channels are never taken from the request: the visit decides them, exactly as in the browser.
Partial success: one malformed event does not discard the batch.
curl -s -X POST "https://app.sourceloop.ai/api/v1/events" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"event_type": "conversion",
"event_name": "demo_booked",
"external_id": "crm-lead-8812",
"email": "jane@acme.com",
"sl_aid": "3f1c2a9e-7b1d-4c55-9d1e-0a1b2c3d4e5f",
"occurred_at": "2026-10-08T14:03:00Z",
"revenue": 0
}
]
}' const res = await fetch(
"https://app.sourceloop.ai/api/v1/events",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"events": [
{
"event_type": "conversion",
"event_name": "demo_booked",
"external_id": "crm-lead-8812",
"email": "jane@acme.com",
"sl_aid": "3f1c2a9e-7b1d-4c55-9d1e-0a1b2c3d4e5f",
"occurred_at": "2026-10-08T14:03:00Z",
"revenue": 0
}
]
}),
},
);
const data = await res.json(); import os, requests
res = requests.post(
"https://app.sourceloop.ai/api/v1/events",
headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
json={
"events": [
{
"event_type": "conversion",
"event_name": "demo_booked",
"external_id": "crm-lead-8812",
"email": "jane@acme.com",
"sl_aid": "3f1c2a9e-7b1d-4c55-9d1e-0a1b2c3d4e5f",
"occurred_at": "2026-10-08T14:03:00Z",
"revenue": 0
}
]
},
)
data = res.json() Headers
Idempotency-Keystringheader
Request body
websitestringeventsobject[]required
Event
events[].event_typestringExample
customevents[].event_namestringExample
signup_completedevents[].occurred_atstring (date-time)When it happened (default now). Up to 90 days back.
events[].external_idstringYour id for this event. The same one again never counts twice.
Identity
events[].emailstringevents[].phonestringevents[].anonymous_idstringThe visitor id: the _sl_aid cookie, or the sl_aid hidden form field. Also accepted as sl_aid or visitor_id.
Value
events[].revenuenumberevents[].currencystringevents[].propertiesobject
Responses
-
200Accepted and rejected counts, the event_id of each accepted event by index, and the reason for each rejected one -
400Something was wrong with the request or the credential. -
429Too many requests for this workspace in the current minute (`rate-limit-exceeded`), the monthly allowance used (`quota-exceeded`), or too many analytics queries running at once (`too-many-concurrent`). Wait Retry-After seconds and repeat the request unchanged.