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 player
PrimaryMedia.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
updateSpacenow returns an error whenfeatureTogglesturnsautojoin_spaceon for a Space whosejoinablesetting isfalse. It previously returned the Space with no error while leaving auto-join off, so the call looked successful but changed nothing.spaceis nownullanderrorssays the Space must be joinable first. None of the request’sfeatureTogglesare applied, though atitleordescriptionin the same call is still saved. The Mighty API cannot change a Space’sjoinablesetting yet, so turn on Allow Anyone to Join in the Space’s privacy settings in the web app, then retry. Sendingautojoin_spaceastruefor a Space that already has auto-join on is accepted as before.createGiftnow rejects a gift whoserecipientIdis the calling member, as its reference description (“to another member”) says. The payload returns a nullgiftTransactionandYou cannot send a gift to yourselfinerrors, and no points are spent. It previously created the gift and spent the caller’s own points on it.
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:postIdandreplyToIdoncreateCommentidonupdateComment,updatePost,updateEvent,updateRsvp, anddeleteRsvpeventIdoncreateRsvpspaceIdonnetwork.posts,network.savedPosts,network.events, andnetwork.memberspostIdonnetwork.comments
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 If you are moving from the Admin API, two other differences remain: a member added through
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:
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.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 }.- 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.
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 IDRead one member’s responses to a custom field with Read a post’s gallery images in the order the Host arranged themTell archived Spaces apart with Use membership mutations for Tags and BadgesRead when a member’s account was created with Create a limited member with Use any available Network image with Read every coursework status, and count an item’s children with
node(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.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.mainImages 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.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.updateCustomFieldAnswer 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.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.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.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. Headless tokens require host:read:network,
host:read:network_members, or host:write:network_members; other members receive null.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 purchased
PaymentSubscription.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 hid
bodyHtml, 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 Network
network.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.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 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 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 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.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.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_infrom 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.
Coursework.completionCriteria— the field’s description in the reference now says that an item set toPRIMARY_MEDIAreportsBUTTONwhile 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 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.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.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 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 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.
createEventandupdateEventaccept optionalthumbnailIdandheaderIdarguments 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. OnupdateEvent, an explicitnullclears an image.- The asset upload endpoint (
POST /networks/:network_id/assets) accepts two new asset styles for event images:square_thumbnailandcinema_header. createEventandupdateEventaccept optionalstreetandcityarguments 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; onupdateEvent, an explicitnullclears a field.Post.venueandPoll.venuereturn the free-text venue or place name saved on a post, such as “Blue Bottle Coffee”, or null when none is set.Event.venuealready returned this value, so the place name now reads the same way on all three post content types.createLiveSpacestarts a livestream and returns it with theurlto open to join the room as its creator. PassspaceIdto start it in a space, or omit it to start it on the Network. Recording is off unless you passrecord: true, andnotifyMembers: truetells the space’s members it has started. A livestream draws down your Network’s streaming hours until it ends.completeLiveSpaceends 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.
createEvent/updateEvent— thecityargument is free text, so you can append a state or province and a postal code after the city name (for exampleLondon, Ontario, N5V 3N8). Including them improves the accuracy of the coordinates geocoding derives fromstreetandcity.
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.