Home / Learn / GTM Engineering / Stage 9: Integration-Led Growth

Stage 9 of 1220 min readUpdated 2026-10-07

Stage 9: Integration-Led Growth

Learn how integrations drive distribution: OAuth with PKCE, Slack and Salesforce marketplaces, Zapier, embedded iPaaS, co-selling, and listing optimization.

  • Build: Build a Slack app with OAuth install, encrypted token storage and an install-to-activation funnel, then write and test its marketplace listing.
  • Free, no signup

The short answer

Integration-led growth means your product gets found, trusted and kept because it connects to tools the buyer already uses. A GTM engineer owns the technical half: OAuth connections, marketplace listings and their review rules, automation-platform apps, and the data that shows which installs become revenue. This stage teaches the plumbing and each marketplace's rules.

Why this matters

The roadmap says: "Buyers trust their existing stack more than your cold email. Ecosystems are the ultimate distribution." Test it, do not repeat it.

  • Buyers avoid reps. Gartner's March 2026 press release is titled "67% of B2B buyers prefer a rep-free experience". Gartner's page returned HTTP 403 to automated fetching, so only the headline is checked here and the survey method is not. It supports self-serve discovery. It does not measure trust in an existing stack.
  • Partners lift win rates, unevenly. Crossbeam's network data (article dated November 8, 2024, data as of October 28, 2024) reports an 11.7% average win rate lift when partners are involved. The lift is 2.5% for 5 to 10 connected partners, 37.1% for 50 or more, and a 1% drop for enterprise firms in the 5 to 10 band. The page does not say whether lift means percentage points or a relative change. The data covers companies on the vendor's own network, so it is correlation, from a company that sells ecosystem tools.
  • Integrations and churn. Crossbeam's 2023 survey says integration users are 58% less likely to churn. The page gives no sample size or method. Engaged customers may simply be the ones who connect. Test it on your own cohorts.
  • Platforms control the gate. This part is true. Slack limits commercially distributed apps outside its Marketplace (apps created on or after May 29, 2025, and new installs of older ones) to 1 request per minute and 15 objects per request on conversations.history and conversations.replies.

Verdict: "ultimate distribution" is overstated. Ecosystems are one strong channel for products that sit beside a platform with a big installed base. No study found tests the "trust" claim directly. Every marketplace also sets minimums, so a startup with no customers cannot just list and wait.

Core concepts

Integration-led growth, defined

Integrations do three jobs. They are a distribution channel (a listing is a search result inside the tool the buyer lives in), a retention hook (a connected product costs more to remove) and a data source for the scoring and routing work from earlier stages. It is not partner marketing, and it is not a public API. An API is a door. An integration is a built, supported room behind it. Worked example: a sales team lives in Slack all day, so an app that answers /signal acme.com there gets used more than a dashboard in another tab.

ShapeYou buildExample
Automation-platform appTriggers and actions on Zapier or MakeA Zapier integration with 5 to 10 Zap templates
Native marketplace appAn OAuth app plus handlers inside one platformA Slack app with a slash command
Embedded integrationsAn in-product "Connect your tools" layerMerge, Paragon or Nango behind a settings page

OAuth 2.0 authorization code flow with PKCE

OAuth lets a user give your app limited access to their account elsewhere without sharing a password. The authorization code flow, shown with Slack:

StepWhoWhat happens
1Your appSends the browser to https://slack.com/oauth/v2/authorize with client_id, scope, redirect_uri and a random state
2UserReads the permission screen and clicks Allow
3ProviderRedirects to your redirect_uri with a short-lived code and your state
4Your appChecks state matches what it sent, which blocks forged callbacks
5Your appServer-side, POSTs code and client_secret to https://slack.com/api/oauth.v2.access
6ProviderReturns an access token (Slack bot tokens start xoxb-), plus bot_user_id, app_id, team, and authed_user if you asked for user scopes

Fields are from Slack's OAuth guide. The Marketplace rules require the state parameter.

