Conventions
These hold for every v2 endpoint, now and later.
Shapes
Section titled “Shapes”- JSON, in camelCase.
- Times are ISO 8601 in UTC (
2026-10-01T15:00:00.000Z). Dates with no time, such as birthdays, areYYYY-MM-DD. - Money is a decimal string with its currency, never a float:
{ "amount": "25.00", "currency": "USD" }. - Every listed field is always present. Empty is
null, not missing. The only exceptions are fields added byinclude=, which appear only when asked for. - Ignore fields you don’t know. v2 adds fields over time.
Lists take ?limit= (1–200, default 50) and ?cursor=, and return:
{ "items": [ ... ], "nextCursor": "eyJ..." }- Pass
nextCursorback as?cursor=until it’snull. - Cursors are opaque. They only work with the filters they came from.
- A walk sees every item once, in a stable order, even while the data changes. Each list says what its order is.
- A page may hold fewer than
limititems, for example when an app’s youth rule leaves some out. Keep going untilnextCursorisnull.
include=
Section titled “include=”include= adds related data in the same call: ?include=rsvpCounts,ticketTypes. Each endpoint lists what it accepts. Asking for anything else is an error, never silently ignored.
Writes
Section titled “Writes”- The body is a JSON object of at most 16 KB, sent with
Content-Type: application/json. - Fields are checked strictly. An unknown field, a missing required one, the wrong type or text that’s too long gets
400 invalid_body, with the field named inparameter. Text is trimmed. - A
PATCHchanges only the fields sent.nullclears a field that can be empty.
Errors
Section titled “Errors”Errors have one shape:
{ "error": { "code": "invalid_parameter", "message": "limit must be a whole number from 1 to 200", "parameter": "limit" } }codeis stable: switch on it. New codes may be added.messageis for people, and may change.parameternames the query parameter or body field at fault.
| Status | Codes |
|---|---|
| 400 | invalid_parameter, invalid_body, invalid_profile, invalid_descriptor, not_a_member |
| 401 | unauthorized |
| 403 | forbidden, insufficient_scope, key_owner_not_leader, app_not_installed, scope_not_allowed, app_suspended |
| 404 | not_found |
| 409 | no_profile, already_exists, position_inactive, position_full, already_assigned, assignment_ended, version_not_current, nothing_to_pay, payments_not_ready |
| 413 | payload_too_large |
| 429 | rate_limited: wait for Retry-After |
| 500 | internal_error |
| 503 | unavailable: try again |
Versioning
Section titled “Versioning”Within v2, changes only add:
- endpoints;
- optional query parameters and
include=values; - response fields;
- enum values;
- error codes.
Never within v2:
- removing or renaming anything;
- changing a type or format;
- making a parameter required;
- narrowing what’s accepted.
Grouperly’s build checks every change against the published OpenAPI document and fails on a breaking one. A breaking change would be v3. v2 would then keep working for 12 months, with Deprecation and Sunset headers from the day v3 ships.
The OpenAPI document’s info.version says which v2 release is live.