Skip to main content
This guide walks you through a server-side integration that connects a Shopify store to your Mighty Network. When a customer pays for a Shopify product, your integration grants them the Spaces that product unlocks. When the order is refunded or canceled, it takes that plan access away. Use this pattern when Shopify is where you take payment but the thing you’re selling lives in Mighty: a course, a cohort, a membership, or a members-only Space bundled with a physical product.
Selling merchandise inside your Network is a different job. To embed a Shopify storefront in a page, see How Do I Embed a Shopify Buy Button in My Mighty Network?, which needs no code.

How it works

Shopify collects the money. Mighty grants access through a free Payment Plan that holds the Spaces the customer bought. Your service sits between them. It receives Shopify webhooks and calls the Mighty API as a Network Host. Granting a free plan, rather than adding the member to Spaces one by one, gives you three advantages:
  • One ID per product. The plan decides which Spaces a customer gets. You can change that list in Mighty without redeploying your integration.
  • Clean revocation. Removing the plan removes the Spaces that only it grants.
  • Limited Members work. A customer who should get only the purchased Spaces, not your whole Network, can be created as a Limited Member holding that plan.

Before you begin

You need:
  • A Mighty Network on the Scale plan or above, which is required to create OAuth applications
  • Host access to that Network
  • A Shopify store where you can add webhooks
  • A server you control that can receive HTTPS requests and store secrets, such as a small Node.js or Python app, or a cloud function
The code samples use Node.js 18 or later with Express. The same steps apply in any language.

Step 1: Create a free plan for each product

In your Mighty Network, create one free Payment Plan for each Shopify product that grants access. Add the Spaces that product unlocks. The plan must be an active, free, subscription-style plan. createPaymentPlanMembership rejects paid plans and one-time purchase plans, because Shopify, not Mighty, has already taken payment. If you’ll grant the plan by creating a Limited Member (Step 4), it must also grant at least one Space and must not include the Network — createMember rejects a planId that includes the Network. Keep every plan to Spaces only, including plans for FULL_MEMBER products. createMember already adds a full member to your Network, so the plan doesn’t need to. It also keeps refunds predictable: revoking a plan that includes the Network, in a Network people can’t join freely, removes the member from the whole Network unless something else still pays for their membership. See Revoke access on refund or cancellation.
A free plan can be joined by anyone who reaches its join page. Treat it as a back-office plan: don’t feature it on your landing page or share its link. Your integration is the only intended way in.
Optionally, create a tag such as Shopify customer so you can find and segment these members later.

Step 2: Register an OAuth application

Your integration authenticates with OAuth 2.0 and acts as the Host who authorizes it.
  1. In your Mighty Network, go to Network Admin > Integrations > OAuth Applications and click New OAuth Application.
  2. Choose the Confidential client type. Your integration runs on a server, so it can keep a Client Secret.
  3. Register a redirect URI on your server, such as https://integrations.example.com/mighty/callback.
  4. Select these scopes:
  5. Save the application and store the Client ID and Client Secret in your secret manager.
This is a first-party integration that you build and run against your own Network, so you can enable Skip consent screen. See OAuth Applications for the full setup reference.

Step 3: Authorize the integration once

A Host completes the Authorization Code flow one time. Your service stores the resulting refresh token and uses it for every webhook after that. No customer ever signs in to your integration.
1

Start authorization from your server

Add a route, such as /mighty/connect, that generates a state nonce and a PKCE code_verifier, stores both in the server-side session, and redirects to https://YOUR_SUBDOMAIN.mn.co/oauth/authorize with the two scopes above.
2

Sign in as a Host

Open /mighty/connect and sign in with a Host account. Host scopes only work for Hosts, so a member or Moderator can’t complete this step.
3

Exchange the code and store the tokens

On the callback, verify state and iss, then exchange the code at /oauth/token. Store the refresh token encrypted at rest. It is the credential your integration runs on.
Follow the backend web app architecture: the server generates state and the verifier, receives the callback, and exchanges the code itself.
Authorize with a Host account that will stay a Host. The integration acts as that person. If they stop being a Host, token refreshes still succeed but every Mighty API call returns 401. If their refresh token is revoked, refreshes fail with invalid_grant.

Keep an access token fresh

