Skip to main content
Posts whose type a Space doesn’t allow are now hidden from Network Host and moderator tokens tooThis is phase 2 of the change announced on October 15, 2026. Leaving out includeDisallowedPostTypes now hides posts whose type their Space doesn’t allow from every token, including one acting for a Network Host or Network moderator. A list leaves them out, a read by ID returns a POST_TYPE_NOT_ALLOWED error, and a link to such a post returns null. The comments on them follow the same rule. We’re treating this as a bug fix: the API now hides what the apps already hide. It takes effect two weeks after that announcement, rather than after the three-month notice period in the deprecation policy.To keep getting these posts, pass includeDisallowedPostTypes: true with a token that acts for a Network Host or Network moderator. node, nodes, links to a post and comment results from network.search take no argument, so they never return such posts.The same rule now covers six operations that take no includeDisallowedPostTypes argument: createPollAnswer, updatePoll, createAnswerCheer, deleteAnswerCheer, updateVideoProgress and Member.videoProgresses. No token can reach such a post through them.
  • In a Space with the Feed switched off, createPollAnswer, updatePoll, createAnswerCheer and deleteAnswerCheer refuse a poll or question with POST_TYPE_NOT_ALLOWED, and updateVideoProgress and Member.videoProgresses refuse a post or article with NOT_FOUND.
  • For a poll or question in a Space with Polls switched off, createPollAnswer, updatePoll, createAnswerCheer and deleteAnswerCheer now return POST_TYPE_NOT_ALLOWED instead of NOT_FOUND. Its extensions.postTypeDisallowedBy names the feature that is off.
  • updateVideoProgress and Member.videoProgresses now accept a post in a Space with Articles switched off when the Feed and Posts are on.
Limited Members no longer see chats with ordinary membersWhen the member your token acts for is a Limited Member, with access to a Plan’s Spaces but no Network membership, me.directMessages now leaves out any chat whose other participants aren’t all Network Hosts or Moderators. Threads under such a chat are left out too, and node and nodes return null for its DirectMessage ID. No field is added or removed, and the response shape is unchanged.Every mutation that takes the ID of such a chat, or of one of its messages, now returns NOT_FOUND. That covers createDirectMessage, deleteDirectMessage, createDirectMessageReaction, deleteDirectMessageReaction, markDirectMessageRead, markDirectMessageUnread, updateConversation, archiveConversation, leaveConversation, createConversationMemberships, deleteConversationMemberships, createConversationBookmark and deleteConversationBookmark. createConversation returns FORBIDDEN when its recipients would make up such a chat.
  • Chats with only Network Hosts and Moderators stay visible, including a group chat that holds one Limited Member alongside Hosts or Moderators.
  • Nothing is deleted. If the member becomes a full Network member again, the chats come back with their full history.
  • The other participants in such a chat keep seeing it, unless they’re Limited Members too.
We’re treating this as a security fix. A Limited Member may only share a chat with Network Hosts and Moderators, but that rule was checked only when someone joined a chat, so a member whose Network access was turned off kept messaging ordinary members in the chats they already had. It takes effect now, rather than after the three-month notice period in the deprecation policy.If your integration lists a Limited Member’s chats, expect some to drop out of the list. Treat a null node result or a NOT_FOUND or FORBIDDEN error on one of them as that chat no longer being open to the member.
Upload STEP, shape, RAR, and log filesThe REST upload endpoint and createUploadSession now accept STEP files (.stp, .step), shape files (.shp), RAR archives (.rar), and log files (.log). A .pages file declared as application/vnd.apple.pages is accepted too. When you leave the content type out or send application/octet-stream, the server now picks the type from the filename’s extension on both paths. A content type sent as a comma-separated list is refused. See Limits and validation for the full allowlist.
Resume media playback where a member left offYou can now save and read a member’s place in a media asset, so your player can pick up where they stopped. Call the new updateMediaProgress mutation with the postId the member is playing from, the externalAssetId of the asset, and positionSeconds and durationSeconds in whole seconds. The postId only checks that the member can open the post; the position belongs to the asset, not to the post. When the track ends, send positionSeconds equal to durationSeconds; your player should then start the next play from the beginning.Read positions back from the new mediaProgresses connection on Member, optionally narrowed with externalAssetIds. Each MediaProgress has an externalAssetId, a positionSeconds and a durationSeconds. A member only ever sees their own positions; asking on behalf of another member returns an empty list.This sits alongside updateVideoProgress and Member.videoProgresses, which are unchanged. Those record a percentage watched and a position for a video within a post, keyed by the post and a videoId. A native video’s primaryMedia.externalAssetId works both as that videoId and as an externalAssetId here, but the two record separately, so a position saved through one isn’t returned by the other.
Order lists with orderBy; the older sort arguments are deprecatedEvery list on Network that took sort, sortBy or sortOrder now takes orderBy instead: members, paymentCoupons, paymentPlans, paymentSubscriptions, purchases, invites, leads, automationRules, marketingAudiences, marketingBanners, badges, and tags. Pass one ordering, as in purchases(orderBy: { createdAt: DESC }), or, on every list but members, leads, purchases and paymentSubscriptions, several in priority order, as in paymentCoupons(orderBy: [{ status: ASC }, { createdAt: DESC }]). Each list has its own input type, such as MembersSortInput, naming the fields it can be ordered by. Leaving orderBy out keeps each list’s current default order.The arguments these lists took before, sort with sortOrder (sortBy with sortOrder on badges and tags, and sortOrder alone on leads), still work and order by the same fields, but they are deprecated and will be removed on or after February 1, 2027. A request that sends orderBy together with an older argument that changes the order returns a BAD_USER_INPUT error. sortOrder sent without sort never changed the order of paymentCoupons, marketingAudiences or marketingBanners, and is still ignored there. To move over, replace sort: MEMBER_NAME, sortOrder: ASC with orderBy: { memberName: ASC }, and so on for each list. On leads, replace sortOrder: ASC with orderBy: { createdAt: ASC }. Two values have no one-to-one match. On members, drop sort: REFERRAL_COUNT: the Ambassador roster (ambassador: true) always orders by referral count and takes no orderBy. On paymentCoupons, sort: STATUS ignored sortOrder, so replace it with orderBy: [{ status: ASC }, { createdAt: DESC }].members and leads order by one field at a time, so they accept one orderBy entry. purchases and paymentSubscriptions can only be ordered by createdAt, so they accept one entry too. A members cursor works across the old and new arguments for the same ordering, so you can switch in the middle of paging. For a full roster export, order members by resourceId.
markAllConversationsRead can mark conversations in the backgroundWhen a member has only a few unread conversations, markAllConversationsRead still marks them all before it returns. When they have many, it now queues the marking and returns right away, so success: true means the request was accepted, not that every conversation is already read. A query for conversations or unread counts made right after the mutation can then return the earlier state. The marking usually finishes within seconds, but for a member with thousands of unread conversations it can take a few minutes.The response is the same either way, so don’t treat success: true as completion. If you show read state or unread counts after calling the mutation, query them again once the marking has had time to finish, rather than reading them straight away.Calling the mutation again while a queued run is in progress is safe, though you don’t need to. It marks anything that arrived in the meantime, and never undoes what the first run marked.We’re treating this as a change that protects the platform’s reliability: marking every conversation within the request timed out for members with many unread conversations and held the request open for up to 29 seconds. It takes effect now, rather than after the three-month notice period in the deprecation policy.
Change one Discovery section at a timeThree new mutations let you change a Discovery page’s sections one section at a time, alongside updateGalleryGroup and reorderGalleryGroup:
  • createDefaultGalleryGroups saves a page’s default sections so each gets an ID. Until a page is customized, its default sections come back from galleryGroups with a null id and can’t be targeted. Call it first on such a page. It leaves a page whose sections are already saved as it is, so it’s safe to call again. Omit spaceId for the Network’s own Discovery page.
  • createGalleryGroup adds a custom Top Spaces or Spaces Near You section for a Spaces Collection to the bottom of the Network’s Discovery page. The section starts hidden; show it with updateGalleryGroup.
  • deleteGalleryGroup deletes a custom section. Other sections can only be hidden.
