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:

FeedURL
Membershttps://api.gradual-api.com/public-api/v1/incremental/users
Event attendeeshttps://api.gradual-api.com/public-api/v1/incremental/event-attendees
Course enrollmentshttps://api.gradual-api.com/public-api/v1/incremental/course-enrollments
Course certificateshttps://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 a cursor. Omit both to get every row.
  • cursor, string, optional, the nextCursor from your previous page. Takes precedence over startTime.
  • 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, true when more changes are already waiting. Keep paging without delay while this is true.
  • serverTimestamp, string, the point in time this response is complete up to. See Delivery guarantees.
  • size, number, the number of rows in results.

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 year
curl -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 page
curl -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 above
  • includes, string, optional, comma-separated list of the relations to hydrate. Accepts questionnaire, spaces, and points. Any other value returns 400.

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",
"userEmail": "[email protected]",
"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:

  • id and userId are 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",
"attendeeEmail": "[email protected]",
"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:

  • id and attendeeId are the same value for this feed.
  • eventId is the event the registration belongs to. Join it against Get an Event by Event ID.
  • cancelledAt is set when the attendee cancelled their registration, and empty otherwise. A cancelled registration is still removed: false.
  • joinedAt, lastOnlineAt, checkedInAt, and registrationSource follow 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",
"enrolleeEmail": "[email protected]",
"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",
"enrolleeEmail": "[email protected]",
"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:

  • id identifies the enrollment record, not the member. A member who is reset and re-enrolled on the same course has one row per enrollment.
  • activeStatus is ACTIVE, CANCELLED_BY_ADMIN, CANCELLED_BY_USER, or RESET. This is the enrollment lifecycle. removed stays false through all of it.
  • terminatedAt is when the enrollment was cancelled or reset, and empty while it is active.
  • progressStatus, completedAt, progress, and score describe 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",
"recipientEmail": "[email protected]",
"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",
"recipientEmail": "[email protected]",
"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:

  • id and certificateId are the same value for this feed.
  • certificateStatus is Issued, Expired, or Revoked. A revoked certificate stays in the feed as removed: false and carries revokedAt and revocationReason.
  • expiryDate is empty when the certificate does not expire.
  • recipientTitle and recipientCompany are captured when the certificate is issued and do not follow later profile changes.

Errors

  • 400, an invalid cursor, startTime, pageSize, or includes value. The message names the problem.
  • 401, missing or invalid credentials.
  • 429, rate limited. See Rate Limits.