Pagination, Rate Limits, and Errors
Collection endpoints use cursor pagination. Every successful page uses the same envelope:
{ "items": [], "nextCursor": null, "asOf": "2026-09-11T15:30:00.000Z"}Follow the cursor
Section titled “Follow the cursor”- Make the first request with your filters and optional
limit. - Process
items. - If
nextCursoris notnull, repeat the same request and pass that exact value ascursor. - Stop when
nextCursorisnull.
Treat cursors as opaque secrets. Don’t decode, edit, or concatenate them, and don’t store one as a permanent resume token. Each cursor is cryptographically bound to its organization, filters, asOf window anchor, and position. If you change a filter and reuse the cursor, you get 400 INVALID_CURSOR.
curl --get --fail-with-body \ --header "Authorization: Bearer $ROGUE_ARENA_TEAM_API_KEY" \ --data-urlencode "types=userAnsweredQuestion" \ --data-urlencode "cursor=$NEXT_CURSOR" \ "https://arena.roguelabs.io/api/v1/team/events"asOf holds the upper bound of the time series steady while you page. It anchors pagination and the time window. It is not a transactional snapshot, so the current roster, course relationships, current progress, and deployment status can change between pages. A membership change can invalidate a roster-bound cursor, and other changes to current state can affect later pages. If that happens, discard the cursor and restart the export. If you need to show which window a completed export read from, store asOf with it.
Page sizes
Section titled “Page sizes”| Endpoint family | Default | Maximum |
|---|---|---|
| Members and compact progress | 100 | 200 |
| Answer attempts | 100 | 100 |
| Deployments and events | 100 | 500 |
For compact progress, limit is the number of members evaluated on that page.
A member can have several course, scenario, or curriculum rows, so the items
array can hold more entries than limit. It can also be empty with a non-null
nextCursor when none of the evaluated members have active progress. Always
follow nextCursor. Only a null cursor marks the end of the export.
Team-wide event exports support organizations with up to 5,000 current members. For a larger roster, pass userId and export one member at a time.
Responses deliberately leave out a total count. Keep paging until nextCursor is null.
Time windows
Section titled “Time windows”Answers, deployments, and events accept RFC 3339 from and to filters with an explicit UTC designator or offset. Query timestamps can have at most millisecond precision, for example 2026-09-11T15:30:00.123Z or 2026-09-11T10:30:00.123-05:00, and the API converts them to UTC. from is inclusive and to is exclusive. If you leave them out, event and history queries use a bounded recent window. A single request can’t span more than 366 days, so split longer exports into adjacent windows.
Response rows and opaque cursors can keep microsecond precision, even though query inputs stop at milliseconds. Don’t truncate a cursor or rebuild one from a returned timestamp. Always pass nextCursor back unchanged.
Rate limits
Section titled “Rate limits”Normal calls are limited to 60 requests per minute per key and 120 requests per minute per organization. Fresh progress calls have deliberately tighter limits: 6 per minute per key and 12 per minute per organization.
The edge gateway also applies a coarse flood ceiling per source. Whether a 429 comes from that guard or from the key and organization budgets, it uses the same error envelope and includes Retry-After.
A 429 response includes Retry-After. Wait that many seconds, add a small random delay, and then retry. Don’t retry right away in a tight loop. Using separate keys doesn’t get around the organization-wide budget.
Progress is live data, and full responses aren’t cached. Use the compact endpoint for scheduled exports, avoid polling each user individually, and fetch detailed progress only when you drill into one user.
Error envelope
Section titled “Error envelope”Errors use a stable JSON shape:
{ "error": { "code": "RATE_LIMITED", "message": "Request rate limit exceeded", "requestId": "…", "retryAfterSeconds": 42 }}| HTTP status | Typical code | Action |
|---|---|---|
| 400 | BAD_REQUEST, INVALID_CURSOR | Correct the parameter or restart pagination without a cursor |
| 401 | UNAUTHORIZED | Check the bearer header, expiry, and whether the key was revoked |
| 403 | FORBIDDEN | Create or use a key with the required permission |
| 404 | NOT_FOUND | Confirm the user ID belongs to the key’s current organization |
| 409 | CONFLICT | The current state doesn’t allow the action, for example the deployment isn’t running. Read the message and resolve that state before you retry; on deploy, read the admission object for the reason |
| 409 | DEPLOYMENT_NOT_READY | The deployment is still deploying. Poll it until readiness is ready, then request the VPN config again |
| 409 | VPN_GATEWAY_FAILED | The deployment’s VPN gateway couldn’t be prepared; the message says why. Request again once the deployment is ready, or start or revert it to retry |
| 409 | VPN_CONFIG_NOT_READY | The VPN config is still being created, or its creation failed. Poll the config list until it’s ready; if it failed, request a new config |
| 429 | RATE_LIMITED | Honor Retry-After |
| 500 | INTERNAL_ERROR | Record the request ID and retry cautiously; contact Rogue Labs if it persists |
| 503 | UPSTREAM_UNAVAILABLE | Retry with exponential backoff; the API never substitutes empty training data for an upstream failure |
Retry 429 and transient 503 responses. Don’t automatically retry authorization or validation failures.