Avoid overwriting someone else’s change with lastEditedAtGalleryGroup.lastEditedAt returns when a section was last shown, hidden or created. Pass the value you read as expectedLastEditedAt to updateGalleryGroup or deleteGalleryGroup, and the write is refused with a CONFLICT error when someone changed the section since. The error’s conflict extension gives when the section changed and, when it’s known, who changed it. lastEditedBy is null for a change made with updateGalleryGroups. The argument is optional: leave it out to write whatever the section holds.
updateGalleryGroup keeps a Discovery page to 8 shown sectionsShowing a section on a Discovery page that already shows 8 now returns a message in the payload’s errors instead of saving, the same limit updateGalleryGroups already applied.updateGalleryGroups is deprecatedupdateGalleryGroups replaces every section of a page in one write, so it overwrites changes made in between your read and your write. It keeps working until at least January 31, 2027. Use the mutations above instead. They work on a Space’s Discovery page too: pass the Space’s spaceId to createDefaultGalleryGroups, then show, hide and move its sections with updateGalleryGroup and reorderGalleryGroup. Custom sections stay on the Network’s own Discovery page, as they did with updateGalleryGroups.Discovery section writes can return CONFLICT while another change is in progressupdateGalleryGroups, updateGalleryGroup, createGalleryGroup, deleteGalleryGroup and createDefaultGalleryGroups now take turns with other Discovery section writes and Space reordering in the same Network. When another of those changes holds the Network for too long, the write is refused with a CONFLICT error and nothing is saved. Retry it. updateGalleryGroups also saves all of its sections or none of them.We’re treating the 8-section limit on updateGalleryGroup, these CONFLICT errors and updateGalleryGroups saving all or none as fixes that protect your Discovery pages. updateGalleryGroup let a page show more sections than the limit updateGalleryGroups already enforced, and two writes at once could save a second set of default sections. They take effect now, rather than after the three-month notice period in the deprecation policy.Avoid overwriting someone else’s Promotional Banner edit with lastEditedAtMarketingBanner.lastEditedAt returns when a Banner’s content last changed. Turning a Banner on or off doesn’t move it. Pass the value you read as expectedLastEditedAt to updateMarketingBanner, and the update is refused with a CONFLICT error when the Banner’s content changed since. The error’s conflict extension names who made the change and when. The argument is optional: leave it out to update whatever the Banner holds. lastEditedAt is returned to Hosts only.
Turn uploaded audio and video into Course Lessons with PENDING_LESSON_MEDIAUploadAssetStyle has a new PENDING_LESSON_MEDIA value. Pass it as the assetStyle of createUploadSession, with an assetKind of AUDIO or NATIVE_VIDEO and a Course’s spaceId, and once you call completeUploadSession, the file becomes a new Lesson at the end of the Course, leading with that media. Upload one file per Lesson you want to create.The credential must be allowed to edit the Course’s content, and the Course must be able to take another Lesson. For NATIVE_VIDEO, the credential must also belong to a Host or Moderator on a Network with native video, or createUploadSession returns a FORBIDDEN error. A spaceId your credential can’t view returns a NOT_FOUND error. For the Lesson-specific refusals, createUploadSession returns a validation error in errors instead of a session: when the Space isn’t a Course you can edit, when the Network can’t use that kind of media to lead a Lesson, when the Course is still generating its outline, or when it has reached its maximum number of Lessons and Sections.The Lesson is created in the background shortly after completeUploadSession, so it can take a moment to appear, and its media may still be processing when it does. It can still fail to appear if the Course fills up, the credential loses edit access, or the media can no longer lead a Lesson by then. In that case the upload is discarded, and nothing reports the outcome, so check the Course’s Lessons and stop waiting after a reasonable time.
Receive a webhook when a member returns to your NetworkYou can subscribe to MEMBER_RETURNED through createWebhookCallback or updateWebhookCallback. The delivered event type is MemberReturnedHook, and its payload includes the member, network_id, previous_last_visited_at, and returned_at timestamps.The event fires when a member visits your Network after being away for at least 7 days. A member’s first visit doesn’t trigger it. To apply a longer threshold, for example to remove an “Inactive” tag only from members who were away for 45 days, compare previous_last_visited_at with returned_at before you act.To find members who haven’t returned, read Member.lastActiveAt on a schedule.network.poll and network.polls return polls to every member who can read themnetwork.poll and network.polls now return a poll or question to the member your token acts for whenever that member can read it. Before, network.poll returned a FORBIDDEN error and network.polls an empty list unless your token acted for a Network Host. A token acting for a Network moderator was refused too.
  • network.poll returns a NOT_FOUND error for a poll the member can’t read. A poll held for review after members report it stays hidden from members other than its author and the members who can review it, as it does on network.post.
  • network.polls returns a null node for each poll in a Space the member can’t read, as network.posts does, so skip null nodes as you page. It leaves out a poll held for review, except for its author and the members who can review it. Passing spaceId for a Space whose posts the member can’t read returns a NOT_FOUND error.
  • network.polls now leaves out polls in an archived Space or a Space being deleted, as network.posts does. This applies to every token, including one acting for a Network Host.
