Skip to main content
Back to Guides

Event Notifications

Receive platform events by polling the events API

Thought Industries exposes platform activity — enrollments, completions, certificates, quiz attempts, and more — through the GET /incoming/v2/events/:kind endpoint. Integrations poll this endpoint on a schedule and follow the returned cursor until the feed is drained.

No outbound webhook management API

There is currently no REST endpoint for registering, listing, or deleting outbound webhooks, and the platform does not sign or POST event payloads to arbitrary URLs you supply. If you have seen references to endpoints such as POST /incoming/v2/webhooks, an X-TI-Signature header, or whsec_ secrets, treat those as documentation errors — they are not implemented.

The platform does host inbound webhook receivers for specific integrations (Salesforce, Foxy, SurveyGizmo, Integrity Advocate). Those are wired up as part of enabling the integration and are not general-purpose event delivery.

Polling the events API

Call GET /incoming/v2/events/:kind with a startDate bound (epoch milliseconds) and follow pageInfo.cursor until hasMore is false. See Notifications & Learner Actions for per-kind parameters and payload shapes.

curl
# Poll for new course actions since your last checkpoint
curl -s -H "Authorization: Bearer $TI_API_KEY" \
  "https://api.thoughtindustries.com/incoming/v2/events/courseAction?startDate=$LAST_MS"

# Response shape
# {
#   "pageInfo": { "cursor": "...", "hasMore": true },
#   "events": [ { ... }, { ... } ]
# }
drain.js
// Poll a single event kind and follow the cursor until drained
async function drain(kind, apiKey, startMs) {
  let cursor = null;
  let params;
  do {
    params = new URLSearchParams({ startDate: String(startMs) });
    if (cursor) params.set('cursor', cursor);

    const res = await fetch(
      `https://api.thoughtindustries.com/incoming/v2/events/${kind}?${params}`,
      { headers: { Authorization: `Bearer ${apiKey}` } }
    );
    const body = await res.json();

    for (const evt of body.events) {
      // process(evt) — must be idempotent, events may repeat across polls
    }

    cursor = body.pageInfo.hasMore ? body.pageInfo.cursor : null;
  } while (cursor);
}

Event kinds

Each kind is a separate path — poll each one you care about independently.

courseActionCourse-level learner activity (starts, completions, progress).
courseEnrollmentLearner enrolled in a course.
courseCompletionLearner completed all required course items.
courseCertificateCertificate issued for a completed course.
learningPathActionLearner activity within a learning path.
learningPathEnrollmentLearner enrolled in a learning path.
quizAttemptLearner submitted a quiz attempt.
assignmentSubmissionLearner submitted an assignment.

Best practices

Persist a checkpoint per kind

Track the highest timestamp you have processed for each kind and pass it as startDate on the next poll.

Drain the cursor before advancing

Keep following pageInfo.cursor until hasMore is false, then update your checkpoint.

Design handlers to be idempotent

Overlapping polls may return the same event twice. Deduplicate by event id or a natural key.

Respect rate limits

The events endpoint is throttled separately from resource endpoints. Back off on 429 responses using the Retry-After header.