The incremental endpoints are change feeds. Instead of re-reading a whole resource on every run, you poll for the rows that changed since the last time you asked, and apply them to a copy you keep on your side. They are built for warehouse and CRM syncs.
There are four feeds, all tenant-wide:
| Feed | URL |
|---|---|
| Members | https://api.gradual-api.com/public-api/v1/incremental/users |
| Event attendees | https://api.gradual-api.com/public-api/v1/incremental/event-attendees |
| Course enrollments | https://api.gradual-api.com/public-api/v1/incremental/course-enrollments |
| Course certificates | https://api.gradual-api.com/public-api/v1/incremental/course-certificates |
All four are GET, return application/json, and are rate limited at the Medium tier.
How the feeds work
Every feed shares one request and response contract.
Query Parameters:
startTime, string, optional, ISO 8601 timestamp. Returns rows changed at or after this time. Used only when you do not pass acursor. Omit both to get every row.cursor, string, optional, thenextCursorfrom your previous page. Takes precedence overstartTime.pageSize, number, optional, 1 to 100, default 50.
The members feed takes one extra parameter, includes, described in its own section below.
Response Example:
{"results": [],"nextCursor": "eyJ2IjoxLCJ0IjoxNzcyMTA4MzIwMDAwLCJpZCI6IjY1NGQ2NmY4ZWE1MzJlYTJhYzhjZjAwNCIsImYiOiI0ZjUzY2RhMThjMmJhYTBjIn0","hasMore": true,"serverTimestamp": "2026-08-24T09:12:00.000Z","size": 50}
Response Fields:
results, array, the changed rows, ordered oldest change first.nextCursor, string, the resume point for your next request. Absent when the page is empty.hasMore, boolean,truewhen more changes are already waiting. Keep paging without delay while this istrue.serverTimestamp, string, the point in time this response is complete up to. See Delivery guarantees.size, number, the number of rows inresults.
Row shape
Each row carries three fields on top of the resource payload:
id, string, the row's unique identifier. Use it as the primary key of your copy.removed, boolean, whether the row was deleted.updatedAt, string, ISO 8601 timestamp of the change.
A deleted row arrives as a tombstone: no resource fields, only the envelope plus removedAt.
{"id": "654d66f8ea532ea2ac8cf004","removed": true,"removedAt": "2026-08-24T08:41:12.000Z","updatedAt": "2026-08-24T08:41:12.000Z"}
removed covers deletion only. Everything else a record can become stays in the payload: a deactivated member is removed: false with archived: true, a cancelled enrollment is removed: false with a cancelled activeStatus, and a revoked certificate is removed: false with certificateStatus: "Revoked". Read those fields rather than treating a lifecycle change as a delete.
Paging through a feed
Bootstrap, then resume. On the first run, pass startTime (or omit it to pull everything). After that, pass cursor on its own.
# 1. First run: everything changed since the start of the yearcurl -H "x-client-id: $CLIENT_ID" -H "authorization: Bearer $TOKEN" \"https://api.gradual-api.com/public-api/v1/incremental/users?startTime=2026-01-01T00:00:00Z&pageSize=100"# 2. Keep paging while hasMore is true, passing the nextCursor from the previous pagecurl -H "x-client-id: $CLIENT_ID" -H "authorization: Bearer $TOKEN" \"https://api.gradual-api.com/public-api/v1/incremental/users?cursor=$NEXT_CURSOR&pageSize=100"# 3. hasMore is false: store nextCursor, sleep, then send the same request again later
Store nextCursor once a run finishes and reuse it as the starting point of the next run. It stays valid indefinitely.
The cursor is opaque. Do not parse it, build one, or edit it. Send back exactly the string you were given.
Delivery guarantees
Ordering. Rows come back oldest change first. A row that changes while you are paging moves to the end of the feed, so you see its latest state rather than losing it.
At least once. The same row can arrive more than once, on retries and across runs. Apply every row as an upsert keyed on id and the feed becomes safe to replay.
A two minute delay. Each response holds back anything that changed in the last two minutes, and serverTimestamp tells you how far the response is complete. This is what makes a cursor safe to resume from. Expect a change made in Gradual to show up in the feed a couple of minutes later.
Cursor errors
A cursor is rejected with 400 when it is malformed, or when Gradual has changed the cursor format. The message tells you which:
{"message": "invalid cursor"}
{"message": "invalid cursor: unsupported version, restart from startTime"}
Recover by re-bootstrapping: drop the stored cursor and send startTime set a little before the serverTimestamp of your last successful page. Duplicate rows are expected and harmless, because every row is an upsert.
Members feed
URL: https://api.gradual-api.com/public-api/v1/incremental/users
HTTP Method: GET
Content type: application/json
Tier: Medium
Query Parameters:
startTime,cursor,pageSize, as described aboveincludes, string, optional, comma-separated list of the relations to hydrate. Acceptsquestionnaire,spaces, andpoints. Any other value returns400.
Rows carry the same member fields as Get a User by User ID. The three relations are off by default, and their keys (questionnaire, spaces, lifetimePoints, remainingPoints) are absent from the row unless you ask for them.
Response Example:
{"results": [{"userId": "5f8d0d55b54764421b7156c3","userFirstName": "Jane","userLastName": "Doe","displayName": "Jane Doe","userTitle": "Product Manager","userCompany": "Acme Inc.","userLinkedIn": "https://www.linkedin.com/in/janedoe","userLocation": "London, UK","userAvatarUrl": "https://cdn.example.com/avatars/jane.png","memberType": "Standard","approvalStatus": "approved","signUpAt": "2026-01-12T09:31:00.000Z","lastLoginAt": "2026-05-20T14:02:11.000Z","onboardingAt": "2026-01-12T09:45:00.000Z","archived": false,"id": "5f8d0d55b54764421b7156c3","removed": false,"updatedAt": "2026-08-24T08:55:03.000Z"},{"id": "6553ce608b3f645e79d95034","removed": true,"removedAt": "2026-08-24T09:01:44.000Z","updatedAt": "2026-08-24T09:01:44.000Z"}],"nextCursor": "eyJ2IjoxLCJ0IjoxNzcyMTA4MTA0MDAwLCJpZCI6IjY1NTNjZTYwOGIzZjY0NWU3OWQ5NTAzNCIsImYiOiI0ZjUzY2RhMThjMmJhYTBjIn0","hasMore": false,"serverTimestamp": "2026-08-24T09:12:00.000Z","size": 2}
Field notes:
idanduserIdare the same value for this feed.- With
includes=questionnaire, an edit that changes only a profile answer may not bump the member row, so it can wait for the next member-level change before it is emitted. Answers are current in the rows you do receive.
Event attendees feed
URL: https://api.gradual-api.com/public-api/v1/incremental/event-attendees
HTTP Method: GET
Content type: application/json
Tier: Medium
Covers registrations across every event in the community, so it needs no event ID. Rows carry the same fields as Get Event Attendees, plus eventId and cancelledAt.
Response Example:
{"results": [{"attendeeId": "654d66f8ea532ea2ac8cf004","userId": "654d6623ea532ea2ac8ced28","attendeeFirstName": "Jane","attendeeLastName": "Doe","attendeeDisplayName": "Jane Doe","attendeeType": "Attendee","ticketType": "General Admission","registeredAt": "2026-08-20T23:10:48.118Z","joinedAt": "2026-08-22T17:02:35.000Z","lastOnlineAt": "2026-08-22T18:14:07.000Z","checkedInAt": "","registrationSource": "selfSignUp","attendeeLinkedUrl": "https://www.linkedin.com/in/janedoe","attendeeCompanyName": "Test Company","attendeePosition": "Software Engineer","eventQuestions": "What is your dietary preference?: Vegetarian;","eventId": "654180e3566f8d5a7fb49e86","cancelledAt": "","id": "654d66f8ea532ea2ac8cf004","removed": false,"updatedAt": "2026-08-22T18:14:07.000Z"}],"nextCursor": "eyJ2IjoxLCJ0IjoxNzcxNzczMjQ3MDAwLCJpZCI6IjY1NGQ2NmY4ZWE1MzJlYTJhYzhjZjAwNCIsImYiOiI0ZjUzY2RhMThjMmJhYTBjIn0","hasMore": false,"serverTimestamp": "2026-08-24T09:12:00.000Z","size": 1}
Field notes:
idandattendeeIdare the same value for this feed.eventIdis the event the registration belongs to. Join it against Get an Event by Event ID.cancelledAtis set when the attendee cancelled their registration, and empty otherwise. A cancelled registration is stillremoved: false.joinedAt,lastOnlineAt,checkedInAt, andregistrationSourcefollow the same rules as the Get Event Attendees endpoint.
Course enrollments feed
URL: https://api.gradual-api.com/public-api/v1/incremental/course-enrollments
HTTP Method: GET
Content type: application/json
Tier: Medium
Covers enrollments across every course in the community, so it needs no course ID.
Response Example:
{"results": [{"userId": "654d6623ea532ea2ac8ced28","enrolleeFirstName": "Jane","enrolleeLastName": "Doe","enrolleeDisplayName": "Jane Doe","enrolleePosition": "Software Engineer","enrolleeCompany": "Test Company","courseId": "665a1b2c3d4e5f6a7b8c9d0e","enrolledAt": "2026-01-20T14:30:00.000Z","activeStatus": "ACTIVE","terminatedAt": "","progressStatus": "Completed","completedAt": "2026-02-15T09:00:00.000Z","progress": "100%","score": "90","id": "665a1b2c3d4e5f6a7b8c9d20","removed": false,"updatedAt": "2026-02-15T09:00:00.000Z"},{"userId": "6553ce608b3f645e79d95034","enrolleeFirstName": "John","enrolleeLastName": "Smith","enrolleeDisplayName": "John Smith","enrolleePosition": "Product Manager","enrolleeCompany": "Gradual Inc","courseId": "665a1b2c3d4e5f6a7b8c9d0e","enrolledAt": "2026-03-10T08:00:00.000Z","activeStatus": "CANCELLED_BY_USER","terminatedAt": "2026-04-02T11:20:00.000Z","progressStatus": "In Progress","completedAt": "","progress": "75%","score": "","id": "665a1b2c3d4e5f6a7b8c9d21","removed": false,"updatedAt": "2026-04-02T11:20:00.000Z"}],"nextCursor": "eyJ2IjoxLCJ0IjoxNzc1MTMzNjAwMDAwLCJpZCI6IjY2NWExYjJjM2Q0ZTVmNmE3YjhjOWQyMSIsImYiOiI0ZjUzY2RhMThjMmJhYTBjIn0","hasMore": false,"serverTimestamp": "2026-08-24T09:12:00.000Z","size": 2}
Field notes:
ididentifies the enrollment record, not the member. A member who is reset and re-enrolled on the same course has one row per enrollment.activeStatusisACTIVE,CANCELLED_BY_ADMIN,CANCELLED_BY_USER, orRESET. This is the enrollment lifecycle.removedstaysfalsethrough all of it.terminatedAtis when the enrollment was cancelled or reset, and empty while it is active.progressStatus,completedAt,progress, andscoredescribe the learner's progress on that enrollment, with the same values as Get Course Enrollees.- Progress is a snapshot taken when the row is emitted. Progress and completion are tracked on a separate record, so finishing a lesson does not by itself re-emit the enrollment. Use the Get Course Enrollees endpoint when you need progress that is current to the minute.
Course certificates feed
URL: https://api.gradual-api.com/public-api/v1/incremental/course-certificates
HTTP Method: GET
Content type: application/json
Tier: Medium
Covers certificates across every course in the community, so it needs no course ID. Rows carry the same fields as Get Course Certificate Recipients, plus courseId, revokedAt, and revocationReason.
Response Example:
{"results": [{"certificateId": "665a1b2c3d4e5f6a7b8c9d10","certificateName": "Engineering Leadership Certificate","userId": "654d6623ea532ea2ac8ced28","recipientName": "Jane Doe","recipientTitle": "Software Engineer","recipientCompany": "Test Company","certificateStatus": "Issued","issuedAt": "2026-02-15T09:00:00.000Z","expiryDate": "2028-02-15T00:00:00.000Z","certificateUrl": "https://community.gradual/home/certificate/abcd1234567890","courseId": "665a1b2c3d4e5f6a7b8c9d0e","revokedAt": "","revocationReason": "","id": "665a1b2c3d4e5f6a7b8c9d10","removed": false,"updatedAt": "2026-02-15T09:00:00.000Z"},{"certificateId": "665a1b2c3d4e5f6a7b8c9d11","certificateName": "Engineering Leadership Certificate","userId": "6553ce608b3f645e79d95034","recipientName": "John Smith","recipientTitle": "Product Manager","recipientCompany": "Gradual Inc","certificateStatus": "Revoked","issuedAt": "2026-01-10T10:00:00.000Z","expiryDate": "","certificateUrl": "https://community.gradual/home/certificate/efgh5678901234","courseId": "665a1b2c3d4e5f6a7b8c9d0e","revokedAt": "2026-06-18T13:45:00.000Z","revocationReason": "Issued in error","id": "665a1b2c3d4e5f6a7b8c9d11","removed": false,"updatedAt": "2026-06-18T13:45:00.000Z"}],"nextCursor": "eyJ2IjoxLCJ0IjoxNzgxNzk1NTAwMDAwLCJpZCI6IjY2NWExYjJjM2Q0ZTVmNmE3YjhjOWQxMSIsImYiOiI0ZjUzY2RhMThjMmJhYTBjIn0","hasMore": false,"serverTimestamp": "2026-08-24T09:12:00.000Z","size": 2}
Field notes:
idandcertificateIdare the same value for this feed.certificateStatusisIssued,Expired, orRevoked. A revoked certificate stays in the feed asremoved: falseand carriesrevokedAtandrevocationReason.expiryDateis empty when the certificate does not expire.recipientTitleandrecipientCompanyare captured when the certificate is issued and do not follow later profile changes.
Errors
400, an invalidcursor,startTime,pageSize, orincludesvalue. The message names the problem.401, missing or invalid credentials.429, rate limited. See Rate Limits.