We’re treating this as a bug fix. These two fields refused readers that network.posts and node(id:) already served the same polls to, and network.polls now leaves out the same Spaces network.posts already leaves out. It takes effect now, rather than after the three-month notice period in the deprecation policy.If your integration reads FORBIDDEN from network.poll as “this token can’t see polls,” check for NOT_FOUND instead.
Read a video’s own shape from PrimaryMedia.aspectRatio and Asset.aspectRatioThe new PrimaryMedia.aspectRatio field returns the width divided by the height of a post’s header video as it plays, for example 0.5625 for a 9:16 portrait clip or about 1.78 for 16:9. It’s read from the video itself, not from its cover image, so it stays right when a Host picks a cover of a different shape. Rotation is already applied, so a portrait recording from a phone reads as portrait.Asset.aspectRatio now returns the same value for a native video. Before, a video returned only an aspect ratio supplied when it was uploaded, and null when none was. When a video has both, the video’s own frame takes precedence. Images return the same value as before.PrimaryMedia.aspectRatio is null for audio, and while the video’s shape is unknown, for example until it finishes processing, or for some older videos. In those cases Asset.aspectRatio returns the aspect ratio supplied at upload, or null when there isn’t one. When either field is null, size your player from the cover image’s shape instead.
Message pages keep their place while a conversation changesThe messages connection on Space, DirectMessage, and ConversationThread now hands out cursors that name a message rather than a position in the list. Paging on with after continues from that message even when new messages arrive or older ones are deleted between requests. Before, a page read after new messages arrived repeated messages you already had, and a page read after a deletion skipped one. A cursor keeps working after the message it names is deleted: the next page starts at the nearest remaining message on that side.last with before returns the messages immediately newer than a cursor, still newest first. To catch up on what arrived after the newest message you hold, pass its cursor as before, then keep passing each response’s startCursor as the next request’s before while hasPreviousPage is true.Cursors you already hold keep working. A cursor issued before this change returns the same page it did before, and the cursors in that response name messages, so your next request pages by message with no change on your side.Open a conversation at a message with aroundMessageIdThe messages connection takes a new optional aroundMessageId argument, which accepts a resource ID or GlobalID. It returns a page centered on that message: about half the page newer than it, the message itself, then older ones. Near either end of a conversation, the other side makes up the shortfall, so the page stays full whenever the conversation has enough messages. Use it to open a conversation at a message from a notification or search result, then page outward with the page’s cursors.The message must be one the page can show: a system message needs includeSystemMessages: true, otherwise you get Message not found. aroundMessageId takes first only; combining it with after, before, or last returns an error.Read id through the Message interfaceThe Message interface now declares id, so a query that selects messages through Message reads their IDs without a ... on ChatMessage or ... on PrivateMessage fragment.Filter me.conversations and me.directMessages by unread or bookmarkedme.conversations and me.directMessages take a new optional filter argument. Pass { unread: { eq: true } } for conversations with messages the member your token acts for hasn’t read, or { bookmarked: { eq: true } } for the ones they’ve bookmarked. Pass both to list only conversations that match both. Omitting filter lists every conversation as before.
Posts whose type a Space doesn’t allow: a new argument, and a new default in two phasesWhen a Host switches off a content type in a Space, such as Events, Polls, Articles or the Feed, the app’s feed and search hide the posts of that type. A content type the Network’s plan doesn’t include counts as switched off. The API now hides these posts too, and the comments on them, in two phases.These fields take a new optional includeDisallowedPostTypes argument: network.posts, Space.posts, Course.posts, Course.coursework, network.savedPosts, network.pins, Space.pins, network.polls, network.post, network.coursework, network.poll, network.comment, network.comments(postId:), network.reactions(targetId:), network.automationRule and network.automationRules. On an automation rule, it decides whether a trigger or action target that is such a post comes back.
  • true returns such posts, and the comments on them, when your token acts for a Network Host or Network moderator. For any other token it’s ignored.
  • false hides them from every token. A list leaves them out, a read by ID returns a POST_TYPE_NOT_ALLOWED error whose extensions.postTypeDisallowedBy names the feature that is off (for example { "key": "events", "label": "Events" }), and a link to such a post returns null.
  • Leaving it out keeps returning such posts to a token acting for a Network Host or Network moderator, as before, until phase 2. Every other token gets the new behavior now, including one acting for a Host or moderator of a single Space.
Fields that take no argument follow the same rule as leaving it out: node, nodes, any field that links to such a post, and comment results from network.search.Phase 2, on October 30, 2026. Leaving the argument out hides these posts from Network Host and Network moderator tokens too. To keep getting them, pass includeDisallowedPostTypes: true before then. To see the new default now, pass false.
Custom Field reads no longer return Tags and BadgesThe Custom Field reads now return only the Custom Fields a Host creates:
  • Network.customFields leaves out your Network’s Tag and Badge fields. Filtering it by responseType: TAG or responseType: BADGE returns an empty list.
  • Network.customField(id:) returns the same not-found result for a Tag or Badge field’s ID as for a missing field.
  • Member.customFieldResponses leaves out the Tags and Badges a member holds.
