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_afterfilter 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.
The updated_after filter
Section titled “The updated_after filter”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:
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.
The sync loop
Section titled “The sync loop”- Initial export: fetch the complete list once with
ordering=updated_atand page through all pages. Remember theupdated_atof the last object as your cursor, copied verbatim from the response. - Each sync run: request the first page of
?updated_after=<cursor>&ordering=updated_at. - Process the results and upsert by
id: because the cut-off is inclusive, the object at the cursor shows up again; with theidas key this is harmless. - Set the cursor to the
updated_atof 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). - 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 withpage=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.
What counts as a change
Section titled “What counts as a change”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_atof 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_activechanges without updatingupdated_at. A user who merely logs in does not appear in your delta.
Deletions
Section titled “Deletions”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.
How often should you poll?
Section titled “How often should you poll?”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.