> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mightynetworks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Mighty API updates and changes

<Update label="September 15, 2026">
  **A deprecation policy for the Mighty API**

  We now publish a [deprecation policy](/deprecation-policy) that sets out how we handle breaking
  changes to the API. We announce a deprecation at least three months before we deploy the breaking
  change, record the deprecation and its replacement on this changelog, mark the deprecated element
  in the GraphQL schema, and notify you in-app if we detect that your integration still uses it.
  Security updates, critical bug fixes, and changes required to protect the reliability or integrity
  of the platform are outside that notice period.

  The policy also defines what we count as a breaking change, for GraphQL and for REST, and which
  additions are compatible, so you can tell from a changelog entry whether you need to act.
</Update>

<Update label="September 14, 2026">
  **The member roster pages all the way through**

  `network.members` no longer stops at 10,000 members. Previously `pageInfo.hasNextPage` went false
  once you had paged that far even though `totalCount` reported the whole roster, so a Network with
  more members than that could not be exported — and the same ceiling applied to `members(spaceId:)`
  and `members(planId:)`. Keep paging until `hasNextPage` is false and you now reach the end of any
  roster. See the [members reference](/members) for the full set of filters and fields.

  Two things to know about the pages you get. A page can hold fewer members than you asked for, or
  none at all, and still have more after it — that happens when a member was removed just after the
  roster read them, and `pageInfo.hasNextPage` is the only end-of-roster signal. And `totalCount` is
  re-counted on every request, so treat it as the current roster size rather than counting your pages
  against it. A member who has just joined may not appear yet.

  Cursors keep their opaque form but their contents have changed, including the per-edge cursors on
  `edges { cursor }`. An iteration that was part-way through when this shipped is rejected with a
  `BAD_USER_INPUT` error naming the cursor — start that iteration again from the first page. Always
  page from `pageInfo.endCursor` rather than from an edge's own cursor; see the
  [pagination guide](/api-pagination) for the general pattern.

  A cursor now belongs to the run that created it, so hold `sort`, `sortOrder` and `ambassador`
  constant across a paging run — and hold `term` constant too when you sort by `ROLE`, where the
  term's relevance is part of the ordering. Change one of those and the cursor is rejected with
  `BAD_USER_INPUT` rather than quietly returning the wrong page; start again from the first page
  under the ordering you want. Narrowing the other filters mid-run is still fine. The Ambassador
  roster's cursors are not interchangeable with the full roster's in either direction.

  **A new sort key for roster exports: `RESOURCE_ID`**

  `MemberSort` gains `RESOURCE_ID`, which sorts by the member's resource ID. Use it whenever you are
  reading a whole roster: it is the only sort key that never changes for a member, so a full
  enumeration returns every member exactly once. Choose it on your first call — switching to it while
  you already hold a cursor is rejected, because a cursor belongs to the sort that created it. Under
  the other sorts a member whose key moves while you page — their last visit, their name, their role
  — can come back twice or be missed, because each page is a fresh read rather than a snapshot. The
  Ambassador roster is always ordered by referral count and does not accept `RESOURCE_ID`, so that
  roster keeps the same caveat.
</Update>

<Update label="September 8, 2026">
  **`updateComment` links bare URLs and email addresses in a comment body**

  A bare URL or email address in the `body` you send to `updateComment` now comes back as a link in the stored comment, the way `createComment` has always handled one. An edited comment previously kept both as plain text, so the same text produced links when you created a comment and none when you updated it. An email address becomes a `mailto:` link.

  If you compare a comment's `body` against the text you sent, expect link markup around any bare URL or email address in it. Send your own `<a>` markup when you want to control the link yourself, because an anchor that is already in the text is left as it is.
</Update>