We’re treating this as a bug fix. Tags and Badges already have their own fields, and returning them as Custom Fields duplicated that data and made them look like fields a Host created. It takes effect now, rather than after the three-month notice period in the deprecation policy.If you read a member’s Tags or Badges through customFieldResponses, read Member.tags and Member.badges instead. They return the same data:
To list your Network’s Tags and Badges, use Network.tags and Network.badges. To assign or remove them, use createTagMemberships, createBadgeMemberships, deleteTagMemberships, and deleteBadgeMemberships.createCustomField accepts only PUBLIC or PRIVATE privacyPassing ADMIN returns a message in the payload’s errors list and creates nothing. To create a field only Hosts, moderators, and the responding member can see, pass privacy: PRIVATE with responseBy: INDIVIDUAL_MEMBER.Tag.customField and Badge.customField are deprecatedEvery Tag returns the same customField, and so does every Badge, so these fields tell you nothing about the Tag or Badge you’re reading. Their IDs also no longer resolve through Network.customField(id:). Read the Tag’s or Badge’s own fields instead.Tag.customField and Badge.customField keep working until January 15, 2027, when we remove them. Before then, remove them from your queries.New custom field types: Number, Yes/No, Date, Date Range, Recurring Date, URL, Phone Number and LocationCustomFieldResponseType has nine new values: NUMBER, BOOLEAN, DATE, DATE_RANGE, RECURRING_DATE, URL, PHONE_NUMBER, LOCATION and MULTI_LOCATION. A query that switches on the response type should handle them. On MemberCustomFieldResponse, only a LOCATION response has a text value (the place’s label), and a MULTI_LOCATION response keeps its labels in locations, so falling back to text shows the others as unanswered.Read an answer of these types from the new fields on CustomFieldAnswer and MemberCustomFieldResponse: number, booleanValue, date, startDate/endDate (date range), month and day (recurring date), url, phoneNumber, and latitude/longitude. A MULTI_LOCATION response lists each place in MemberCustomFieldResponse.locations, typed CustomFieldAnswerLocation. Write the same values with the matching arguments on UpdateCustomFieldAnswerInput, using CustomFieldAnswerLocationInput for locations. A date range takes both startDate and endDate, or null for both to clear it. A recurring date takes month and day together. A location takes the place’s label as text, together with latitude and longitude.CustomField now describes each field’s limits: minValue/maxValue for a number field, minDate/maxDate for a date or date range field, locationGranularity for a location field, and collectsContactDetails, true for a phone number or an Address-level location. Treat a field where collectsContactDetails is true as personal contact information.CustomFieldFilter can now match these answers: numberRange (an IntRangeInput), dateRange (a DateRangeInput), booleanValue, months, and for location fields location (a CustomFieldLocationFilter center and radius) or labels.
Read and set a Network’s default profile image packNetwork has a new avatarPackStyle field, and updateNetwork accepts a new optional avatarPackStyle argument. Both use the new NetworkAvatarPackStyle enum. The pack is what members who haven’t uploaded their own profile picture are randomly assigned one from, the same setting as Default Profile Images in the Network’s Imagery settings.
WEEKLY removed from NotificationEmailFrequencyThe NotificationEmailFrequency enum no longer has a WEEKLY value. No weekly email was ever sent, so a member set to WEEKLY received no notification emails at all. Members who had it are now set to DAILY, and emailFrequency returns only ALL, DAILY, or NONE.Sending emailFrequency: WEEKLY to updateNotificationSetting now fails validation. Use DAILY for a digest.
Refetch more objects by IDYou can now select id: ID! on AbuseReport, BillingPlan, DailyVisitStreak, OnboardingPointer, PaymentPlanPrice, and SpaceTemplate, and refetch these objects through node(id:) or nodes(ids:). These types now implement Node, so Relay can share their cached fields across queries and generate refetchable fragments. Treat the new IDs as opaque values.EventInstance also implements Node and can be refetched using its existing occurrence ID. Its ID format and all existing resourceId, uuid, and canonicalName fields are unchanged.Refetching respects access permissions and the current Network. Abuse reports and onboarding pointers require a Network Host or Moderator. Hidden pricing options are available to the Network Host or their buyer, and published options are available to members. Daily streaks require the streak feature and an active streak; Space templates must be offered to the Network. Pricing refetches require read:network, host:read:network, or host:read:network_plans consent. Event occurrences retain the event’s access and OAuth consent requirements. A deleted object or one you cannot access returns null.
Sign in hosts and members in one authorization flow/oauth/authorize accepts a new optional require_host parameter. Send require_host=false to request member and host scopes together: a host is granted everything requested, and a member or moderator signs in and is granted the member scopes only, where before they were blocked at the consent screen. Read the scope field of the token response to see which set the user received. You no longer need a second authorization flow to upgrade hosts to host scopes.A request made only of host scopes is still blocked for members. require_host=true requires a host even when you request no host scope, so a host-only application can turn members away up front. Requests that omit the parameter behave exactly as before. See Host scopes and sign-in.
Create API keys for your own scriptsAs a Host, you can now create long-lived API keys under Network Admin > Integrations > Mighty API, for calling the Mighty API from your own scripts and server-side jobs without registering an OAuth application. A key acts as you, carries only the host scopes you choose, and never expires. Send it as a Bearer token exactly like an OAuth access token.The key value is shown once, when you create it, and can’t be retrieved again. Revoke a key from the same API keys card to end its access immediately. See API Keys.
Play native video and audio from your own playerPrimaryMedia.url now returns an HLS manifest URL your player can load directly, from any origin and with no member session, including on a private Network. The URL carries its own short-lived credential, so send it no Authorization header or other custom request headers. The variant playlists, captions, and media segments it points to load the same way.The new PrimaryMedia.urlExpiresAt field says when that URL stops working. Refetch the post before then rather than reusing a URL you stored.Native video and audio attached in a post’s body get the same playable URL from two new fields on Asset: playlistUrl and playlistUrlExpiresAt. Asset.url is unchanged and does not play; use playlistUrl.PrimaryMedia.url and Asset.playlistUrl are null while an upload is still processing, so show a placeholder until a refetch returns a URL. Assets your token cannot see stay hidden, as before.
Fixes this month
  • updateSpace now returns an error when featureToggles turns autojoin_space on for a Space whose joinable setting is false. It previously returned the Space with no error while leaving auto-join off, so the call looked successful but changed nothing. space is now null and errors says the Space must be joinable first. None of the request’s featureToggles are applied, though a title or description in the same call is still saved. The Mighty API cannot change a Space’s joinable setting yet, so turn on Allow Anyone to Join in the Space’s privacy settings in the web app, then retry. Sending autojoin_space as true for a Space that already has auto-join on is accepted as before.
  • createGift now rejects a gift whose recipientId is the calling member, as its reference description (“to another member”) says. The payload returns a null giftTransaction and You cannot send a gift to yourself in errors, and no points are spent. It previously created the gift and spent the caller’s own points on it.
  • createPost and updatePost now mark each image in assetIds as one of the post’s own images, the same as uploading it with metadata[is_main_image]=true, so you no longer need to declare is_main_image when uploading an image to attach to a post. A post with a single image now shows that image on the post page as well as on its feed card. Previously the post page skipped it. These images are not marked:
    • Images that the post’s body already displays inline.
    • Images attached to an article.
    • Files other than images.
    A post (postType of post) whose only attachment is one image is now stored as an image post, so its contentKind is image_file instead of text. Any other post stays text, and a link post stays link. updatePost with assetIds switches the kind in either direction, including assetIds: [], which removes every attachment.
Documentation clarifications
  • The reference descriptions now say Profile Fields where they said Custom Fields, matching the Mighty web and mobile apps. Only the description text changed: type, field, argument, and enum names are the same, so existing queries keep working unchanged.
  • createPaymentPlan — the spaceIds argument’s description in the reference no longer says the Cohost Space cannot be included. Networks no longer have a Cohost Space, so there was nothing for that rule to exclude. The argument still needs at least one active Space, and its behavior is unchanged.