Access tokens expire after one hour. Refresh on demand before you call the Mighty API, and persist the new refresh token whenever one is returned:
mighty-auth.js
Alert someone when a refresh returns invalid_grant. Until a Host re-authorizes, the integration can’t grant access to new customers. If multiple workers run this integration, serialize refreshes across processes too — for example, with a lock in the database that holds the refresh token — so only one worker ever exchanges a given refresh token.

Step 4: Map Shopify products to Mighty plans

Your integration needs to know which Mighty plan each Shopify product grants. Find the plan and tag IDs with the hosted GraphiQL explorer or a one-off query:
Keep the mapping in configuration, keyed by Shopify product or variant ID:
config.js
Use LIMITED_MEMBER when a purchase should unlock only that plan’s Spaces, and FULL_MEMBER when the customer should also join your Network. A plan mapped to LIMITED_MEMBER must grant at least one Space and must not include the Network — confirm that when you create the plan in Step 1. See What is a Full vs. Limited Membership? for the difference.

Step 5: Receive Shopify webhooks

In Shopify, subscribe your service to these topics: You can add webhooks in the Shopify admin under Settings > Notifications > Webhooks, or subscribe from a Shopify app. See Shopify’s webhooks documentation for both options. Every delivery carries an X-Shopify-Hmac-Sha256 header: a base64-encoded HMAC-SHA256 of the raw request body, signed with your webhook secret. Verify it against the raw body before you parse it, and reject anything that doesn’t match:
server.js
Shopify can deliver the same event more than once, and out of order. Record the X-Shopify-Webhook-Id of each processed delivery and skip repeats — that handles duplicates. It doesn’t handle reordering: a retried orders/paid that arrives after refunds/create would re-grant access you already revoked. The handlers below record each refunded or canceled line item and skip it on a later orders/paid, which is what handles reordering. The Mighty mutations are idempotent too, so a retried grant or revoke reports the state you asked for instead of failing.

Step 6: Call the Mighty API

Wrap the GraphQL endpoint in a small helper. Remember that requests to api.mn.co must send a User-Agent, and that GraphQL reports most errors with HTTP 200. Check the errors array on every response:
mighty.js
The thrown error’s codes array carries each error’s extensions.code — for example NOT_FOUND or THROTTLED — so callers can branch on the reason instead of matching message text.

Find or create the member

Resolve each Shopify customer to a Mighty member in this order:
  1. Your own records. Once you’ve resolved a customer, save their Mighty member ID against the Shopify customer ID (or, when an order carries no customer object, the normalized email) and reuse it.
  2. memberByEmail. Finds an existing member by the order’s email address. A failed lookup (no member found) counts against a per-Network budget of 10 failed lookups every 10 minutes, and every first-time customer is one, so a busy store can hit it. Back off and retry when memberByEmail raises THROTTLED.
  3. createMember. Creates the account and adds it to your Network. Accounts belong to a single Network: if the address already has an account in your Network, such as a former member’s, it’s reused. An account in another Mighty Network isn’t.
handlers.js
createMember emails the new member a welcome. For a full member, the welcome carries a sign-in link. A Limited Member receives the plan’s welcome email instead. Pass sendWelcomeEmail: false if you’d rather send your own onboarding email from Shopify.
memberByEmail returns null unless your Network’s plan includes member email visibility and that member has agreed to share their email with Hosts. Networks that use SSO are exempt from the consent requirement. When an existing member stays hidden, createMember refuses with This person is already a member of this Network. Route those orders to a person for review rather than retrying them. Saving member IDs after the first purchase keeps this case rare.

Grant the plan and tag the member

handlers.js
createPaymentPlanMembership doesn’t email the member. It reports ALREADY_SUBSCRIBED when the member already holds the plan, so repeated deliveries are safe. The handler settles the member type once per order. If any mapped product in the order is FULL_MEMBER, a new customer is created as a full member and every plan is granted with createPaymentPlanMembership, so a mixed order gives the same result whatever order its line items arrive in.
The Mighty API has no mutation that promotes a Limited Member to a full member. Granting a free plan that includes the Network would, but this guide keeps plans to Spaces only, as recommended in Step 1, because revoking such a plan would remove the member from the Network. When a Limited Member buys a FULL_MEMBER product, the handler still grants the plan, then routes the order to review so a Host can promote them in Network Admin. If you’d rather avoid manual steps, map every product to the same memberType.
A member can hold a tag while they belong to your Network or to any of its Spaces, so a Limited Member holding the plan’s Spaces is tagged like a full member. createTagMemberships reports SKIPPED_NO_MEMBERSHIP only for someone with no membership at all, such as a customer whose access you’ve already revoked.