PKCE (Proof Key for Code Exchange, pronounced "pixy" in the RFC) stops a thief who steals the code from swapping it for tokens. Your app makes a random code_verifier (43 to 128 characters), sends only its SHA-256 hash as the code_challenge in step 1, and sends the real verifier in step 5. The provider hashes and compares. RFC 7636 defines it; use method S256. A worked pair, which the test code reproduces:

ValueExample
code_verifierdBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challengeE9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

The 2025 OAuth security best current practice, RFC 9700 says public clients must use PKCE, recommends it for confidential clients like your server, requires exact redirect URI matching, and says not to use the implicit grant.

Support differs, so check each provider. Salesforce's web server flow help page did not load for automated fetching, so its PKCE support is unverified here. Zapier has built-in PKCE. HubSpot's quickstart and Slack's guide do not mention it. Make PKCE a per-provider switch. Slack's token rotation guide says Bolt handles Slack rotation for you, so the hand-written refresh code below is for providers like HubSpot.

Token storage and refresh

An access token is the key sent with each API call, and it expires. A refresh token gets a new one. Read expires_in instead of guessing. Worked example: a token issued at 10:00:00 with expires_in 1800 expires at 10:30:00, so refresh at 10:29:00 when the 60 second margin starts. A tenant is one customer account, such as one Slack workspace or one HubSpot account. HubSpot access tokens last "currently 30 minutes". Slack tokens with token rotation last 43,200 seconds (12 hours).

  1. Encrypt tokens at rest. Slack's security page says "Never place them in a public code repository or client-side code" and to make sure you are not logging user tokens. Keep the encryption key in an environment variable.
  2. Refresh before expiry, when about 60 seconds remain, not after a 401.
  3. Serialize refreshes per tenant. Slack says refresh tokens "are designed to be used once". Slack's docs say the used token is revoked after a short grace period and an app that reuses it "will fail on the second refresh". If two workers refresh together, one can spend a dead token and the customer's connection can break. Use a lock.
  4. Decide on rotation before launch. Slack says "Token rotation may not be turned off once it's turned on."
  5. Expect revocation. On uninstall, mark the tenant disconnected and prompt a reconnect. Do not retry forever.

Slack apps: Bolt, scopes and Marketplace review

Bolt for Python is Slack's framework. It checks the X-Slack-Signature header and rejects stale requests when you pass your signing secret, per Slack's verification guide. A scope is a named permission such as commands. Ask for the fewest you can justify. The guidelines list admin., identity., search:read, workflow.steps:execute and triggers:* as unsuitable. A slash command is a typed command such as /signal that Slack sends to your server as an HTTP request.

From the guidelines and review guide:

ItemRequirement
Real usageIneligible if installed on fewer than 10 active workspaces and under 10 weekly active users (active means used in the past 28 days, not sandboxes)
Preliminary reviewUp to 10 business days
Functional reviewUp to 10 weeks for new apps, 6 weeks for updates
Short description10 words or fewer
Image1600 x 1000 px, under 2 MB, jpg or png
SupportReply within 2 business days, with no extra account needed to reach you
Landing pagePublic web page with an "Add to Slack" button, post-install next steps and a privacy link
Securitystate on OAuth, TLS 1.2 or higher, signed-request verification
Not eligibleSign in with Slack only, message export or backup, LLM training on Slack data, crypto or NFT payments, coded workflows

You need real usage before you can list, so the first installs cannot come from the listing. They come from your customers, partners and a direct "Add to Slack" link.

Status, October 2026: Salesforce's April 17, 2026 press release says AppExchange, the Slack Marketplace and the Agentforce ecosystem are now one marketplace, AgentExchange (the Italian page was checked). The press release says 2,600+ Slack apps and agents are in it. Slack's developer docs still describe the review above, so follow them for Slack.

Salesforce: security review and AgentExchange