Content ID arguments accept resource IDsThe ID arguments on post, comment, event, RSVP, and reaction operations now take a numeric resource ID as well as a GlobalID, so you can pass the IDs you already hold from the Admin API or from a resourceId field without looking up each record’s GlobalID first. This covers:
  • postId and replyToId on createComment
  • id on updateComment, updatePost, updateEvent, updateRsvp, and deleteRsvp
  • eventId on createRsvp
  • spaceId on network.posts, network.savedPosts, network.events, and network.members
  • postId on network.comments
A GlobalID behaves exactly as before. An ID for a record in another Network, one you cannot see, or a GlobalID for the wrong kind of record returns the same not-found result a missing record does. deleteRsvp.deletedId returns the RSVP’s GlobalID whichever form you pass.A resource ID on its own cannot say whether it names a post or a comment, so createReaction, deleteReaction, and network.reactions take a new optional targetType argument, POST or COMMENT. Pass it whenever targetId is a resource ID; leaving it out returns a BAD_USER_INPUT error. A GlobalID targetId needs no targetType, and any you pass is ignored.
completionCriteria returns BUTTON when a lesson’s primary media is goneCoursework.completionCriteria now returns BUTTON instead of PRIMARY_MEDIA for a lesson whose completion rule is to finish its primary media, but whose primary media slot no longer holds native audio or video (for example, a Host removed it). Members can no longer finish that media, so the lesson is completed with the completion button instead. Before this, the field kept returning PRIMARY_MEDIA and there was nothing for the member to finish.Lessons with no completion rule still return NONE, and lessons that return VIDEO are unchanged. The CourseworkCompletionCriteria enum has no new values. If you switch on completionCriteria to decide what to show, handle BUTTON for these lessons.
Messages you send keep the spacing between paragraphsWhen the text you pass to createMessage, createDirectMessage, or createConversation holds more than one paragraph (<p>First</p><p>Second</p>), the message is now stored with a blank line between them (First<br><br>Second<br>), so it reads the way you wrote it. Before this, unless you set preserveParagraphSpacing: true on createConversation, each paragraph became a single line break and the paragraphs ran together. Empty paragraphs between two paragraphs — including one holding only a <br> — now collapse into that one blank line, and a line break inside a paragraph stays a single break. A message that opens with an empty paragraph (<p></p><p>Hello</p>) is stored with two line breaks before your text (<br><br>Hello<br>). Messages sent before this change are not rewritten.Because every message now keeps its paragraph spacing, preserveParagraphSpacing on createConversation only changes how bare newlines in text are handled: set it to true to turn each newline into a line break. Callers already passing preserveParagraphSpacing: true now also get runs of empty paragraphs collapsed to one blank line. It still defaults to false.createSpaceMemberships can send each Space’s welcome emailcreateSpaceMemberships takes a new optional argument, sendWelcomeEmail. Set it to true to send the member each newly joined Space’s Welcome-to-Space email, the way the Admin API’s POST /admin/v1/networks/:network_id/spaces/:space_id/members does. A Space the member already belongs to sends nothing, and a Space sold in a plan withholds its welcome, as the Admin API does. For each Space whose welcome is sent, the member does not also get the notification saying a Host added them; a Space that withholds its welcome still sends that notification. sendWelcomeEmail defaults to false, so your existing calls behave exactly as before.
If you are moving from the Admin API, two other differences remain: a member added through createSpaceMemberships is not recorded as having joined the Space themselves, and is not added to the Space’s parent Spaces.cheerCount is deprecated in favor of reactionscheerCount on posts, events, and polls counts only legacy cheers. Emoji reactions are not included, so it reads 0 for a post whose engagement is reactions. Read reactions instead: it returns one entry per emoji with its count.cheerCount keeps working until January 15, 2027, when we remove it. Before then, replace it in your queries with reactions { emoji count }.
New integration guides for six popular platformsSix step-by-step guides show you how to connect your Mighty Network to the tools you already use, with the Mighty API’s GraphQL operations and webhooks:
  • HubSpot — keep members and HubSpot contacts in sync both ways, and turn HubSpot activity into tags and invites.
  • ActiveCampaign — keep ActiveCampaign contacts, lists, and tags in sync with your members.
  • Salesforce — sync members, plans, and tags into Salesforce Contacts, and push Salesforce changes back.
  • Shopify — give a customer access when they buy a Shopify product, and remove it on a refund or cancellation.
  • WordPress — build a plugin that connects to your Network with OAuth and shows upcoming events and recent posts.
  • Eventbrite — invite Eventbrite ticket buyers to a Space, and mirror your Eventbrite events in your Network.
