Errors, limits & paging

Conventions every endpoint follows: JSON shapes, error types, rate limits, pagination, dates and versioning.

Requests and responses

Dates and time zones

Errors

Every error has the same shape:

{
  "error": {
    "type": "validation_failed",
    "message": "`due_on`: Invalid ISO date",
    "request_id": "req_8f2a1c9b0d3e4f5a6b7c",
    "details": [{ "field": "due_on", "message": "Invalid ISO date" }]
  }
}
Status type Meaning / what to do
400 invalid_request Malformed JSON, wrong content type, bad cursor. Fix the request.
401 unauthorized Missing, invalid, expired or revoked credential. Re-authenticate (OAuth: refresh, then re-authorize).
403 insufficient_scope The credential lacks write. Ask the member to reconnect with edit access.
403 membership_required Subscription not active. Tell the member; don't retry.
404 not_found No such route, or the object doesn't exist or isn't this member's (we don't distinguish).
405 method_not_allowed See the Allow header.
409 conflict A limit was reached (30 courses, 20 milestones per item).
422 validation_failed Input failed validation; details lists each field.
429 rate_limited Wait Retry-After seconds.
500 internal_error Our fault. Retry with backoff; include request_id if you contact support.

Every response has an X-Request-Id header. Quote it to support@aceversity.com.

Rate limits

120 requests per minute per credential (each API key or OAuth token), fixed one-minute windows. Every response tells you where you are:

RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 42

Over the limit you get 429 with Retry-After. The OAuth endpoints have their own per-IP limits (token: 60/min, registration: 20/hour). Sign-in pages: 15 attempts per 10 minutes per IP.

Recommended client behaviour: on 429 sleep Retry-After; on 5xx or network errors retry up to 3 times with exponential backoff (1 s, 2 s, 4 s) — only for GET, PUT, PATCH and DELETE, which are idempotent. Don't blindly retry POST (you could create a duplicate).

Pagination

List endpoints return:

{ "data": [ … ], "has_more": true, "next_cursor": "MjU" }

Pass limit (1–100, default 25) and cursor=<next_cursor> for the next page. Stop when has_more is false. Small collections (courses, decks, milestones) return everything in one page.

Polling for changes

For automations ("when a new deadline is added, do X"), call GET /v1/items?created_after=<ISO timestamp of your last run> on a schedule. Store the newest created_at you saw and pass it next time. (Webhooks are on the roadmap.)

Versioning