Salesforce's security review is the hardest gate here. What could be confirmed:

  • Two scans. Code Analyzer v5 is free in the Salesforce CLI. The Source Code Scanner (Checkmarx) runs through the Partner Security Portal. Salesforce retired its hosted dynamic scanner (Chimera) in June 2025, so you now run your own dynamic scan, for example with OWASP ZAP or Burp Suite, on external services your package calls. Source: Tekunda, a third party, because Salesforce's own docs returned HTTP 403 to automated fetching.
  • Fee. Paid solutions may carry a security review fee. The amount could not be verified, because Salesforce's docs returned HTTP 403, so confirm it before you plan a budget.
  • Auth. Salesforce has been moving new apps from Connected Apps to External Client Apps. The release timing is unverified, so check your org's release notes.

Free path: a Developer Edition org from developer.salesforce.com/signup. The page returned HTTP 403 to automated fetching, so its limits are unverified.

HubSpot App Marketplace

The listing requirements say: at least three active, unique installs in the past 30 days, counted as production accounts unaffiliated with your organization that show successful app activity; OAuth as the sole authorization method; initial review "within 10 business days" and no more than 60 days in total from shared feedback; one app submitted at a time; live URLs, each capped at 250 characters.

Since September 22, 2025, apps created or migrated to platform version 2025.2 are capped at 25 installs until listed. The restrictions page bars apps that mainly broker credentials or OAuth tokens or are developer-only infrastructure, and delists apps that get no install in a 180 day period (existing installs stay).

Zapier and Make

Zapier apps expose triggers (events), actions (writes) and searches that users wire to other apps without code. On the developer platform, building and publishing is free. A Zapier developer replies "in 1 week or less" after you submit, and an approved integration stays in Beta for 90 days. Zap templates are pre-built workflows, only for public integrations; Zapier recommends 5 to 10, with about 2 weeks of review. Private integrations can invite up to 100 users. Plans start free with 100 tasks a month (pricing).

Make is the visual-scenario alternative. Its pricing page lists Free at $0 for 1,000 credits a month, then Core $12, Pro $21 and Teams $38 a month on monthly billing, each with 10,000 credits. You build custom apps in the web editor or VS Code, per the Make developer docs. Publishing and verification rules are unverified.

Embedded iPaaS: Merge, Paragon and Nango

An embedded iPaaS (integration platform as a service) is a vendor's integration layer that you put inside your product so customers connect their own tools. You trade control and margin for speed.

  • Nango: free plan with 10 connections; pay-as-you-go at $50 a month (which includes $50 of credits) and $0.29 per connection; auth for 1,000+ APIs. Its repo says "fully open source", but the license is the Elastic License 2.0, which bars offering it as a hosted service and is not OSI-approved. Call it source-available.
  • Merge: a unified API. Launch links 3 production accounts free, then $650 a month for up to 10. Higher tiers are contract priced.
  • Paragon: billed on connected users, with a free Platform tier listed. Its site showed no ownership change on October 7, 2026, but no news search was possible, so treat that as unchecked.

These help you connect to many apps. They do not replace a native listing, where review and branding matter.

Co-selling and listing optimization

Co-selling means your rep and a platform's rep work one deal together. On AWS Marketplace, a buyer can spend a committed cloud budget on your software. AWS's fee page says, effective January 5, 2024: SaaS public offers 3%, server products 20%, private offers 3% under $1M, 2% from $1M to under $10M, 1.5% from $10M and on renewals, plus 0.5% for channel partner private offers.

Co-selling needs ecosystem data: which target accounts are also a partner's customers. Crossbeam, which lets two companies compare customer lists without exposing them, merged with Reveal in 2024. The combined company is called Crossbeam, and the announcement says over 25,000 companies had adopted either network. It gives no exact date or terms. Crossbeam's pricing page lists a free plan with 3 seats and a Connector plan at $4,800 a year. Without that tool, join two exchanged customer lists on domain:

