Skip to content

Syncing data

Many integrations mirror users, participations or courses into another system, for example an HR system or a reporting tool. Repeatedly downloading complete lists is slow, and it consumes your rate limit without need: most of the objects have not changed since the last run.

Two mechanisms replace those full exports:

  • Webhooks push an event to you the moment something happens, for example when a participation is completed.
  • Delta queries fetch exactly the objects that changed since a given time, using the updated_after filter described below.

The recommended sync path is webhooks plus delta queries: react to events as they arrive, and use a delta query for the initial import and as a periodic reconciliation. No push channel is immune to missed deliveries, and webhook events currently cover users and participations; changes to courses, groups and the other resources are picked up by your delta queries. Polling with delta queries alone is the fallback when your system cannot receive webhooks.

Users, groups, courses, course categories and participations carry the timestamp updated_at, and their list endpoints accept the query parameter updated_after: the response then contains only objects whose updated_at is at the given time or later (the given time itself is included). Combine it with ordering=updated_at to receive the oldest changes first:

Terminal window
curl "https://api.schulungstool.de/v1/users/?updated_after=2026-01-15T12:00:00&ordering=updated_at" \
-H "Authorization: Token <API key>"

Timestamps use German time without a UTC offset, exactly as the API returns them; an ISO 8601 value with an offset (for example ...Z) is converted automatically. See Dates and times.

One deliberate exception: course ratings are anonymous, and precise timestamps could be matched against the completed_at of participations in the same course. Course ratings therefore carry no timestamps and no updated_after filter. They are a small data set; refresh them with an occasional full export, upserting by id.

  1. Initial export: fetch the complete list once with ordering=updated_at and page through all pages. Remember the updated_at of the last object as your cursor, copied verbatim from the response.
  2. Each sync run: request the first page of ?updated_after=<cursor>&ordering=updated_at.
  3. Process the results and upsert by id: because the cut-off is inclusive, the object at the cursor shows up again; with the id as key this is harmless.
  4. Set the cursor to the updated_at of the last object of the page and repeat from step 2, until a response contains fewer than 25 results (the page size, see Pagination and sorting).
  5. Rare edge case: if all 25 objects of a page share the same updated_at (for example after a bulk import), the cursor would not advance. In that case continue with page=2, page=3, … of the same query until the timestamp changes, then continue with step 4.

Always requesting the first page with a growing cursor (instead of paging once through page=1..n) makes the loop robust: objects that change while you are syncing move to the end of the ordering and are picked up by one of the next requests, nothing is skipped.

If you need absolute completeness, start each run with the cursor moved back one hour; because you upsert by id, the overlap is harmless. It covers two rare cases: timestamps from the repeated hour when daylight saving time ends (one night per year), and changes that became visible only when a longer transaction committed, after your previous run had already passed their timestamp.

updated_at changes whenever a field of the object that the API returns changes, no matter whether the change was made in the admin area, by a learner or through the API. Two details are worth knowing:

  • Group memberships update updated_at of both the affected user and the group, so membership changes surface in delta queries on /v1/users/ and /v1/groups/.
  • User activity alone does not count: last_active changes without updating updated_at. A user who merely logs in does not appear in your delta.

Deleted objects simply disappear from the lists; a delta query cannot report them. For users and participations, subscribe to the webhook events user.deleted and participation.deleted. For the other resources, or as a safety net, reconcile periodically (for example once per night): fetch all id values of the full list and remove local records whose id no longer exists. A detail request for a deleted object returns 404. Note that deleting a user or group also removes it from the other side’s membership lists without updating those objects; the reconciliation covers that case as well.

With webhooks in place, a reconciliation run once per hour or once per night is usually enough. Without webhooks, a delta request is still cheap for both sides, so polling every few minutes is fine. Choose the longest interval that is acceptable for your use case, and keep an eye on the rate limits if your account runs several integrations.