<Update label="September 3, 2026">
  **RSVPs require a write scope**

  `createRsvp`, `updateRsvp`, and `deleteRsvp` now require the calling credential to hold `write:rsvps` (for a member token) or `host:write:network_events` (for a Host credential such as an Admin API key). Either one satisfies the requirement. A read scope no longer does: a member token holding only `read:network` could previously write the member's own RSVP, because reading the event was the only check. A credential without either scope receives a `FORBIDDEN` error naming the scopes that satisfy the requirement. Tokens issued before scope enforcement began are exempt.

  `write:rsvps` also lets a token read the event it is answering and the RSVP it creates — `Event`, `EventInstance`, `EventRecurrenceException`, and `Rsvp` all accept it — but it does not grant listing the Network's events. An integration that browses events still requests `read:network`.

  If your application RSVPs on a member's behalf, request `write:rsvps` — or `host:write:network_events` on a Host credential — when authorizing. Members who already authorized your application need to authorize again so their tokens are reissued with the new scope. See [OAuth Applications — Scopes](/oauth-applications#scopes).
</Update>

<Update label="September 1, 2026">
  **Create a member from an email address**

  `createMember` creates an account for an email address and adds it to your Network as a full member, so you can onboard someone who has never signed up. Pass `email`, `firstName`, and `lastName`. An account that already exists for that address is reused rather than duplicated, and a call naming someone who already belongs to your Network — as a full or limited member — returns an error rather than a second membership. To admit someone who already has an account in your Network, use `createNetworkMembership` instead.

  `role` sets the new member's role on the Network and defaults to `CONTRIBUTOR`. `sendWelcomeEmail` defaults to `true` and emails the new member a sign-in link; set it to `false` to skip it. The payload returns the new `member` and an `errors` list carrying anything you can correct and retry.

  The mutation requires a token acting as a Network Host and holding the `host:write:network_members` scope. See [OAuth Applications — Scopes](/oauth-applications#scopes).
</Update>

<Update label="August 31, 2026">
  **Uploading media requires an asset write scope**

  `createUploadSession`, `completeUploadSession`, and `abortUploadSession` — and the `POST /networks/:network_id/assets` REST endpoint — now require the calling credential to hold `write:assets` (for a member token) or the new `host:write:network_assets` scope (for a Host credential such as an Admin API key). A write scope for another resource, such as `write:posts`, no longer satisfies the upload endpoints. A credential without either scope receives a `FORBIDDEN` error naming the scopes that satisfy the requirement. Tokens issued before scope enforcement began are exempt.

  `deleteAsset` now accepts `host:write:network_assets` alongside `write:assets`, and the `write:assets` consent description covers uploading as well as deleting media.

  If your integration uploads media, request `write:assets` — or `host:write:network_assets` on a Host credential — when authorizing.
</Update>

<Update label="August 28, 2026">
  **A member total on the Network roster**

  `Network.members` returns a `totalCount`: how many members match the search and filters you passed, across every page. Getting that number used to mean paging the roster to the end, because `pageInfo.hasNextPage` only tells you the page in your hand is not the last one.

  The count answers the query you sent, so it narrows with `term` and with the `role`, `memberType`, `spaceId`, `segmentIds`, and `ambassador` filters. Ask for `totalCount` with `first: 1` when you want the size of a roster and not its members. It is an exact count at any size, so a Network with hundreds of thousands of members reports what it actually has.

  Reading the roster still requires a token acting as a Network Host or Moderator. A token without that access continues to receive an empty connection, and its `totalCount` is `0` rather than the roster size.
</Update>

<Update label="August 27, 2026">
  **The `User` type is deprecated in favor of `Member`**

  The [schema](/api) now describes members with a `Member` interface that carries every field `User` had, and every field that returned `User` — `me`, `Comment.author`, `Post.creator`, mutation payloads, and the rest — now returns `Member`. The connection wrappers keep their names for now: `Member.followers`, `Member.followedMembers`, and `Network.blockedMembers` still return a `UserConnection` whose nodes are `User`. `User` remains in the schema as the concrete type implementing `Member`, so your existing requests keep working: plain field selections are unchanged, `... on User` fragments still match, and `__typename` still returns `"User"`.

  When we remove `User`, `Member` will become the concrete type and `__typename` will return `"Member"`. We will announce the removal here before it happens. To be ready for it, write new fragments on `Member`, and accept either `"User"` or `"Member"` wherever you compare `__typename`.

  A few fields are also deprecated in favor of member-named replacements: `Reaction.user` → `Reaction.member`, `Message.user` → `Message.member`, `AutomationRuleLogEntry.user` and `userId` → `member` and `memberId`, and `FeedPostItem.introducedUser` → `introducedMember`. The deprecated names keep working until the removal.
</Update>

<Update label="August 26, 2026">
  **`BROKEN` on `StripeAccountStatus`**

  `Network.stripeAccount.status` returns `BROKEN` when a connection attempt to Stripe failed. The account is still attached to the Network, `lastError` carries the reason Stripe gave, and a Host can reconnect it. If your code switches exhaustively on `status`, add a branch for it.

  `DISABLED` has narrowed to mean only a Stripe account that has been fully disconnected and is no longer attached to the Network. If you treat `DISABLED` as "payments are not working", split that into the two cases: a `BROKEN` account is one a Host can reconnect, and a `DISABLED` one is gone.
</Update>

<Update label="August 25, 2026">
  **Member counts on plans and spaces**

  `PaymentPlan.memberCount` returns how many members hold a plan, and `Space.memberCount` returns how many belong to a space. Counting either used to mean paging the whole member list to the end, which cost a large Network hundreds of requests for one number. Both fields return the total in a single request.

  A plan's count covers everyone with access through it, whether they subscribed or bought it outright. Both fields return `null` unless your token acts as a Network Host or Moderator.

  **Badge and tag grants report what they changed**

  `createBadgeMemberships`, `createTagMemberships`, `deleteBadgeMemberships`, and `deleteTagMemberships` now return an `outcome` alongside the badge or tag. It is a `GrantOutcome` — `GRANTED`, `ALREADY_GRANTED`, `REVOKED`, `NOT_GRANTED`, or `SKIPPED_NO_MEMBERSHIP` — and answers only whether the call changed anything. Which members changed is answered separately by `grantedMembers` on the two create mutations and `revokedMembers` on the two delete mutations, so a batch where some members already held the badge is no longer indistinguishable from one where every member did.

  The two create mutations also stop granting to a member who holds no membership in your Network, the state a ban, a removal, or an abandoned signup leaves behind. That member is dropped from the batch, and a call naming only such members grants nobody and returns `SKIPPED_NO_MEMBERSHIP`. Both cases previously reported plain success, so if you grant from a member list you maintain yourself, read `grantedMembers` rather than assuming every id you sent was granted. A [limited member](/for-hosts/payments-and-access/what-is-a-full-vs-limited-membership) is unaffected, and you can still grant them a badge or tag.

  **`createEvent` takes either form of space ID**

  `createEvent`'s `spaceId` now accepts a space's numeric resource ID as well as its GlobalID, so it matches `createPost`, which already took both. It previously accepted only the GlobalID.

  Nothing changes for calls you already make — a GlobalID is still accepted, and the argument is unchanged in every other way.
</Update>

<Update label="August 24, 2026">
  **Nested connections page 25 at a time, and queries above a complexity of 1,500 are rejected**

  Connections on the query root and on `Network`, such as `network { posts }` and `network { members }`, still page up to 50. Every connection you reach through another connection's page now caps a page at 25, including `Post.comments`, `Comment.replies`, `Event.rsvps`, `User.followers`, `User.spaces`, and `PublicConversation.messages` / `PrivateConversation.messages`. A `first:` or `last:` above the cap is clamped to it rather than rejected, and `pageInfo` and the cursors keep working, so anything already paging by cursor is unaffected.

  A nested connection's page size multiplies both the rows one request materializes and the complexity you are billed for. A page of 50 posts with 50 comments each came to 2,500 rows, and the same page now comes to 1,250.

  Queries are also rejected above a complexity of 1,500. Before a query runs, we estimate its cost from the most it could return: 1 for each row on every connection page (the `first:` or `last:` you supply, clamped to the page cap, or the page cap when you supply neither), 1 for each object field it reaches, and nothing for scalars. That is the same model `extensions.cost.complexity` bills you on after execution. A query estimated above the ceiling is rejected before execution and comes back with a top-level error and no `data`, and the request is billed the 1-unit minimum: `{"errors": [{"message": "Query has complexity of 2703, which exceeds max complexity of 1500"}]}`.

  For example, an inbox list of 50 direct messages selecting `id`, `title`, `lastChatAt`, and `unreadCount` estimates at about 103 and runs. Those same 50 conversations each selecting `messages(first: 25) { nodes { id user { id name } } }` estimate at about 2,700 and are rejected. Fetch `messages` one conversation at a time instead, and only for the conversations whose `lastChatAt` has changed.

  On a successful response, `extensions.cost.complexity` is what your query actually cost, so it is the number to watch as you tune a query toward the ceiling. Both limits apply only to the Mighty API, and both are documented under [Query cost limits](/api#query-cost-limits).
</Update>

<Update label="August 24, 2026">
  **Simpler upload strategy and provider names**

  The `UploadStrategy` and `UploadProvider` values now describe the wire protocol instead of the storage implementation behind it. Use `MULTIPART_SERVER_MEDIATED`, `MULTIPART_PRESIGNED`, `SINGLE_PUT`, and `LEGACY_MULTIPART_FORM` in place of `S3_MULTIPART_SERVER_MEDIATED`, `S3_MULTIPART_PRESIGNED`, `S3_SINGLE_PUT`, and `KALTURA_MULTIPART_FORM`. On `UploadProvider`, `LEGACY_EXTERNAL` replaces `KALTURA`.

  Responses now return the new names. The old names are deprecated but still accepted as input, so a `createUploadSession` request that sends `preferredStrategy: S3_MULTIPART_PRESIGNED` keeps working. Update any code that compares against the returned `strategy` or `provider`, and switch to sending the new names. The deprecated names will be removed in a later release, and that removal will be announced here first.
</Update>

<Update label="August 21, 2026">
  **`location` on events is deprecated — use `venue`**

  The free-text place name on events is now called `venue`: a new field on `Event` and a new optional argument on `createEvent` and `updateEvent`. The old name was ambiguous — the value holds a venue or place name ("Blue Bottle Coffee"), not an address — and real address fields now sit beside it (see this month's additions below).

  `location` is deprecated on `Event` and on both mutations, but keeps working: both names read and write the same value, so nothing breaks today. Update your queries and mutations to use `venue`. A mutation that supplies both `venue` and `location` is rejected, so migrate each call site in one step. The removal of `location` will be announced on this page before it happens.
</Update>

<Update label="August 14, 2026">
  **Direct messages, with member consent**

  The schema now exposes a member's private chats: their direct message conversations (including group conversations) and the reply threads they take part in, including threads started from a space chat. Read them from `me { directMessages }`, and manage them with the conversation and message mutations — `createConversation`, `createDirectMessage`, `deleteDirectMessage`, `markDirectMessageRead`, `markDirectMessageUnread`, `markAllConversationsRead`, `createDirectMessageReaction`, `deleteDirectMessageReaction`, `updateConversation`, `archiveConversation`, `leaveConversation`, `createConversationMemberships`, and `deleteConversationMemberships`.

  Access is gated on two new member scopes: `read:chats` to read, and `write:chats` to send and manage (which also grants read). Both are consent-required — the consent screen is always shown when they're requested, even on applications configured to skip it. A token acts as the member who approved it and reaches only that member's own conversations; hosts gain no access to other members' private chats. A request made without the required scope fails with a `FORBIDDEN` error naming the scope it needs. See [OAuth Applications — Scopes](/oauth-applications#scopes).
</Update>

<Update label="August 2026">
  **General availability**

  The [Mighty API](/api) is generally available on the Scale plan and above. OAuth scopes are now enforced on every token, so a token's access is the user's product permissions intersected with the scopes they approved — request the narrowest set your app needs.

  Breaking changes — removals, renames, and arguments that become required — are deprecated and announced on this page before they take effect, so check it before you upgrade an integration.

  **Scope requirements in the reference**

  The [GraphQL reference](/api) now names the OAuth scope each query, mutation, field, and type requires, so you can pick the scopes your app needs before you write the request rather than discovering them from a `FORBIDDEN` response.

  Where more than one scope reaches the same data, the reference lists every scope that works and any one of them is enough. A `write:` scope also covers what its matching `read:` scope covers, so both appear wherever that applies.

  **Documentation launch**

  The Mighty API documentation is live. Its GraphQL reference is generated from the schema we currently serve, so it always shows what is deployed today but not what changed or why. This changelog is where you find that. It starts here, so it does not cover changes made before this entry.

  From now on, every change to the published schema is recorded on this page:

  * **New features and changes to what already exists** get their own dated entry — new queries, mutations, fields, and enum values, along with renames, removals, deprecations, arguments that become required, and changes to permissions, pagination, or error codes. Everything that ships on a given day appears in a single entry.
  * **Bug fixes and documentation clarifications** are collected into one entry per month, so you can review a month of them together.

  **Additions this month**

  * `createEvent` and `updateEvent` accept optional `thumbnailId` and `headerId` arguments to set an event's card thumbnail and header image. Upload the image through the asset upload endpoint first, then pass the returned asset id. On `updateEvent`, an explicit `null` clears an image.
  * The asset upload endpoint (`POST /networks/:network_id/assets`) accepts two new asset styles for event images: `square_thumbnail` and `cinema_header`.
  * `createEvent` and `updateEvent` accept optional `street` and `city` arguments on in-person (`local`) events, and the address is geocoded to map coordinates automatically. Supplying an address on any other event type returns a validation error; on `updateEvent`, an explicit `null` clears a field.
  * `Post.venue` and `Poll.venue` return the free-text venue or place name saved on a post, such as "Blue Bottle Coffee", or null when none is set. `Event.venue` already returned this value, so the place name now reads the same way on all three post content types.
  * `createLiveSpace` starts a livestream and returns it with the `url` to open to join the room as its creator. Pass `spaceId` to start it in a space, or omit it to start it on the Network. Recording is off unless you pass `record: true`, and `notifyMembers: true` tells the space's members it has started. A livestream draws down your Network's streaming hours until it ends.
  * `completeLiveSpace` ends a livestream and stops it consuming those hours. One that never went live is aborted, and one that is broadcasting is stopped, disconnecting its attendees. Ending a livestream that has already ended succeeds and changes nothing.

  **Documentation clarifications**

  * `createEvent` / `updateEvent` — the `city` argument is free text, so you can append a state or province and a postal code after the city name (for example `London, Ontario, N5V 3N8`). Including them improves the accuracy of the coordinates geocoding derives from `street` and `city`.

  **Automation rule targets can be a host**

  `AutomationRuleTarget` now includes `User`, so a rule whose target is a host resolves like any other.

  A rule can fire when a host receives a direct message, and that rule's target is the host. Because the union did not list `User`, a request selecting `triggerTargetable` on one of those rules failed rather than returning the target, and an action's `targetable` behaved the same way.

  Select the host's fields with a `... on User` fragment. Without it you read only `__typename` from the target, the same as for any other type in the union you do not spread.
</Update>
