Skip to content

Conventions

These hold for every v2 endpoint, now and later.

  • JSON, in camelCase.
  • Times are ISO 8601 in UTC (2026-10-01T15:00:00.000Z). Dates with no time, such as birthdays, are YYYY-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 by include=, 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 nextCursor back as ?cursor= until it’s null.
  • 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 limit items, for example when an app’s youth rule leaves some out. Keep going until nextCursor is null.

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.

  • 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 in parameter. Text is trimmed.
  • A PATCH changes only the fields sent. null clears a field that can be empty.

Errors have one shape:

{ "error": { "code": "invalid_parameter", "message": "limit must be a whole number from 1 to 200", "parameter": "limit" } }
  • code is stable: switch on it. New codes may be added.
  • message is for people, and may change.
  • parameter names 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

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.