Revoke access on refund or cancellation

handlers.js
Revoking a free plan ends access immediately. deletePaymentPlanMembership is idempotent too: it reports NOT_SUBSCRIBED when the member no longer holds the plan. Revoking a Spaces-only plan removes the member from the plan’s Spaces, with three exceptions. They keep:
  • A Space that another plan or purchase still grants them.
  • A Space a Host added them to directly.
  • For a full member, any Space in the plan that’s open for Network members to join.
A full member’s Network membership stays in place. A Limited Member who held only this plan’s Spaces is left with no membership.
If the plan includes the Network, and your Network isn’t open for anyone to join, revoking it also removes the member from the Network and every Space in it, unless another plan or purchase still covers their Network membership. Keep plans to Spaces only, as recommended in Step 1, to avoid removing members on a refund.
To remove a FULL_MEMBER customer from the Network on a refund, call deleteNetworkMembership (scope host:write:network_members) after revoking the plan. The member keeps their content and can rejoin the Network later.
A partial refund doesn’t revoke access. handleRefund adds up each product’s refunded quantity across every refund on the order, and revokes the plan only once all the units of that product are refunded. If a customer bought two units and you refund one, they keep access. The same applies across orders. Before you revoke, check whether the customer holds another paid order for the same product that hasn’t been refunded. If they bought it twice, one refund shouldn’t remove access they’re still paying for.

Step 7: Test the integration

  1. Create a test product in Shopify, map it to a free plan, and place a test order with an email address that isn’t in your Network.
  2. Confirm the new member appears in your Network with the plan’s Spaces, and with the tag if you set one.
  3. Place a second order for the same product. The grant should report ALREADY_SUBSCRIBED.
  4. Refund the order and confirm the member loses the plan’s Spaces, apart from any the exceptions in Revoke access on refund or cancellation keep, and that a full member stays in your Network.
  5. Refund a LIMITED_MEMBER-only order, leaving that customer with no current membership, then place an order for a FULL_MEMBER product with the same customer. Confirm they rejoin as a full member instead of the order stalling.
  6. Place an order for two units of the product and refund one. Confirm the member keeps access, then refund the second unit and confirm access is revoked.
  7. Replay a webhook delivery from your logs and confirm your service skips it.

Go further

Once the core flow works, you can extend it in either direction:
  • Recurring access. If you sell subscriptions through a Shopify subscription app, grant on each successful renewal and revoke when the subscription is canceled or payment fails.
  • Mighty to Shopify. Register a Mighty webhook with createWebhookCallback (requires the host:write:network_integrations scope) to react to events such as MEMBER_JOINED or MEMBER_COURSE_PROGRESS_COMPLETED. You could tag a Shopify customer when they finish a course, or issue a discount code to members of a Space.
  • Reconciliation. Run a nightly job that lists members of each plan with network { members(planId: ...) } and compares them to your paid Shopify orders. It catches anything a missed webhook left out of sync.

Troubleshooting

The token refresh returns invalid_grant

The refresh token is no longer valid. For example, it was revoked, or it was already exchanged by a concurrent refresh. See Refreshing tokens for what invalidates one. Have a Host open /mighty/connect again.

Mighty API calls return HTTP 401 after a successful refresh

Most often, the Host who authorized the integration is no longer a Host. The token refresh doesn’t check the role, but every API call does, so have a current Host open /mighty/connect again. You also get a 401 if that account was banned, or if your Network’s plan no longer includes the Mighty API.

createPaymentPlanMembership returns “Only free plans are supported.”

The mapped plan charges a price in Mighty. Point the product at a free plan, since Shopify has already collected payment.

createMember returns “This person is already a member of this Network.”

The customer is already a member, but memberByEmail couldn’t return them. See the warning in Find or create the member. Look up the member in Network Admin, save their member ID against the Shopify customer, and replay the order.

Requests fail with a JSON parse error and HTTP 403

Your requests to api.mn.co are missing a User-Agent header, so bot protection returned an HTML page. Set one on every request.

Mighty API

Endpoint, request format, rate limits, and errors.

Authentication

OAuth flows, token refresh, and revocation.

OAuth Client Architectures

Where tokens should live for a server-side integration.

GraphQL Schema Explorer

Browse every query and mutation used in this guide.