Overview
This guide walks you through building a service that connects a Mighty Network to Salesforce with the Mighty API. When you finish, you have:- A backfill that copies every member of your Network into Salesforce as a Contact
- A live sync that updates those Contacts within moments of a member joining, editing their profile, buying a plan, or gaining a tag
- A write-back path that tags members in your Network when your team updates a Contact in Salesforce
How the pieces fit
Your sync service sits between the two systems. It holds a host’s Mighty API token and a Salesforce integration credential, and it’s the only component that talks to both. A small service is the simplest way to receive Mighty’s webhooks. Receiving them inside Salesforce would mean exposing a public Apex endpoint through a guest-user site, which Salesforce’s own security guidance advises against. The service treats each webhook as a signal, not as the data itself. When a member event arrives, the service reads that member’s current record from the Mighty API and upserts it into Salesforce. Because every upsert writes the latest state, duplicate or out-of-order deliveries can’t leave a Contact stale.Before you begin
You need:- A Mighty Network on the Scale plan or above. OAuth applications, which issue Mighty API tokens, are available from Scale up. Webhooks follow your plan too.
- A host account on the Network. The integration reads the member roster and registers webhooks, which only hosts can do.
- Member email visibility on your plan, if you want to match members to Contacts by email. The Mighty API returns a member’s email to hosts only when the Network’s plan includes member-email visibility and the member has consented to sharing it. Without it,
emailisnulland you match on the Mighty member ID instead. - A Salesforce org with API access and permission to create custom fields and external client apps.
- An HTTPS endpoint where your sync service can receive webhooks and the OAuth callback.
Step 1: Map your data
Decide which member data belongs in Salesforce before you write any code. This guide syncs the following fields onto the standard Contact object:
Browse the GraphQL Schema Explorer to find other fields worth syncing, such as custom field answers (
Member.customFieldResponses), badges, or course progress. The GraphQL type behind every Member.* field above is named Member, not User.
Step 2: Prepare Salesforce
1
Create the custom fields
In Setup > Object Manager > Contact > Fields & Relationships, create the custom fields from the mapping table. Create
Mighty_Member_ID__c as a Text field and select both Unique and External ID, so you can upsert on it. Use Date/Time for the timestamp fields, Long Text Area for Mighty_Tags__c, and Text for the rest.2
Create an integration user
Create a Salesforce user for the integration, and give it a permission set that grants API access plus read and edit access to Contact and the new fields.
3
Create an external client app
In Setup, open External Client App Manager and create an external client app with OAuth enabled. In its OAuth policies, select Enable Client Credentials Flow and set the integration user as the user the flow runs as. Note the consumer key and consumer secret. See Salesforce’s guide to configuring the client credentials flow for external client apps.Since Spring ‘26, Salesforce blocks creating new connected apps unless Salesforce Support grants an exception, so use an external client app. An existing connected app with the client credentials flow enabled also works.
Step 3: Create a Mighty OAuth application
In your Network, go to Network Admin > Integrations > OAuth Applications and click New OAuth Application. See OAuth Applications for every option.- Client type: Confidential. Your sync service runs on a server, so it can keep a Client Secret.
- Redirect URI: your service’s callback route, for example
https://sync.example.com/oauth/callback. - Scopes: request only what the integration uses:
Save the application, then store the Client ID and Client Secret in your secret manager.
Every scope above except
read:userinfo is a host scope, so only a host of the Network can complete the authorization — that’s intended, since the integration always acts as a host. read:userinfo is a contributor scope that any authorizing member grants for themselves; here that’s still your dedicated host account. Combined with member-email visibility on your plan, it’s what lets Member.email come back as a real address instead of a masked one like j***@***.***.Step 4: Authorize the integration once
A host signs in through the Authorization Code flow one time. Your service keeps the resulting refresh token and uses it to mint fresh access tokens from then on, with no further sign-ins. Add two routes to your service: one that starts the flow and one that receives the callback.server.js
/connect behind your own admin login, deploy the service, and have the integration host visit it. After they approve, check the stored scope. It lists what was actually granted, which can be less than you asked for.
Step 5: Call the Mighty API from your service
Access tokens expire after one hour. The helper below refreshes the token when it’s close to expiry, saves the rotated refresh token, and sends every GraphQL request with the headers the API requires.mighty.js
refreshing only collapses concurrent refreshes within one process. If you run your sync service as multiple instances, put a distributed lock (for example, a database row or a Redis key) around the refresh call too, or route token refresh through a single instance.salesforce.js
refreshFn above requests a new session whenever Salesforce reports the current one expired.
Step 6: Backfill existing members
The backfill pages through the member roster and upserts each page into Salesforce. It uses two queries: one for member profiles and one for subscriptions.Members query
Subscriptions query
members and paymentSubscriptions return at most 50 records per page. The members roster is only available to hosts and moderators; any other token gets an empty list, not an error.
The backfill loads subscriptions first, because they’re returned newest first. The first subscription seen for each member is their most recent one.
backfill.js
Upserting on
Mighty_Member_ID__c creates a new Contact for every member the first time it runs. If your org already has Contacts for many of your members, run a one-time match first: look up existing Contacts by email and write each member’s resourceId into Mighty_Member_ID__c, so the backfill updates them instead of creating duplicates.customFieldResponses, lower first or fetch those fields in a separate query, and watch extensions.cost.complexity on the response.
Step 7: Keep Salesforce current with webhooks
With the backfill done, register a webhook so your service hears about changes as they happen. Run this once, for example from a setup script:Variables
apiKey with a secure random generator and store it in your secret manager. Mighty sends it as a Bearer token in the Authorization header of every delivery, so your endpoint can reject requests that don’t come from Mighty. Always pass includedEvents: if you omit it, the callback subscribes to every event, including posts and comments.
Each delivery is a JSON POST with an event_id, an event_timestamp, and a payload. See Webhooks for the payload of each event. Member events carry the member’s ID in payload.member.id; plan and subscription events carry it in payload.member_id.
MEMBER_JOINED, MEMBER_UPDATED, and MEMBER_LEFT fire once for the Network and again for every Space a member joins, updates their profile in, or leaves — a course, a community, any Space. A Network with many Spaces delivers far more of these events than its member count alone implies. The payload’s space_id and network_id match only for the Network-level event; a Space-level event carries that Space’s own ID in space_id. syncMember below doesn’t need to tell them apart: it always re-reads the member’s current state from member(id:), so a Space-level MEMBER_LEFT — someone leaving a single Space while remaining in the Network — finds the member still resolves and re-upserts their current profile instead of taking the “left” branch. Only a Network-level MEMBER_LEFT, where the ID genuinely no longer resolves, does that.Member query
sync.js
member(id:) distinguishes exactly the two ways this lookup fails, so the sync marks a Contact Left only when the member is genuinely gone, and throws — leaving every Contact untouched — when the token itself lost roster access.
The webhook handler checks the secret, syncs the member, and then responds:
webhooks.js
500 and Mighty retries the delivery later. A retry, or the same event arriving twice, is harmless, because the sync always writes the member’s current state.
Step 8: Write changes back to Mighty
Two-way sync lets Salesforce drive what happens in your Network. For example, when a sales rep marks a Contact as a VIP, the member gets aVIP tag, which can unlock a Space or trigger a Network automation.
First, find the tag’s ID:
createTagMemberships:
salesforce-hooks.js
createTagMemberships can no-op with an empty errors array: outcome is GRANTED when at least one member changed, ALREADY_GRANTED when every member already had the tag (the desired state is met, so this is still a success), or SKIPPED_NO_MEMBERSHIP when no requested member holds a membership in the Network or any of its Spaces, meaning nothing was granted. Check outcome as well as errors, or a skipped grant looks like a success. To remove the tag, call deleteTagMemberships with the same input and check its outcome the same way — REVOKED means something changed, NOT_GRANTED means no member had the tag to begin with. To write a Salesforce value into a member’s profile, such as their account manager’s name, call updateCustomFieldAnswer with the custom field’s ID, the member’s ID, and the text or option IDs to set.
In Salesforce, call the endpoint from a record-triggered flow on Contact:
- Create an external credential that stores the shared secret, and a named credential for your service’s URL that sends it as an
X-Sync-Secretcustom header. See custom headers with named credentials. - Create a record-triggered flow on Contact that runs when the VIP field changes to true.
- On the flow’s Start element, add an asynchronous path. Salesforce doesn’t allow callouts while a record is saving, so the callout must run on that path.
- On the asynchronous path, add an HTTP Callout action that uses the named credential to
POST{ "mightyMemberId": "<Mighty_Member_ID__c>" }to/salesforce/vip.
Step 9: Run it in production
- Reconcile nightly. Webhooks are delivered asynchronously and retried, but a long outage on your side can still lose events. Rerun the backfill on a schedule to catch anything missed among current members. It’s safe to repeat because every write is an upsert. The backfill alone can’t catch a lost
MEMBER_LEFTevent, because a member who left no longer appears in the roster it pages through. Add a second pass after each run: query Salesforce for Contacts whereMighty_Status__c = 'Member'andMighty_Member_ID__cwasn’t in this run’s roster, then callsyncMemberon each. It looks the member up in the Mighty API and setsMighty_Status__ctoLeftfor anyone who’s really gone. - Monitor webhook health. Two safety nets protect a failing endpoint, and only one of them self-heals. A circuit breaker pauses deliveries automatically and resumes them on its own once your endpoint responds with
2xxagain. Separately, 5 consecutive failed deliveries spanning 3 or more days permanently disable the callback: once that happens, deliveries stop for good, and nothing your endpoint does brings them back. Querynetwork { webhookCallbacks(first: 10) { nodes { url disabled consecutiveFailures } } }from a monitor, and alert ondisabled: trueso you can callupdateWebhookCallback(input: { id: ... })— or re-save the webhook in Network Admin — to turn it back on. - Handle a lost authorization. If a token refresh returns
invalid_grant, the refresh token has been revoked or has expired. Alert someone to have the integration host visit/connectagain. - Stay within rate limits. The Mighty API meters GraphQL operations under the same quota model as the Admin REST API, tracked separately. Each webhook delivery costs one or two operations, and a backfill page costs one. A burst of events, such as a bulk tag change, triggers that many syncs at once. On a large Network, cap how many syncs run concurrently, for example with a concurrency limiter such as p-limit. A delivery that fails because of a rate limit returns
500, and Mighty retries it later. - Protect the credentials. Store the Mighty refresh token, Client Secret, webhook secret, and Salesforce consumer secret encrypted, and never log them. See Token handling.
- Follow the changelog. Check the Mighty API changelog before you upgrade, and watch for deprecations of any field your sync reads.
Troubleshooting
Next steps
Mighty API
Learn the endpoint, errors, cost limits, and schema tools.
GraphQL Schema Explorer
Find more member data to sync, and try queries against sample data.
Authentication
Read more about tokens, refresh, and revocation.
Webhooks
See the payload for every webhook event.