SELECT o.account_name, o.amount, p.partner_contact
FROM open_opportunities o
JOIN partner_customers p ON lower(o.domain) = lower(p.domain)
ORDER BY o.amount DESC;

Worked example: if open_opportunities holds Acme at 40000 and Globex at 15000, and the partner's list holds acme.com, the query returns one row, Acme at 40000. That row is your co-sell target, so you ask the partner rep for an intro.

A funnel is the ordered steps users pass through, with some lost at each step. A listing is a landing page with a funnel: impression, view, install click, installed, activated (core action within 7 days). Illustrative numbers: 1,000 views, 120 install clicks (12%), 90 installs, 27 activated (30%). The weakest step is activation, so fix it first. Change one thing at a time: name, 10-word description, first screenshot. Write around the job the user does. Slack can delist abandoned or broken apps, so activation protects the listing too.

Key terms

The tools

ToolWhat it doesPricing modelNotes
Slack Bolt for PythonFramework for Slack apps with OAuth and signature checksFree to buildFree basic sandbox (1 month); standard sandbox lasts 6 months and needs a paid plan or a payment method. Marketplace now sits inside AgentExchange
Salesforce AgentExchangeMarketplace for Salesforce apps, Slack apps and agentsReview fee for paid solutions, amount unverifiedFree Developer Edition org. AppExchange is now AgentExchange. Confirm fees on current docs
HubSpot developer platformBuild and list apps in the HubSpot App MarketplaceFree to build and listFree developer test accounts with a 90 day trial. 25 install cap for 2025.2 apps until listed
Zapier Developer PlatformPublic or private Zapier integrations and Zap templatesFree to build and publishPrivate integrations work for testing with up to 100 users
MakeVisual automation with custom appsFree plan, then $12 to $38 a month (monthly billing)Billed in credits. Publishing rules unverified
NangoAuth, token refresh and API proxy for embedded integrationsFree plan; then from $50 a monthElastic License 2.0 source-available. README says self-hostable, limits unverified
MergeUnified API across software categories3 free production accounts; then $650 a monthContract tiers above Launch
ParagonEmbedded iPaaS with workflows and managed syncFree Platform tier; Pro by salesPriced by connected users
CrossbeamEcosystem data: account overlap with partnersFree plan (3 seats); Connector $4,800 a yearMerged with Reveal. Use a CSV join with no budget

Build the project

You will build "SignalPing", a Slack app that a workspace installs in one click and uses with /signal acme.com to get a score and a reason for an account. A manifest is a YAML file that describes a Slack app, so you can create it without clicking through forms. An HTTPS tunnel (for example cloudflared or ngrok) gives your laptop a public HTTPS address that Slack can reach. Fernet is the encryption recipe in Python's cryptography package. It stores installs securely, records usage events and reports an install-to-activation funnel. You will also write its marketplace listing copy and test it against the requirements above. A provider-neutral OAuth client with PKCE and safe refresh is included for HubSpot.

PieceChoice
App frameworkSlack Bolt for Python on Flask
Install storageBolt's SQLite installation store
Third-party tokensFernet-encrypted rows with a refresh lock
MetricsAn events table and one SQL query
Local hostingAny HTTPS tunnel to localhost
  1. Join the Slack Developer Program.
  2. Provision a free sandbox workspace.
  3. Start an HTTPS tunnel to port 3000 and copy its host name.
  4. Replace YOUR-HOST in the manifest below with that host name.
  5. Create a Slack app from the manifest, with the single bot scope commands.
  6. Set the client ID, client secret and signing secret as environment variables.
  7. Run app.py.
  8. Open /slack/install in a browser, click Add to Slack, and confirm a row in slack_installations.
  9. Run /signal acme.com in Slack and confirm you see an answer and an events row.
  10. Add oauth_pkce.py and connect it to a HubSpot developer test account.
  11. Run the funnel query and record installs, activated installs and the rate.
  12. Write the listing copy: a 10-word description, a privacy page and a support page.
display_information:
  name: SignalPing
  description: Account buying signals in Slack, one command.
