Conventions
Everything on /v1 behaves the same way, so a client writes the paging loop, the timestamp parsing and the caching once. This page is the whole of it; the API reference covers what each endpoint returns.
Collections come in pages
Section titled “Collections come in pages”Every collection answers with the same envelope, and the counts describe the whole result set rather than the page, so you can size the loop before running it.
{ "data": [], "page": 1, "pageSize": 25, "totalItems": 63, "totalPages": 3}| Parameter | Default | Notes |
|---|---|---|
page |
1 |
Starts at 1 |
pageSize |
25 |
Maximum 100 |
sort |
Per endpoint | field|asc or field|desc, for example sort=name|desc |
Filtering, sorting and paging all happen on our side. There is no “return everything and filter it yourself” mode, and no endpoint that hands you the whole workspace.
Bad input is refused rather than quietly corrected: an unknown sort field, a pageSize of 500 or a malformed sort all answer 400 naming what is wrong and what the valid values are.
Timestamps and time zones
Section titled “Timestamps and time zones”Timestamps are RFC 3339 with an offset: 2026-08-14T09:12:44+00:00. Field names carry no Utc suffix, because the format already says it.
Time zones are returned as the identifier the rotation was configured with (Europe/Rome, America/New_York), never collapsed onto some other name that happens to share the same rules today.
The schedule endpoint asks you to name the window you want, with from and to, both required and no more than 90 days apart. A projection has no natural end, so there is no default.
Conditional reads
Section titled “Conditional reads”Reads that are a pure function of one rotation carry a strong ETag:
GET /v1/rotations/664f… HTTP/1.1Authorization: Bearer rr_live_...
HTTP/1.1 200 OKETag: "7:2026-08-14T17:00:00.0000000Z"Send it back on the next poll and an unchanged resource costs you a 304 with no body:
GET /v1/rotations/664f… HTTP/1.1Authorization: Bearer rr_live_...If-None-Match: "7:2026-08-14T17:00:00.0000000Z"
HTTP/1.1 304 Not ModifiedTreat the value as opaque and send back exactly what you were given, quotes included. The 304 carries the validator too, so keep replacing the one you hold.
Conditional writes
Section titled “Conditional writes”Every write against a rotation that already exists requires an If-Match header. It is not optional: a write without one is refused 428, never carried out unconditionally.
Read first, send back the validator you were given, and the write happens only if nothing moved in between:
POST /v1/rotations/664f…/rotate HTTP/1.1Authorization: Bearer rr_live_...If-Match: "7:2026-08-14T17:00:00.0000000Z"
HTTP/1.1 200 OKETag: "9:2026-08-21T17:00:00.0000000Z"The response carries the new validator, so a client that writes twice in a row does not have to read in between.
| What you send | What comes back |
|---|---|
| The current validator | 200 with the new state and a fresh ETag |
| A validator that is no longer current | 412 Precondition Failed, and nothing changed |
No If-Match at all |
428 Precondition Required, and nothing changed |
If-Match: * |
200. The write goes ahead against whatever version is current |
Send the validator back exactly as you received it, quotes included. A weak validator (W/"7") never matches, so it reads as 412.
When you have not read first
Section titled “When you have not read first”If-Match: * is the way out, and it is a real one. In HTTP it means “any current representation”, so sending it says: write this against whatever is there now, I accept that somebody may have changed it since.
POST /v1/rotations/664f…/rotate HTTP/1.1Authorization: Bearer rr_live_...If-Match: *
HTTP/1.1 200 OKETag: "9:2026-08-21T17:00:00.0000000Z"That is a genuine last-write-wins write, with everything that implies: two callers sending * a second apart both succeed, and the shift lands two people along. Reach for it when the write is the whole intent and there is nothing to compare against, for example a deploy script that puts a named person on duty and does not care who held it before. Avoid it for rotate, where “advance by one” applied twice is a mistake nobody can see.
The opt-out is per request. Sending * once changes nothing about the next write, which is checked normally if it carries a real validator.
Reads never ask for If-Match. Only writes do.
Creating things
Section titled “Creating things”POST /v1/rotations requires an Idempotency-Key header instead of an If-Match. There is no version to match on before the rotation exists, and rotation names are not unique, so nothing else could tell your retry apart from a second rotation you meant to create.
Send any value you can reproduce on a retry, up to 255 characters. A UUID per attempt is the usual choice; a run id from your pipeline works just as well, as long as retrying the same attempt sends the same key.
POST /v1/rotations HTTP/1.1Authorization: Bearer rr_live_...Idempotency-Key: 8f0b2f5a-3f2d-4a1e-9a94-0c2f7f1a5b33Content-Type: application/json
{"name": "Platform on-call", "owners": ["U0123456789"]}
HTTP/1.1 201 CreatedLocation: /v1/rotations/664f…ETag: "1:2026-08-16T09:00:00.0000000Z"Retry that request within 24 hours and you get 200 with the rotation you already created, and nothing new is made. A timeout is therefore safe to retry: either the first attempt never landed and the retry creates the rotation, or it did and the retry shows you what it created.
| What you send | What comes back |
|---|---|
| A key not seen before | 201 with the new rotation, a Location and an ETag |
| The same key again, within 24 hours | 200 with the rotation it created the first time, and nothing new |
| The same key again, more than 24 hours later | 409 Conflict, and nothing created |
No Idempotency-Key at all |
428 Precondition Required, and nothing created |
The replay answers 200 rather than 201, so a client counting creates by status is not told the same rotation was made twice. It carries the ETag either way, because the answer to a create is the precondition for the next write to it.
The window is 24 hours, measured from when the rotation was created. Past that the key is refused rather than replayed, so generate a fresh key per attempt rather than holding a fixed one.
Creating needs the write:rotation scope, and a key restricted to particular rotations cannot create: a new rotation is not one of the rotations it names.
Caching
Section titled “Caching”Every /v1 response carries Cache-Control: private. Answers are scoped to one key’s workspace, so they may be cached by your own client and never by a shared proxy in between.
Polling
Section titled “Polling”Every key has a rate limit, and it is generous. Poll no faster than you need an answer and use the ETag where there is one: a conditional poll that returns 304 barely touches your allowance.
If you are polling to find out who is on call, one request is enough: GET /v1/rotations/{rotationId}/on-call?expand=users returns the people and their names in a single call.
Next steps
Section titled “Next steps”- Errors: what a
400,403or404body looks like and how to branch on it - API reference: per-endpoint parameters and response shapes
- Scopes and access: why a collection may be shorter than you expect
