- Home
- /
- Help Center
- /
- ReactIn APIs
- /
- Track campaign events via API
Track campaign events via API
Read the full outreach timeline of any campaign — invites sent, accepted, messages, replies and skips — programmatically, to sync outreach status into your CRM or build custom reporting.
Every lead a campaign processes generates a campaign event — a timeline that records when the invite was sent, when it was accepted, when your messages went out, when the lead replied, or why they were skipped. The Campaign Events API exposes that timeline programmatically, so you can sync outreach status into your CRM, build custom reporting, or trigger workflows when a lead replies.
Prerequisites
Before you start, you need:
-
Your Organization ID — identifies which workspace you're querying.
-
Your Campaign ID — the campaign whose events you want to read. You'll find it in the campaign URL or campaign settings.
-
Your organization API key — a secret token used to authenticate every request. Find it, along with your Organization ID, under Settings → API Keys in the app.
⚠️ This endpoint only accepts your organization API key or a campaign-scoped key. A SmartList/list API key (the one auto-generated when you create a "ReactIn API" list) won't work here and returns
401 Unauthorized.
Keep your API key secret. Never commit it to source control or expose it in client-side code.
Base URL
All endpoints are served under your ReactIn instance and scoped to your organization:
https://app.reactin.io/api/{organizationId}/...
Authentication
Authenticate by sending your organization (or campaign-scoped) API key in the Authorization header. Both the Bearer prefix and a raw token are accepted:
Authorization: Bearer YOUR_API_KEY
A missing or invalid key returns 401 Unauthorized.
Endpoint
GET /api/{organizationId}/campaigns/{campaignId}/events
Returns the campaign's events, sorted newest-first by creation date (the most recently added leads first — not by latest activity), paginated and filterable by status and date.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | number | 1 | Page number (≥ 1) |
| pageSize | number | 10 | Events per page (10–50) |
| status | string | – | Filter by step reached — matches events that have reached at least the given step (cumulative, not only the exact current step). Comma-separate several values to combine them (OR logic). |
| dateFrom | string (ISO 8601) | – | Only events created on or after this date |
| dateTo | string (ISO 8601) | – | Only events created on or before this date |
Available status filter values. Each one matches events that have reached at least this step:
| Value | Meaning |
|---|---|
| pending | Queued, no action taken yet |
| connection_sent | Connection invite sent |
| connection_accepted | Connection invite accepted |
| message_sent | First message sent |
| message_replied | The lead replied |
| follow_up_sent | A follow-up message was sent |
| skipped | The lead was skipped (see skippedReason) |
ℹ️ The
statusfilter is cumulative:?status=connection_acceptedalso returns leads who have since been messaged or replied (their responsestatuswill then bemessage_sent,message_replied, etc.).?status=skippeddoesn't include leads skipped because they were already one of your connections, even though those still show"status": "skipped"in the response.
Example request
curl -s \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://app.reactin.io/api/ORG_ID/campaigns/CAMPAIGN_ID/events?status=message_replied,connection_accepted&pageSize=25"
Example response
{
"data": [
{
"id": "evt_123",
"status": "message_replied",
"createdAt": "2026-05-19T10:48:00.000Z",
"connectionSentAt": "2026-05-15T09:00:00.000Z",
"connectionAcceptedAt": "2026-05-16T14:20:00.000Z",
"messageSentAt": "2026-05-17T08:30:00.000Z",
"messageRepliedAt": "2026-05-19T10:48:00.000Z",
"followUpMessageSentAt": null,
"lead": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"linkedinProfileUrl": "https://www.linkedin.com/in/janedoe"
},
"skippedAt": null,
"skippedReason": null
}
],
"page": 1,
"nextPage": 2
}
Response fields
-
data[]— the events on this page.-
status— the latest step the lead has reached (only the most advanced one: a lead who accepted then replied — or who replied after a follow-up was sent — showsmessage_replied, notfollow_up_sent). This is the current step, whereas thestatusfilter above is cumulative. To detect replies reliably, key offmessageRepliedAt(or themessage_repliedfilter) rather than assuming a follow-up step means the lead hasn't replied. -
connectionSentAt,connectionAcceptedAt,messageSentAt,messageRepliedAt,followUpMessageSentAt— timestamps for each step, ornullif it hasn't happened. -
lead— the lead this event belongs to (firstName,lastName,email,linkedinProfileUrl). -
skippedAt/skippedReason— set when the lead was skipped, otherwisenull.
-
-
page— current page number. -
nextPage— the next page number, ornullwhen there are no more.
Rate limits
Requests are rate-limited per organization (30 requests / minute for reads). Every response includes the current limit state via X-RateLimit-* headers. When you exceed the limit you receive 429 Too Many Requests with a Retry-After header (in seconds) — back off and retry after that delay.
Error handling
| Status | Meaning | Typical cause |
|---|---|---|
| 200 | OK | Request succeeded |
| 400 | Bad Request | Invalid query parameter (bad status, malformed date, out-of-range pageSize) |
| 401 | Unauthorized | Missing or invalid API key |
| 404 | Not Found | Organization or campaign doesn't exist in your workspace |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Something went wrong on our end — try again |
Putting it together
A typical reporting loop looks like this:
-
Poll
GET /campaigns/{campaignId}/events?status=message_repliedon a schedule to detect new replies. -
Track which events you've already handled by their
id, and re-check them on each poll — a lead's status and timestamps evolve over time. Don't usedateFromas an incremental cursor: it filters on when the event was created (when the lead entered the campaign), not on when the reply or acceptance happened, so a fresh reply on an older lead would be missed. -
Walk the pages via
nextPageuntil it'snull. -
Sync each event into your CRM or trigger your own workflow (alert, task, hand-off).