features:
  bot_user:
    display_name: signalping
  slash_commands:
    - command: /signal
      url: https://YOUR-HOST/slack/events
      description: Show the latest buying signal for an account domain
      usage_hint: acme.com
oauth_config:
  redirect_urls:
    - https://YOUR-HOST/slack/oauth_redirect
  scopes:
    bot:
      - commands
settings:
  token_rotation_enabled: true
python -m venv .venv && source .venv/bin/activate
pip install slack-bolt flask requests cryptography
export SLACK_CLIENT_ID="paste-client-id"
export SLACK_CLIENT_SECRET="paste-client-secret"
export SLACK_SIGNING_SECRET="paste-signing-secret"
python app.py

app.py (tested with slack-bolt 1.30.0 and Flask 3.1: a signed request from an installed workspace returns the answer, a bad signature gets HTTP 401):

import os
import sqlite3
import time

from flask import Flask, request
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.oauth.installation_store.sqlite3 import SQLite3InstallationStore
from slack_sdk.oauth.state_store.sqlite3 import SQLite3OAuthStateStore

DB = os.environ.get("SIGNALPING_DB", "signalping.db")
CLIENT_ID = os.environ["SLACK_CLIENT_ID"]

installation_store = SQLite3InstallationStore(database=DB, client_id=CLIENT_ID)
state_store = SQLite3OAuthStateStore(database=DB, expiration_seconds=600)
installation_store.init()
state_store.init()
with sqlite3.connect(DB) as db:
    db.execute("CREATE TABLE IF NOT EXISTS events "
               "(team_id TEXT NOT NULL, name TEXT NOT NULL, ts INTEGER NOT NULL)")

app = App(
    signing_secret=os.environ["SLACK_SIGNING_SECRET"],
    oauth_settings=OAuthSettings(
        client_id=CLIENT_ID,
        client_secret=os.environ["SLACK_CLIENT_SECRET"],
        scopes=["commands"],
        installation_store=installation_store,
        state_store=state_store,
    ),
)

SIGNALS = {  # mock feed; in production this is a warehouse query
    "acme.com": {"score": 82, "reason": "Visited pricing 3 times this week"},
    "globex.io": {"score": 41, "reason": "New hire: VP Sales"},
}


def lookup(domain):
    return SIGNALS.get(domain.strip().lower())


@app.command("/signal")
def signal_command(ack, command, respond):
    ack()
    domain = command.get("text", "").strip()
    with sqlite3.connect(DB) as db:
        db.execute("INSERT INTO events VALUES (?, 'command_used', ?)",
                   (command["team_id"], int(time.time())))
    hit = lookup(domain)
    if not hit:
        respond(f"No signal found for `{domain}`. Try `/signal acme.com`.")
        return
    respond(f"*{domain}* score *{hit['score']}*\n{hit['reason']}")


flask_app = Flask(__name__)
handler = SlackRequestHandler(app)


@flask_app.route("/slack/events", methods=["POST"])
@flask_app.route("/slack/install", methods=["GET"])
@flask_app.route("/slack/oauth_redirect", methods=["GET"])
def slack_routes():
    return handler.handle(request)


if __name__ == "__main__":
    flask_app.run(port=3000)

oauth_pkce.py (the PKCE pair, URL builder and refresh path were tested with mocked HTTP; the pair reproduces the RFC 7636 example). Set HUBSPOT_CLIENT_ID, HUBSPOT_CLIENT_SECRET and HUBSPOT_REDIRECT_URI, and a TOKEN_KEY from Fernet.generate_key():

import base64
import hashlib
import os
import secrets
import sqlite3
import time
import urllib.parse

import requests
from cryptography.fernet import Fernet

HUBSPOT = {  # HubSpot's OAuth quickstart does not document PKCE, so it is off
    "authorize_url": "https://app.hubspot.com/oauth/authorize",
    "token_url": "https://api.hubapi.com/oauth/v3/token",
    "scopes": ["oauth", "crm.objects.contacts.read"],
    "pkce": False,
}