You’ll find all six under Integration guides in the sidebar.Network Hosts can edit posts and course content in every SpaceA token acting as a Network Host and holding host:write:network_posts can now edit another member’s post or article with updatePost, and change course content with createCoursework, updateCoursework, publishCoursework, unpublishCoursework, and reorderCoursework, in any Space in your Network — including Spaces the Host has not joined. Until now those calls returned FORBIDDEN unless the Host held a Host or Moderator role in that Space, or wrote the content themselves. This is the reach the scope’s consent screen already describes, and it matches what the Admin API’s coursework endpoints allow a Network Host key.Network Hosts can opt in to unpublished course contentCourse reads remain published-only by default. A Network Host with no role in the Space can pass includeUnpublished: true to receive HIDDEN, PENDING, PROCESSING, DRAFT, and SCHEDULED items when their token holds host:read:network_posts, host:read:network, or host:write:network_posts. The new argument is available on Course.posts, Course.coursework, FlexSpace.posts, Coursework.parent, Coursework.children, Coursework.childCount, and CourseProgress.items.Nothing changes for a token without a host scope: write:posts still reaches only content the member could edit anyway. Announcement posts, forked posts, and other members’ Zoom events stay off-limits, as they are for a Space Host. If your integration made its token’s owner a Space Host so it could manage course content, it no longer needs to.Read an unpublished coursework item by IDnetwork.coursework(idOrSlug:) takes a new optional includeUnpublished argument. Set it to true to get a lesson, quiz, section, or overview that isn’t published yet (draft, hidden, pending, or scheduled). This matches what the Admin API’s GET /admin/v1/networks/:network_id/spaces/:space_id/courseworks/:id returns. You get the item if you’re a Host or moderator of its course, or if you use a Network Host token holding host:read:network_posts, host:read:network, or host:write:network_posts. Other callers still get NOT_FOUND. When you leave the argument out, it defaults to false, so the field keeps returning published items only.
Member.location is in the published schemalocation on Member and User is now part of the published schema, so you can generate clients and validate queries that select it. It returns the member’s self-reported location, and never a location guessed from their IP address. On a Network whose Host has turned member locations off, location returns null. Before this change, selecting it on such a Network failed validation with Field 'location' doesn't exist on type 'Member'.
Fetch invites, custom field answers, and custom field options by IDnode(id:) now resolves an Invite or a CustomFieldAnswer from the id you read off it, so you can refetch one record without paging back through network.invites or customField.answers. Before this, both types listed Node among their interfaces but node(id:) returned an error for them. The same people can read them as before: an Invite is returned to its sender and to Hosts and Moderators of the Space or Network it invites to, and a CustomFieldAnswer to Network Hosts and Moderators who can see the custom field it answers. Anyone else gets null.A CustomFieldOption is stored as a member segment, so node(id:) still returns its id as a Segment and a ... on CustomFieldOption fragment never matches. Fetch one option with customField.option(id:) instead, which takes a resource ID or GlobalID and returns NOT_FOUND when the option does not belong to that custom field.
Read one member’s responses to a custom field with answers(memberId:)CustomField.answers takes an optional memberId argument that narrows the connection to one member’s responses, so you no longer page through every member’s answers to find one. It takes a numeric resource ID or a GlobalID. Most fields hold one response per member; a multi-location field holds one per place the member picked, and you get all of them. Each CustomFieldAnswer carries its id, resourceId, text, createdAt, and updatedAt.A memberId that matches nobody, or names a member of another Network, returns an empty connection rather than an error. Otherwise the argument changes nothing about who can read responses: only Hosts and Moderators see them, a Mighty API token needs host:read:network, host:read:network_members, or host:write:network_members (see OAuth Applications — Scopes), and every other viewer gets an empty connection. This replaces the Admin API’s GET /admin/v1/networks/:network_id/custom_fields/:custom_field_id/members/:member_id/answers endpoint.
Read a post’s gallery images in the order the Host arranged themmainImages on posts, events, and polls returns the post’s gallery images — the uploaded images that make up its carousel — in the order the Host arranged them. assets returns attachments in upload order, so a gallery the Host reordered came back with the wrong image first. mainImages leaves out images placed inside the post body, and is an empty list when the post has no gallery. For the single image that represents the post, use imageUrl.Asset.position returns the asset’s zero-based place among the post’s attachments, so a client that already reads assets can sort them itself. It is null when no position is recorded.If you are moving off the Admin API, mainImages { imageUrl } replaces the images list on GET /admin/v1/networks/:network_id/posts and GET /admin/v1/networks/:network_id/posts/:id. Where mainImages is empty, fall back to the post’s imageUrl, the way images fell back to a single representative image. The URLs are different renditions of the same images: images returned a rendition sized for link-preview cards, while mainImages { imageUrl } returns the sized image used for display.
Tell archived Spaces apart with Space.archived, and leave them out with includeArchived: falseSpace.archived says whether a Host has archived a Space. network { spaces } has always returned archived Spaces alongside active ones, with nothing on the Space to tell them apart. Now each Space says which it is.network { spaces } also takes a new optional argument, includeArchived. It defaults to true, so an existing query returns exactly the Spaces it did before. Pass includeArchived: false to list active Spaces only; this works for any viewer. The order of the results is unchanged.If you are moving off the Admin API, spaces(includeArchived: false) matches GET /admin/v1/networks/:id/spaces, which lists active Spaces only.
Use membership mutations for Tags and BadgesupdateCustomFieldAnswer and deleteCustomFieldAnswer now reject Tag and Badge Custom Fields, return a null response, and explain the error in errors. These calls previously replaced or removed a member’s entire Tag or Badge set.Use createTagMemberships and createBadgeMemberships to assign them, and deleteTagMemberships and deleteBadgeMemberships to remove them. Other Custom Field types are unchanged. This matches the Admin API and prevents accidental membership removal.Mighty API updateSpace no longer adds you to the SpaceupdateSpace no longer adds the member your token acts as to the Space it updates. This matches the Admin API. Renaming a Space or changing its description still works without a Space membership.A featureToggles update now requires a Space role allowed to change every requested feature. If any feature is outside that role, errors names it and the mutation applies nothing. A Network Host with no role in the Space can join it as a Host through createSpaceMemberships, then retry the update.Fetch a single comment or RSVP by IDnetwork.comment(id:) and network.rsvp(id:) accept resource IDs or GlobalIDs. They replace GET /admin/v1/networks/:network_id/posts/:post_id/comments/:id and GET /admin/v1/networks/:network_id/events/:event_id/rsvps/:id when fetching one record. Both lookups enforce Network and parent-content visibility, returning NOT_FOUND when the record is unavailable.Comments require read:network, write:comments, host:read:network, or host:read:network_posts. RSVPs require read:network, write:posts, write:comments, host:read:network, host:read:network_posts, or host:read:network_events to read any RSVP. A token holding only write:rsvps can read the acting member’s own RSVP and no others. See OAuth Applications — Scopes.
Read when a member’s account was created with Member.createdAtMember.createdAt returns when the member’s account in your Network was created. It can differ from joinedAt, which is when their full Network membership began. A member who first belonged only to specific Spaces and later became a full member, or who left and rejoined, has a joinedAt later than their createdAt. A member who has only ever been a limited member has the same value for both.Any member of the Network can read it, and it needs no scope beyond the one that already lets you read the member. It carries the Admin API’s member created_at; the Admin API’s updated_at corresponds to Member.updatedAt.
Create a limited member with createMembercreateMember now takes memberType. The default, FULL_MEMBER, joins the new account to your Network as before. Pass LIMITED_MEMBER with planId — a free plan that does not include the Network — and the account is granted that plan instead, so it holds the plan’s Spaces and no Network membership, making it a limited member. Space access for that tier comes only from a plan, so there is no equivalent of the Admin API’s member_type: limited and space_ids, and createSpaceMemberships returns SKIPPED_LIMITED_MEMBER rather than adding one.planId is required for a limited member and rejected for a full member. A plan that is missing, from another Network, archived, paid, one-time, Space-less, or Network-including is refused before the account is created, so a wrong planId leaves nothing behind. A limited member is always a CONTRIBUTOR, and someone already in your Network is refused — use createPaymentPlanMembership to grant a plan to an account that is already there. Their welcome email carries no sign-in link.The call creates a subscription, so it also requires host:write:network_plans. See OAuth Applications — Scopes.
Use any available Network image with updateBadgeupdateBadge now accepts any available image in your Network as avatarId, regardless of who uploaded it. This matches createBadge and the Admin API. The field accepts a resource ID or GlobalID; passing null still leaves the Badge image unchanged.Both mutations now return the same validation messages: Asset not found, Asset must be an image, Asset cannot be a video, or Choose an available image. for a blocked image or one whose original was deleted. If your integration matches the former updateBadge messages Image not found. or That asset is not an image., update it.Link to a Payment Plan’s public page with landingPageUrlPaymentPlan.landingPageUrl returns the public URL of the plan’s Landing Page, where members and prospective members view and purchase the plan — https://<your Network domain>/plans/<plan ID>. Use it for “join” or “buy” links in emails, websites, and CRM or automation flows instead of building the URL yourself. Members can read it as well as Hosts, and it needs no scope beyond what reading the Payment Plan already requires.It replaces the permalink field on the Admin API’s plan responses (GET /admin/v1/networks/:id/plans and GET /admin/v1/networks/:id/plans/:plan_id). The existing PaymentPlan.url is unchanged: it is still the Admin page where a Host reviews and publishes the plan.
Member.spaces leaves out archived Spacesnetwork { member(id:) { spaces } } no longer returns Spaces the Host has archived. It now lists only the active Spaces the member belongs to, matching the Admin API’s GET /admin/v1/networks/:id/members/:member_id/spaces. Before this change archived Spaces came back alongside live ones, and nothing on Space let you tell them apart.The query is unchanged and needs no edit. If your integration audits access or syncs a member’s Spaces to another system, expect archived Spaces to drop out of the list. Restoring an archived Space returns it to the list. Who can read the field is unchanged: the member themselves and Network Hosts and moderators, with an empty list for everyone else.Read a Custom Field option’s color with CustomFieldOption.colorCustomFieldOption.color returns the option’s display color as a hex code, or null when unset. It is available through CustomField.options, CustomFieldAnswer.options, and MemberCustomFieldResponse.selectedOptions, matching segments[].color in Admin API Custom Field answers.Network Hosts and moderators can read the field. Mighty API tokens require host:read:network, host:read:network_members, or host:write:network_members; other members receive null.
Read every coursework status, and count an item’s children with Coursework.childCountCourseworkStatus gains three values: PROCESSING, for an item whose media is still being processed, and DRAFT and SCHEDULED, for items saved but not published. None of them is visible to members. A course could already hold items in these states — a lesson whose video was still processing, or an item the Admin API saved as a draft or scheduled — and reading one through Course.coursework, Coursework.children, Coursework.parent, or node returned an error instead of data. Those reads now return the item with its status. If your client switches over CourseworkStatus exhaustively, add a case for the new values. A Host or Moderator of the course sees these items, as does an eligible Network Host token that passes includeUnpublished: true; members do not. The new values fix reads that failed, so they ship as a critical bug fix, outside the three-month notice period in the deprecation policy.createCoursework and updateCoursework still set only POSTED, HIDDEN, or PENDING. Passing one of the new values returns a message in the payload’s errors list and leaves the course unchanged. To keep an item from members, set it to HIDDEN. Course.coursework(status:) accepts every value as a filter.To filter by more than one status, pass statuses:, a list of CourseworkStatus values, to Course.coursework, Coursework.children, or Coursework.childCount. An item matches if its status is any value in the list, so statuses: [POSTED, HIDDEN, PENDING] leaves out processing, draft, and scheduled items. Like status:, it narrows what you can already see and never widens it. An empty list applies no filter. status: keeps working; when you pass both, an item must match both.Coursework.childCount returns how many direct children an item has, so you no longer have to page through children to count them. It counts the same items children lists to you: a Host or Moderator’s count includes children that are not posted, and a member’s count covers posted children only. That differs from the Admin API’s children_count, which counts every child, archived ones included, whatever its status. Pass the same statuses: to childCount as to children and the count matches the filtered list.
Read the price point a member subscribed to or purchasedPaymentSubscription.price and Purchase.price return the PaymentPlanPrice the member actually holds — its currency, interval, list amount, isOneTime, and installmentCount. Until now the only pricing you could reach from a transaction was plan, the Payment Plan, whose prices list every option the Host offers — so a plan sold both monthly and yearly, or in more than one currency, gave you no way to tell which of those options a member was on.PaymentSubscription.pendingPrice does the same for a move that has not taken effect yet. A member switching from monthly to yearly inside one Payment Plan leaves pendingPlan pointing at the plan they are already on, so pendingPrice is what names the option they are moving to. It is null when no move is pending.amount on the subscription or purchase is unchanged: it stays the figure the member is billed after any Promo Code discount, and price.amount is the list price before that discount. plan.prices still lists every option the Payment Plan offers. price and pendingPrice are null when the price point they name no longer exists.Email a member a sign-in link with sendMemberSignInEmailsendMemberSignInEmail emails an existing member a link that signs them in to your Network, so you can help a member who cannot get in. Pass the member as memberId, which takes a numeric resource ID or a GlobalID. The payload returns the member you named and an errors list carrying anything you can correct and retry. createMember already sends a sign-in link to a member you create; this mutation sends one to a member who is already there.The email offers two ways back in. Its heading and main button read “Reset your password” — or “Add a password” for a member who has never set one — and a “Sign in with Magic Link” link sits below them, which signs the member in without a password. Sending it issues a new password reset for the member, so any reset link they were already holding stops working. Their existing password changes only if they use the new link, and sessions they have open elsewhere keep working. If the member has no email address on file, errors says so and no email goes out. A memberId that matches nobody, or a member of another Network, returns a NOT_FOUND error.A member can be sent at most three of these emails an hour, counted across every Host and credential sending to them. A send past that returns a THROTTLED error and no email goes out, so wait and try again later rather than retrying straight away.The mutation requires a token acting as a Network Host and holding the host:write:network_members scope. A credential without that scope receives a FORBIDDEN error naming it, and a Moderator cannot send the email. It replaces the Admin API’s POST /admin/v1/networks/:id/members/:member_id/password_resets endpoint, which sends this same email. See the members reference for the rest of what you can do with a member.
Post body fields leave out content the author hidbodyHtml, bodyText, and body on posts, events, and polls no longer include content hidden in the post body with the hidden attribute, an inline display: none, or an inline visibility: hidden. For those three cases you now get what a browser shows, so plain-text consumers and clients that render the HTML without CSS stop showing that hidden text. Content hidden only through a stylesheet class is still returned. The stored post body is unchanged, and posts without hidden content return exactly what they did before.If your integration read hidden metadata out of bodyText or bodyHtml, it no longer arrives through these fields.Read Spaces Collections by ID and include system CollectionsYou can fetch one Spaces Collection with network.spacesCollection(id:), using a resource ID or GlobalID. The lookup returns NOT_FOUND when the Collection is missing or inaccessible to the acting member. SpacesCollection.id continues to return the resource ID.Network Hosts and Network Moderators can include system Collections in network.spacesCollections(includeSystem: true) and fetch them by ID. The default list still excludes them, and other members still see only visible, non-system Collections. The new SpacesCollection.systemCollection field identifies system Collections. The lookup, system inclusion, and system metadata require Collection read or write consent.reorderSpace moves one Space at a time, and each move commits independently. Sending several moves, even as aliased mutations in one request, does not make them one transaction, so a failed move leaves earlier moves in place. This differs from the all-or-nothing positional batch in the Admin API. To build a complete order, move the last Space to the top, then move each earlier Space above the one you moved before it with atItemId. After each move, check both the top-level errors and reorderSpace.errors. On a CONFLICT, fetch the Spaces again and recompute the remaining moves before retrying.
Network Hosts and Moderators see every scheduled post in the Networknetwork.posts(statuses: [SCHEDULED]) now returns every scheduled post in the Network when you query it as a Host or Moderator of the Network itself. Previously it returned only the scheduled posts you created, plus those in Spaces where you hold a Host or Moderator role, so a Network Host who held no role in the Space a post was scheduled into never saw that post.The query is unchanged and needs no edit. If your integration reads scheduled posts for a Network Host or Moderator, expect more of them back, and page until pageInfo.hasNextPage is false rather than assuming a single page covers the Network.Everyone else sees what they saw before: their own scheduled posts, plus those in the Spaces they host or moderate. See the GraphQL reference for the full set of filters and fields.
Filter members by Custom Profile Field answersNetwork.members now accepts a customFields argument: a list of CustomFieldFilter inputs that narrows the roster by what members answered on a Custom Profile Field. The MemberFilter input createAnnouncement uses to build its audience already accepted customFields, and it now accepts the same contains filter.Each entry names a customFieldId and at least one of withResponse (whether the field was answered), segmentIds (which choice-field options were selected), or contains (a case-insensitive match against part of a text or web address field’s answer). A member must match every entry you pass, and a request can name a given Custom Profile Field only once. A request naming more than 50 Custom Profile Fields is refused.createAnnouncement now refuses customFields entries it used to quietly skip or overwrite, so an audience is never wider than you asked for. It returns an error for a customFieldId that does not exist or that you cannot read on a member profile, for segmentIds on a field that has no options, for a segment ID that is not one of that field’s own options, for an entry that names none of withResponse, segmentIds, or contains, and for a field named more than once. Send only fields you can read, segmentIds only on choice fields with that field’s own option IDs, and each field once.
Receive webhooks for lesson completions and event cancellationsYou can subscribe to MEMBER_LESSON_COMPLETED through createWebhookCallback or updateWebhookCallback. The delivered event type is MemberLessonCompletedHook, and its payload includes the member, course, lesson, and completed_at timestamp.The event fires when a lesson first satisfies its completion criteria, including the first view of a lesson set to complete on visit. Repeated views or completion requests do not create another event. If you reset the member’s progress, completing the lesson again creates a new event. Completing a quiz, section, or course overview does not trigger this event.You can also subscribe to EVENT_CANCELED when a published event or recurring series is canceled, and to EVENT_INSTANCE_CANCELED when one occurrence of a recurring event is canceled. The delivered event types are EventCanceledHook and EventInstanceCanceledHook.EVENT_CANCELED includes the standard event payload. EVENT_INSTANCE_CANCELED identifies the recurring event with post_id and includes the canceled occurrence’s starts_at, ends_at, and instance_index values.
A deprecation policy for the Mighty APIWe now publish a 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.
The member roster pages all the way throughnetwork.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 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 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_IDMemberSort 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.
Asset upload endpoint accepts the Admin API’s upload fieldsThe POST /networks/:network_id/assets endpoint accepts the Admin API upload’s optional fields, so an upload call moves across with one rename: the file part is file here where the Admin API calls it asset_file. There is now a page for the endpoint: see Asset uploads for the full request and response contract.asset_style is optional and defaults to post. Four styles join the ones it already accepted: post_description for images inside a post’s description, thumbnail for a video’s cover image, cover for a Space cover photo, and header for the Space and Network Primary Image.original_aspect_ratio and metadata are stored rather than ignored. Send original_aspect_ratio as a positive decimal such as 1.7778 or as a W:H ratio such as 16:9, and a ratio is stored as W / H. Send metadata as nested form fields or as a JSON object string; the accepted keys are is_main_image and is_video_image. For file and base64 uploads, is_main_image is retained only for the supported main-image formats; source_url uploads skip that format check.The response gains three fields. url is the CDN URL, matching the Admin API’s url. original_aspect_ratio echoes what you sent, as a decimal or null. metadata returns the stored declarations, or {} when you sent none.Three requests are answered differently. A file or base64 upload over 25 MB returns 413 with File size must be 25MB or smaller where it returned 400 before; a source_url whose fetched file is over 25 MB still returns 400. A request naming more than one of file, base64, and source_url returns 400, so name exactly one; it used to be accepted with both sources set. A video or audio asset_style returns 400 pointing you at createUploadSession, which is the path that uploads in chunks and transcodes the result so it plays.
updateComment links bare URLs and email addresses in a comment bodyA 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.
RSVPs require a write scopecreateRsvp, 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.
Create a member from an email addresscreateMember 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.
Fixes this month
  • Access tokens issued to an OAuth application a Network Host created now expire one hour after issuance, as Refreshing tokens describes. They previously stayed valid for 180 days, so a leaked token could be used long after the documented lifetime. This is a security fix. Read expires_in from each token response and exchange your refresh token for a new pair when the access token expires. Tokens issued before the fix keep their original expiry.
Documentation clarifications
  • Coursework.completionCriteria — the field’s description in the reference now says that an item set to PRIMARY_MEDIA reports BUTTON while its primary media slot holds no Mighty audio or video. The behavior itself is unchanged and is described in the September 29 entry above.
Uploading media requires an asset write scopecreateUploadSession, 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.
A member total on the Network rosterNetwork.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.
The User type is deprecated in favor of MemberThe schema 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.
BROKEN on StripeAccountStatusNetwork.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.
Member counts on plans and spacesPaymentPlan.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 changedcreateBadgeMemberships, 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 is unaffected, and you can still grant them a badge or tag.createEvent takes either form of space IDcreateEvent’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.
Nested connections page 25 at a time, and queries above a complexity of 1,500 are rejectedConnections 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.
Simpler upload strategy and provider namesThe 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.
location on events is deprecated — use venueThe 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.
Direct messages, with member consentThe 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.
General availabilityThe Mighty 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 referenceThe GraphQL reference 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 launchThe 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 hostAutomationRuleTarget 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.