def pkce_pair():
    verifier = secrets.token_urlsafe(64)  # 86 characters, inside 43 to 128
    digest = hashlib.sha256(verifier.encode("ascii")).digest()
    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
    return verifier, challenge


def authorize_url(cfg, state, challenge=None):
    params = {
        "client_id": os.environ["HUBSPOT_CLIENT_ID"],
        "redirect_uri": os.environ["HUBSPOT_REDIRECT_URI"],
        "scope": " ".join(cfg["scopes"]),
        "state": state,
        "response_type": "code",
    }
    if cfg["pkce"] and challenge:
        params.update(code_challenge=challenge, code_challenge_method="S256")
    query = urllib.parse.urlencode(params, quote_via=urllib.parse.quote)
    return cfg["authorize_url"] + "?" + query


def token_request(cfg, **fields):
    fields["client_id"] = os.environ["HUBSPOT_CLIENT_ID"]
    fields["client_secret"] = os.environ["HUBSPOT_CLIENT_SECRET"]
    fields["redirect_uri"] = os.environ["HUBSPOT_REDIRECT_URI"]
    r = requests.post(cfg["token_url"], data=fields, timeout=15)
    r.raise_for_status()
    return r.json()


class TokenStore:
    """One encrypted row per tenant."""

    def __init__(self, path, key):
        self.db = sqlite3.connect(path, isolation_level=None, timeout=30)
        self.fernet = Fernet(key)
        self.db.execute(
            "CREATE TABLE IF NOT EXISTS tokens "
            "(tenant TEXT PRIMARY KEY, blob BLOB, expires_at REAL)"
        )

    def save(self, tenant, resp):
        pair = resp["access_token"] + "\n" + resp["refresh_token"]
        self.db.execute(
            "INSERT OR REPLACE INTO tokens VALUES (?, ?, ?)",
            (tenant, self.fernet.encrypt(pair.encode()),
             time.time() + int(resp["expires_in"])),
        )

    def load(self, tenant):
        row = self.db.execute(
            "SELECT blob, expires_at FROM tokens WHERE tenant=?", (tenant,)
        ).fetchone()
        if not row:
            return None
        access, refresh = self.fernet.decrypt(row[0]).decode().split("\n")
        return {"access": access, "refresh": refresh, "expires_at": row[1]}


def get_access_token(cfg, store, tenant, skew=60):
    """Refresh at most once per expiry. BEGIN IMMEDIATE is the lock, so two
    workers cannot spend the same one-time refresh token."""
    store.db.execute("BEGIN IMMEDIATE")
    try:
        tok = store.load(tenant)
        if tok is None:
            raise LookupError("tenant has not connected")
        if tok["expires_at"] - skew > time.time():
            return tok["access"]
        body = token_request(cfg, grant_type="refresh_token",
                             refresh_token=tok["refresh"])
        body.setdefault("refresh_token", tok["refresh"])
        store.save(tenant, body)
        return body["access_token"]
    finally:
        store.db.execute("COMMIT")

After the HubSpot callback, check that the returned state matches the one you sent. Then call token_request(HUBSPOT, grant_type="authorization_code", code=code), then store.save(str(response["hub_id"]), response). HubSpot's token response includes hub_id.

The funnel query counts a workspace as activated if it ran /signal within 7 days of first install. On three sample installs it returned 3 installs, 1 activated, 33.3 percent:

WITH installs AS (
  SELECT team_id, MIN(installed_at) AS installed_at
  FROM slack_installations
  GROUP BY team_id
),
activated AS (
  SELECT DISTINCT i.team_id
  FROM installs i
  JOIN events e
    ON e.team_id = i.team_id
   AND e.name = 'command_used'
   AND e.ts >= CAST(strftime('%s', i.installed_at) AS INTEGER)
   AND e.ts <= CAST(strftime('%s', i.installed_at, '+7 days') AS INTEGER)
)
SELECT
  (SELECT COUNT(*) FROM installs) AS installs,
  (SELECT COUNT(*) FROM activated) AS activated_7d,
  ROUND(100.0 * (SELECT COUNT(*) FROM activated)
        / NULLIF((SELECT COUNT(*) FROM installs), 0), 1) AS activation_pct;

No-budget path. Every piece is free. The Slack sandbox is free as a basic sandbox (archived after 1 month of inactivity). A standard sandbox lasts six months but needs a paid plan or a payment method on file. HubSpot developer test accounts are free with a 90 day trial of many enterprise features, and they expire after 90 days with no API calls. A Salesforce Developer Edition org is free. A private Zapier integration is free, but Zap templates need a public one. You cannot reach the Marketplace usage minimum of 10 active workspaces alone, so a real submission is out of scope. The listing copy is not.

What done looks like. A sandbox user installs the app, runs /signal, and sees an answer. The installation table holds a bot token that no log shows. The funnel query returns numbers. Your listing copy meets every row of the Slack table, and a stranger can say what the app does from your 10-word description.

Stretch goal. Add the HubSpot callback route with TokenStore, or build the Salesforce External Client App version of the flow with PKCE on.

Common mistakes

  • Building before checking marketplace minimums. Slack wants 10 active workspaces and HubSpot wants 3 unaffiliated installs, so plan how to find early installs.
  • Asking for broad scopes "just in case". Reviewers read the list, and Slack calls several scopes generally unsuitable.
  • Skipping the state check on the callback. An attacker could then attach their account to a victim's session.
  • Refreshing tokens from several workers at once. A one-time refresh token means the loser can break the connection.
  • Treating approval as the finish. Slack can delist apps with broken pages or no active use, and HubSpot delists apps with no installs in 180 days.
  • Quoting vendor ecosystem statistics as proof. "58% less likely to churn" and "11.7% win rate lift" come from sellers of ecosystem tools with unclear method.
  • Counting installs as success. Count activated installs, then pipeline and retention.

Check yourself

1. Why does the OAuth callback compare the returned state to the value your app sent?

2. What does PKCE add to the authorization code flow?

3. Two workers both refresh the same expiring token with the same one-time refresh token. What does Slack's documentation say about reusing it?

4. A founder says "integrations cut churn by 58%, so fund three integrations". What is the best response?

5. Your Slack app is active in 2 workspaces with 5 weekly active users and you want a Marketplace listing. What blocks you?

6. You sell an embedded iPaaS wrapper and want a HubSpot Marketplace listing. Which HubSpot rule should worry you most?

How to learn this faster

Learn OAuth first, because every marketplace here sits on it. Write one authorize URL and one token request by hand with curl before you use an SDK. Then read one marketplace's requirement page end to end. Skip Salesforce Apex and any iPaaS until you can explain what it hides from you.

Deliberate-practice drill. Pick a listing for a product you know. Rewrite its description in 10 words, guess the scopes it needs, and name the funnel step where it loses the most users. Then check against public signals such as review counts.

  • Day 1: Read the verifier and challenge sections of RFC 7636 and reproduce the example pair in Python.
  • Day 2: Provision the Slack sandbox, create the app from the manifest, and get /slack/install working.
  • Day 3: Add /signal, the events table and the funnel query, and run them on test installs.
  • Day 4: Connect a HubSpot test account and force a refresh by editing expires_at.
  • Day 5: Write the listing copy and a one-page co-sell plan, then check both against the requirement tables.

Resources

Next module

Integrations give you trackable installs, accounts and activity. Stage 10, Dark Social and Community Attribution, covers the opposite problem: buying influence that happens in private groups, DMs and communities, where no install event exists to count.

Built the project and passed the quiz? Mark the stage done. Your progress stays in this browser only.

Talk to Claude instead of typing to it.

Ducktate turns what you say into clean text, on your Mac, and pastes it anywhere.

Get early access