> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useparagon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Salesforce

> Connect to your users' Salesforce accounts.

export const integration_0 = "Salesforce"

export const IntegrationsCompatibility = ({workflows = true, actionkit = false, proxy = true, managedSync = false, authType = "Basic Auth", integrationName: integrationNameProp, integrationSlug: integrationSlugProp}) => {
  const FEATURE_REQUEST_ENDPOINT = "https://agosnlmllwykglihfhiw.supabase.co/functions/v1/handle-vote";
  const FEATURE_REQUEST_HEADERS = {
    "Content-Type": "application/json",
    "Authorization": `Bearer sb_publishable_DlIzCjWe8NiqjZnLziegbg_P-w5L9X4`,
    'apikey': `sb_pubishable_DlIzCjWe8NiqjZnLziegbg_P-w5L9X4`
  };
  const SESSION_VOTE_PREFIX = "paragon_compat_vote:";
  const PURPLE = "rgb(102, 69, 230)";
  const slugifyFeature = label => label.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
  const integrationKeyFromPathname = pathname => {
    if (!pathname) return "";
    const match = pathname.match(/\/(?:resources\/)?integrations\/([^/?#]+)/);
    return match ? decodeURIComponent(match[1]).replace(/\/$/, "") : "";
  };
  const voteStorageKey = (integrationKey, featureKey) => `${SESSION_VOTE_PREFIX}${integrationKey}:${featureKey}`;
  const readVoteFromStorage = (integrationKey, featureKey) => {
    if (typeof sessionStorage === "undefined") return false;
    try {
      return sessionStorage.getItem(voteStorageKey(integrationKey, featureKey)) === "1";
    } catch {
      return false;
    }
  };
  const writeVoteToStorage = (integrationKey, featureKey) => {
    try {
      sessionStorage.setItem(voteStorageKey(integrationKey, featureKey), "1");
    } catch {}
  };
  const readPageTitleFallback = () => {
    if (typeof document === "undefined") return "";
    const h1 = document.querySelector("article h1") || document.querySelector("main h1") || document.querySelector('[class*="title"] h1') || document.querySelector("h1");
    const text = h1?.textContent?.trim();
    return text || "";
  };
  const getRuntimeConfig = () => {
    if (typeof window === "undefined") return null;
    return window.__PARAGON_FEATURE_REQUEST__ ?? null;
  };
  const resolveEndpoint = () => {
    const rt = getRuntimeConfig();
    if (rt?.endpoint) return String(rt.endpoint).trim();
    return String(FEATURE_REQUEST_ENDPOINT || "").trim();
  };
  const resolveHeaders = () => {
    const rt = getRuntimeConfig();
    const base = {
      ...FEATURE_REQUEST_HEADERS,
      ...rt?.headers && typeof rt.headers === "object" ? rt.headers : {}
    };
    return base;
  };
  const isValidEmail = s => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/).test(String(s).trim());
  const renderCompatCheckSvg = (sizePx = 18) => <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill={PURPLE} style={{
    width: `${sizePx}px`,
    height: `${sizePx}px`,
    flexShrink: 0,
    display: "block",
    verticalAlign: "middle",
    margin: "0 auto"
  }} aria-hidden>
      <path d="M320 576C178.6 576 64 461.4 64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576zM438 209.7C427.3 201.9 412.3 204.3 404.5 215L285.1 379.2L233 327.1C223.6 317.7 208.4 317.7 199.1 327.1C189.8 336.5 189.7 351.7 199.1 361L271.1 433C276.1 438 282.9 440.5 289.9 440C296.9 439.5 303.3 435.9 307.4 430.2L443.3 243.2C451.1 232.5 448.7 217.5 438 209.7z" />
    </svg>;
  const products = useMemo(() => [{
    label: "Managed Sync",
    value: managedSync
  }, {
    label: "ActionKit",
    value: actionkit
  }, {
    label: "Workflows",
    value: workflows
  }, {
    label: "Proxy API",
    value: proxy
  }, {
    label: "Auth Type",
    value: authType
  }], [actionkit, managedSync, workflows, proxy, authType]);
  const [mounted, setMounted] = useState(false);
  const [resolvedIntegrationKey, setResolvedIntegrationKey] = useState(() => integrationSlugProp?.trim() || "");
  const [resolvedDisplayName, setResolvedDisplayName] = useState(() => integrationNameProp?.trim() || "Integration");
  const [inlineFormOpen, setInlineFormOpen] = useState(false);
  const [formFeature, setFormFeature] = useState({
    featureLabel: "",
    featureKey: ""
  });
  const [email, setEmail] = useState("");
  const [description, setDescription] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [submitError, setSubmitError] = useState("");
  const [voteClickError, setVoteClickError] = useState("");
  const [voteLogging, setVoteLogging] = useState(false);
  const [loggingFeatureKey, setLoggingFeatureKey] = useState("");
  const [, bumpVoteUi] = useState(0);
  const inlineCardRef = useRef(null);
  useEffect(() => {
    setMounted(true);
    const path = typeof window !== "undefined" ? window.location.pathname : "";
    const fromPath = integrationSlugProp?.trim() || integrationKeyFromPathname(path);
    setResolvedIntegrationKey(fromPath);
    const fromDom = readPageTitleFallback();
    const name = integrationNameProp?.trim() || fromDom || (fromPath ? fromPath.replace(/-/g, " ") : "Integration");
    setResolvedDisplayName(name);
  }, [integrationNameProp, integrationSlugProp]);
  const dismissInlineForm = useCallback(() => {
    setInlineFormOpen(false);
    setSubmitError("");
  }, []);
  const handleRequestClick = useCallback(async ({featureLabel, featureKey}) => {
    setVoteClickError("");
    const integrationKey = resolvedIntegrationKey || integrationKeyFromPathname(typeof window !== "undefined" ? window.location.pathname : "");
    if (!integrationKey) {
      setVoteClickError("Could not determine integration. Set the integrationSlug prop on this page.");
      return;
    }
    if (readVoteFromStorage(integrationKey, featureKey)) {
      return;
    }
    setLoggingFeatureKey(featureKey);
    setVoteLogging(true);
    const votedAt = new Date().toISOString();
    const ctaPayload = {
      integration_key: integrationKey,
      integration_name: resolvedDisplayName,
      feature_key: featureKey,
      feature_label: featureLabel,
      vote_phase: "cta_click",
      voted_at: votedAt
    };
    try {
      const endpoint = resolveEndpoint();
      if (endpoint) {
        const res = await fetch(endpoint, {
          method: "POST",
          headers: resolveHeaders(),
          body: JSON.stringify(ctaPayload)
        });
        if (!res.ok) {
          const text = await res.text().catch(() => "");
          throw new Error(text || `Request failed (${res.status})`);
        }
      } else if (typeof console !== "undefined" && console.warn) {
        console.warn("[IntegrationsCompatibility] FEATURE_REQUEST_ENDPOINT is empty; vote not sent. Set endpoint or window.__PARAGON_FEATURE_REQUEST__.");
      }
      writeVoteToStorage(integrationKey, featureKey);
      bumpVoteUi(v => v + 1);
      setFormFeature({
        featureLabel,
        featureKey
      });
      setEmail("");
      setDescription("");
      setSubmitError("");
      setInlineFormOpen(true);
    } catch (err) {
      setVoteClickError(err instanceof Error ? err.message : "Could not log your vote. Please try again.");
    } finally {
      setVoteLogging(false);
      setLoggingFeatureKey("");
    }
  }, [resolvedIntegrationKey, resolvedDisplayName]);
  useEffect(() => {
    if (!inlineFormOpen) return undefined;
    const id = window.setTimeout(() => {
      inlineCardRef.current?.scrollIntoView?.({
        behavior: "smooth",
        block: "nearest"
      });
    }, 80);
    return () => window.clearTimeout(id);
  }, [inlineFormOpen]);
  const submitEnrichment = useCallback(async () => {
    const trimmed = email.trim();
    if (!isValidEmail(trimmed)) {
      setSubmitError("Please enter a valid work email.");
      return;
    }
    const integrationKey = resolvedIntegrationKey || integrationKeyFromPathname(typeof window !== "undefined" ? window.location.pathname : "");
    if (!integrationKey) {
      setSubmitError("Could not determine integration. Set the integrationSlug prop on this page.");
      return;
    }
    const {featureKey, featureLabel} = formFeature;
    const endpoint = resolveEndpoint();
    const enrichedAt = new Date().toISOString();
    const payload = {
      integration_key: integrationKey,
      integration_name: resolvedDisplayName,
      feature_key: featureKey,
      feature_label: featureLabel,
      vote_phase: "enrichment",
      email: trimmed,
      description: description.trim() || undefined,
      voted_at: enrichedAt,
      enriched_at: enrichedAt
    };
    setSubmitting(true);
    setSubmitError("");
    try {
      if (endpoint) {
        const res = await fetch(endpoint, {
          method: "POST",
          headers: resolveHeaders(),
          body: JSON.stringify(payload)
        });
        if (!res.ok) {
          const text = await res.text().catch(() => "");
          throw new Error(text || `Request failed (${res.status})`);
        }
      } else if (typeof console !== "undefined" && console.warn) {
        console.warn("[IntegrationsCompatibility] FEATURE_REQUEST_ENDPOINT is empty; enrichment not sent. Set endpoint or window.__PARAGON_FEATURE_REQUEST__.");
      }
      setInlineFormOpen(false);
      setEmail("");
      setDescription("");
    } catch (err) {
      setSubmitError(err instanceof Error ? err.message : "Something went wrong. Please try again.");
    } finally {
      setSubmitting(false);
    }
  }, [email, description, formFeature, resolvedDisplayName, resolvedIntegrationKey]);
  const renderCompatProductCell = ({product, integrationKey, onRequestClick, alignWide, voteLogging, loggingFeatureKey}) => {
    const isAuthType = product.label === "Auth Type";
    const href = typeof product.value === "string" && !isAuthType ? product.value : null;
    const featureKey = slugifyFeature(product.label);
    const alreadyVoted = !isAuthType && integrationKey && !href && !product.value ? readVoteFromStorage(integrationKey, featureKey) : false;
    const unsupported = !isAuthType && !href && !product.value;
    const cellInner = isAuthType ? product.value : href ? <div style={{
      display: "inline-flex",
      alignItems: "center",
      gap: "6px"
    }}>
        {renderCompatCheckSvg()}
        <a href={href} className="compat-doc-link">
          Docs
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 17l9.2-9.2M17 17V7H7" /></svg>
        </a>
      </div> : product.value ? renderCompatCheckSvg() : alreadyVoted ? <span style={{
      fontSize: "12px",
      fontWeight: 500,
      color: "rgba(55, 55, 58, 0.85)"
    }} className="compat-requested-text">Requested</span> : <button type="button" className="compat-pill" disabled={voteLogging && loggingFeatureKey === featureKey} onClick={() => onRequestClick({
      featureLabel: product.label,
      featureKey
    })} style={{
      cursor: voteLogging && loggingFeatureKey === featureKey ? "not-allowed" : "pointer",
      opacity: voteLogging && loggingFeatureKey === featureKey ? 0.65 : 1
    }}>
        <svg className="compat-pill-plus" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{
      color: PURPLE
    }} aria-hidden>
          <line x1="12" y1="5" x2="12" y2="19"></line>
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
        {voteLogging && loggingFeatureKey === featureKey ? "Submitting..." : "Request"}
      </button>;
    if (alignWide) {
      return <div style={{
        fontSize: "13px",
        padding: "8px 8px",
        minHeight: unsupported || alreadyVoted ? "40px" : undefined,
        flex: 1,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        textAlign: "center"
      }}>
          {cellInner}
        </div>;
    }
    return <span style={{
      fontSize: "13px",
      width: "80px",
      textAlign: "center",
      display: "inline-flex",
      justifyContent: "center"
    }}>
        {cellInner}
      </span>;
  };
  return <div style={{
    backgroundColor: "transparent",
    border: "0px solid rgba(225, 225, 229, 1)",
    borderRadius: "12px",
    padding: "0px 0px",
    overflow: "hidden"
  }}>
      <style>{`
        .compat-wide { display: flex; }
        .compat-narrow { display: none; }
        @media (max-width: 640px) {
          .compat-wide { display: none !important; }
          .compat-narrow { display: flex !important; }
        }
        
        .compat-doc-link {
          color: rgb(102, 69, 230);
          font-weight: 500;
          font-size: 13px;
          display: inline-flex;
          align-items: center;
          gap: 2px;
          text-decoration: none;
        }
        .compat-doc-link:hover {
          text-decoration: underline;
          text-underline-offset: 2px;
        }
        .dark .compat-doc-link {
          color: rgb(162, 140, 255);
        }

        .compat-pill {
          display: inline-flex;
          align-items: center;
          justify-content: center;
          gap: 4px;
          box-sizing: border-box;
          min-height: 32px;
          padding: 6px 12px;
          border-radius: 999px;
          border: 1px solid rgba(0, 0, 0, 0.12);
          background-color: rgba(250, 250, 249, 1);
          font-size: 12px;
          font-weight: 500;
          color: rgba(35, 35, 38, 1);
          font-family: inherit;
          line-height: 1.2;
          width: auto;
          text-decoration: none;
          margin: 0;
          transition: box-shadow 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
        }
        .compat-pill:hover:not(:disabled) {
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }
        
        a.compat-doc-link {
          text-decoration: underline !important;
          text-underline-offset: 2px !important;
          border-bottom: none !important;
          box-shadow: none !important;
          background: none !important;
        }
        a.compat-doc-link:hover {
          opacity: 0.8;
        }

        .dark .compat-pill {
          background-color: rgba(255, 255, 255, 0.05);
          border-color: rgba(255, 255, 255, 0.1);
          color: rgba(255, 255, 255, 0.85);
        }
        .dark .compat-pill:hover:not(:disabled) {
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
          background-color: rgba(255, 255, 255, 0.1);
        }
        .dark .compat-pill-plus {
          color: rgba(255, 255, 255, 0.85) !important;
        }
        .dark .compat-requested-text {
          color: rgba(255, 255, 255, 0.6) !important;
        }

        .compat-form-card {
          margin-top: 24px;
          width: 100%;
          box-sizing: border-box;
          background-color: #fff;
          border-radius: 16px;
          border: 1px solid rgba(0, 0, 0, 0.1);
          box-shadow: 0 8px 24px rgba(0,0,0,0.06);
          padding: 24px;
        }
        .dark .compat-form-card {
          background-color: #0f1114;
          border-color: rgba(255, 255, 255, 0.1);
          box-shadow: 0 8px 24px rgba(0,0,0,0.4);
        }

        .compat-form-close-btn {
          flex-shrink: 0;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          width: 36px;
          height: 36px;
          margin: 0;
          padding: 0;
          border-radius: 8px;
          background: #fff;
          color: rgba(35, 35, 38, 0.75);
          font-size: 24px;
          line-height: 1;
          font-family: inherit;
          border: none;
        }
        .dark .compat-form-close-btn {
          background: rgba(255,255,255,0.05);
          color: rgba(255,255,255,0.7);
        }

        .compat-form-title {
          margin: 0 0 16px;
          font-size: 20px;
          font-weight: 700;
          color: #111;
        }
        .dark .compat-form-title {
          color: rgba(255, 255, 255, 0.95);
        }

        .compat-form-text {
          display: block;
          margin: 0 0 20px;
          font-size: 14px;
          color: rgba(55, 55, 58, 0.72);
          line-height: 1.5;
        }
        .dark .compat-form-text {
          color: rgba(255, 255, 255, 0.6);
        }

        .compat-form-label {
          display: flex;
          flex-direction: column;
          gap: 6px;
          font-size: 13px;
          font-weight: 500;
          color: #333;
        }
        .dark .compat-form-label {
          color: rgba(255, 255, 255, 0.85);
        }

        .compat-form-input-readonly {
          width: 100%;
          box-sizing: border-box;
          padding: 10px 12px;
          border-radius: 8px;
          border: 1px solid rgba(0, 0, 0, 0.08);
          background-color: rgba(250, 248, 245, 0.95);
          font-size: 14px;
          color: rgba(35, 35, 38, 0.95);
        }
        .dark .compat-form-input-readonly {
          background-color: rgba(255, 255, 255, 0.05);
          border-color: rgba(255, 255, 255, 0.1);
          color: rgba(255, 255, 255, 0.7);
        }

        .compat-form-input {
          width: 100%;
          box-sizing: border-box;
          padding: 10px 12px;
          border-radius: 8px;
          border: 1px solid rgba(0, 0, 0, 0.15);
          background-color: #fff;
          font-size: 14px;
          font-family: inherit;
          color: #111;
        }
        .dark .compat-form-input {
          background-color: rgba(255, 255, 255, 0.05);
          border-color: rgba(255, 255, 255, 0.15);
          color: rgba(255, 255, 255, 0.95);
        }

        .compat-form-submit {
          padding: 10px 16px;
          border-radius: 8px;
          border: 1px solid rgba(0, 0, 0, 0.15);
          background: #fff;
          font-size: 14px;
          font-weight: 700;
          font-family: inherit;
          color: #111;
        }
        .dark .compat-form-submit {
          background: rgba(255, 255, 255, 0.1);
          border-color: rgba(255, 255, 255, 0.2);
          color: rgba(255, 255, 255, 0.95);
        }
        .compat-form-tag {
          display: inline-flex;
          align-items: center;
          gap: 6px;
          padding: 6px 10px;
          border-radius: 8px;
          background-color: rgba(102, 69, 230, 0.12);
          border: 1px solid rgba(102, 69, 230, 0.35);
          color: rgb(102, 69, 230);
          font-size: 12px;
          font-weight: 600;
        }
        .dark .compat-form-tag {
          background-color: rgba(162, 140, 255, 0.15);
          border-color: rgba(162, 140, 255, 0.4);
          color: rgb(180, 160, 255);
        }
      `}</style>

      <div className="compat-wide" style={{
    flexDirection: "row",
    gap: "0px"
  }}>
        {products.map(f => <div key={f.label} style={{
    flex: 1,
    minWidth: 0,
    textAlign: "center",
    display: "flex",
    flexDirection: "column"
  }}>
            <div style={{
    fontSize: "13px",
    fontWeight: 500,
    padding: "6px 8px",
    borderBottom: "1px solid rgba(102, 69, 230, 0.15)",
    overflow: "hidden",
    textOverflow: "ellipsis",
    whiteSpace: "nowrap",
    textAlign: "center"
  }}>
              {f.label}
            </div>
            {renderCompatProductCell({
    product: f,
    integrationKey: mounted ? resolvedIntegrationKey : "",
    onRequestClick: handleRequestClick,
    alignWide: true,
    voteLogging,
    loggingFeatureKey
  })}
          </div>)}
      </div>

      <div className="compat-narrow" style={{
    flexDirection: "column",
    gap: "0px"
  }}>
        <div style={{
    display: "flex",
    justifyContent: "space-between",
    borderBottom: "1px solid rgba(102, 69, 230, 0.15)",
    padding: "6px 0"
  }}>
          <span style={{
    fontSize: "13px",
    fontWeight: 500
  }}>Product</span>
          <span style={{
    fontSize: "13px",
    fontWeight: 500,
    width: "80px",
    textAlign: "center"
  }}>Supported</span>
        </div>
        {products.map(f => <div key={f.label} style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    padding: "5px 0"
  }}>
            <span style={{
    fontSize: "13px"
  }}>{f.label}</span>
            {renderCompatProductCell({
    product: f,
    integrationKey: mounted ? resolvedIntegrationKey : "",
    onRequestClick: handleRequestClick,
    alignWide: false,
    voteLogging,
    loggingFeatureKey
  })}
          </div>)}
      </div>

      {voteClickError ? <p style={{
    margin: "16px 0 0",
    fontSize: "13px",
    color: "#b42318"
  }} role="alert">
          {voteClickError}
        </p> : null}

      {inlineFormOpen ? <div ref={inlineCardRef} role="region" aria-labelledby="compat-feature-request-title" className="compat-form-card">
          <div style={{
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    gap: "12px",
    marginBottom: "16px"
  }}>
            <div className="compat-form-tag">
              Feature request
            </div>
            <button type="button" onClick={dismissInlineForm} disabled={submitting} aria-label="Close feature request form" className="compat-form-close-btn" style={{
    cursor: submitting ? "not-allowed" : "pointer"
  }}>
              <span aria-hidden>×</span>
            </button>
          </div>
          <h2 id="compat-feature-request-title" className="compat-form-title">
            Request support for {formFeature.featureLabel}
          </h2>
          <p className="compat-form-text">
            Your vote has been recorded. Add your email and an optional note and we'll notify you when support is added.
          </p>

          <div style={{
    display: "flex",
    flexDirection: "column",
    gap: "16px"
  }}>
            <label className="compat-form-label">
              Integration
              <input type="text" readOnly value={resolvedDisplayName} className="compat-form-input-readonly" tabIndex={-1} />
            </label>
            <label className="compat-form-label">
              Feature
              <input type="text" readOnly value={formFeature.featureLabel} className="compat-form-input-readonly" tabIndex={-1} />
            </label>
            <label className="compat-form-label">
              Work email
              <input type="email" name="email" autoComplete="email" placeholder="you@company.com" value={email} onChange={e => setEmail(e.target.value)} className="compat-form-input" disabled={submitting} />
            </label>
            <label className="compat-form-label">
              Describe your use case (optional)
              <textarea placeholder="What would you build if this were supported?" value={description} onChange={e => setDescription(e.target.value)} rows={4} className="compat-form-input" style={{
    resize: "vertical",
    minHeight: "96px"
  }} disabled={submitting} />
            </label>
          </div>

          {submitError ? <p style={{
    margin: "14px 0 0",
    fontSize: "13px",
    color: "#b42318"
  }} role="alert">
              {submitError}
            </p> : null}

          <div style={{
    display: "flex",
    justifyContent: "flex-end",
    gap: "10px",
    marginTop: "22px",
    flexWrap: "wrap"
  }}>
            <button type="button" onClick={submitEnrichment} disabled={submitting} className="compat-form-submit" style={{
    cursor: submitting ? "not-allowed" : "pointer"
  }}>
              {submitting ? "Submitting…" : "Submit details"}
            </button>
          </div>
        </div> : null}
    </div>;
};

<IntegrationsCompatibility workflows={true} actionkit="/actionkit/integrations/salesforce" proxy={true} managedSync="/managed-sync/integrations/salesforce" authType="OAuth2" />

## Setup Guide

This guide walks you through creating a Salesforce External Client App (ECA) and packaging it as a Second-Generation Managed Package (2GP) for distribution to your users.

<Info>
  If you are currently using a Connected App, see [Migrating from a Connected App](#migrating-from-a-connected-app-to-an-external-client-app) for instructions on adapting to Salesforce's packaging requirements enforced starting in September 2025.
</Info>

### Setting Up Your Salesforce Environment

Before creating your app, you'll need to set up the required Salesforce orgs and enable packaging features.

* **Partner Business Org** (PBO, also referred to as the Dev Hub Org): This is a Salesforce org that is created when you sign up as a Salesforce Partner.
  * You will use this org to create your External Client App, link your Namespace, and publish your Package.
* **Developer Edition Org** (also called a Partner Developer Edition / PDE org if created from the Partner Business Org): This is a Salesforce org for development / testing purposes created from the Salesforce Developer Edition signup or the Environment Hub of your Partner Business Org.
  * You will use this org to create your Namespace only.

<Info>
  **Already have a Partner Business Org with Dev Hub enabled?** Skip to [Creating an External Client App](#creating-an-external-client-app).
</Info>

#### 1. Create a Partner Business Org

[Sign up](https://partners.salesforce.com/pdx/s/trial-org-request?language=en_US) for a trial Partner Business Org.

Within a few minutes of signing up, you will receive an email to set your Salesforce login (note that this is separate from the Partner Community page at `partners.salesforce.com`).

<Warning>
  **Note that the trial is temporary and subject to expiration.**

  While you can test your app end-to-end with the trial Partner Business Org, you can request an active Partner Business Org by logging a case via the Salesforce Partner Community (more information [here](https://help.salesforce.com/s/articleView?id=000380510\&type=1#:~:text=a%20Trial%20PBO.-,How%20to%20request%20a%20PBO%20benefit%3F,-Pre%2Drequisites%3A)). You will first need to enroll in the [AppExchange ISV program](https://www.salesforce.com/en-us/wp-content/uploads/sites/4/documents/partners/readiness/appexchange_isv_onboarding_guide.pdf), which can be done via the Partner Community.
</Warning>

#### 2. Enable Dev Hub and Second-Generation Packaging

In your Partner Business Org:

* Go to **Setup** and search for **Dev Hub** in the Quick Find box.
* Enable the following settings:
  * Enable **Dev Hub**
  * Enable **Unlocked Packages and Second-Generation Managed Packages**
  <Frame>
    <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-dev-hub.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=44bc9668dcb095c78561cd225b3f0e46" alt="" width="2740" height="1040" data-path="assets/salesforce-dev-hub.png" />
  </Frame>

<sub>For more details, see [Salesforce's Dev Hub documentation](https://developer.salesforce.com/docs/atlas.en-us.pkg2_dev.meta/pkg2_dev/sfdx_pkg_enable_devhub.htm).</sub>

#### 3. Create and Link a Namespace

Namespaces uniquely identify your package components in Salesforce. You'll create the namespace in a Developer Edition Org, then link it to your Partner Business Org.

<Steps>
  <Step title="Create a Developer Edition Org">
    If you don't already have a Developer Edition Org, you can create one from your Partner Business Org:

    1. Open the App Launcher and search for **Environment Hub**.
    2. From the Environment Hub, click **Create Org** and set the following details:
       * **Purpose**: Development
       * **Edition**: Partner Developer

    After creating the org, it will take a few minutes to receive your new login details. Your Developer Edition Org will use a separate domain name from your Partner Business Org.

    <Tip>
      We recommend using separate browser profiles for your Partner Business Org and Developer Edition Org to avoid confusion during setup.
    </Tip>
  </Step>

  <Step title="Create a Namespace">
    **In your Developer Edition Org**:

    1. Go to **Setup > Apps > Packaging > Package Manager**.
    2. Click **Edit** next to the Namespace Prefix field.
    3. Enter your desired namespace and click **Check Availability**, then **Review**.
    4. Click **Save**.

    <Frame>
      <img src="https://mintcdn.com/paragon/Ua16mzmXrWI7rHYX/assets/salesforce-package-manager.png?fit=max&auto=format&n=Ua16mzmXrWI7rHYX&q=85&s=11fab5b9b6928985f44e11f852538a3d" alt="" width="2730" height="728" data-path="assets/salesforce-package-manager.png" />
    </Frame>
  </Step>

  <Step title="Link the Namespace to Your Dev Hub">
    **In your Partner Business Org** (make sure to switch back to this login after creating the namespace):

    1. Open the App Launcher and search for **Namespace Registries**.
    2. Click **Link Namespace**.
    3. When prompted, log in with the Developer Edition Org credentials that created the namespace. The namespace you configured will be displayed after you log in and approve access.

    <Frame>
      <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-namespaces.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=3f912964619e2e066171aaff5d17e3db" alt="" width="2712" height="464" data-path="assets/salesforce-namespaces.png" />
    </Frame>

    <Tip>
      If the namespace doesn't appear in the list, switch from "Recently Viewed" to **All Namespaces** and try refreshing the page.
    </Tip>
  </Step>
</Steps>

### Creating an External Client App

With your environment set up, you can now create the External Client App (ECA) that will handle OAuth authentication for your integration.

<Steps>
  <Step>
    In your Partner Business Org, go to **Setup** and navigate to **Platform Tools > Apps > External Client Apps > External Client App Manager**.

    Click **New External Client App**.

    <Frame>
      <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-new-eca.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=9baca1c9761acd7d14cfae84fcff4614" alt="" width="2772" height="964" data-path="assets/salesforce-new-eca.png" />
    </Frame>
  </Step>

  <Step>
    Fill out the details for your app's name as it will appear to your users during the connection process.

    Make sure to configure the following settings:

    | Setting                         | Value    |
    | ------------------------------- | -------- |
    | **Distribution State**          | Packaged |
    | **API (Enable OAuth Settings)** | Enabled  |
  </Step>

  <Step>
    Under **OAuth Settings**, configure:

    **Callback URL:**

    ```
    https://passport.useparagon.com/oauth
    ```

    <Note>For on-premise installations, use `https://passport.[Your Base URL]/oauth`</Note>

    **OAuth Scopes:** Select the scopes required for your integration. By default, Paragon requests:

    * Access the identity URL service
    * Manage user data via APIs
    * Perform requests at any time

    **Flow Enablement**: Check the box for **Enable Authorization Code and Credentials Flow**. Leave the box for **Require user credentials in the POST body** unchecked.

    <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-flow-enablement.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=5dc7484603d3f1e7cfcc4928bcf365b8" style={{ width: '400px' }} width="1362" height="520" data-path="assets/salesforce-flow-enablement.png" />

    * **Security**: Uncheck the box for **Require Proof Key for Code Exchange (PKCE) Extension for Supported Authorization Flows**.
          <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-oauth-security.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=5c99f8297504d436c2761f2b0b69dcbf" style={{ width: '400px' }} width="1360" height="454" data-path="assets/salesforce-oauth-security.png" />
  </Step>

  <Step>
    Click **Save** to create your External Client App.

    See the next section to get your Consumer Key and Consumer Secret from the External Client App and add them to Paragon.
  </Step>
</Steps>

#### Add Your Salesforce App to Paragon

After creating your External Client App, you'll need to enter it Consumer Key and Consumer Secret of your External Client App into Paragon.

<Steps>
  <Step title="Get Your Consumer Key and Consumer Secret from Salesforce">
    In your Partner Business Org, go to **Setup > Platform Tools > Apps > External Client Apps > External Client App Manager** and view your External Client App.

    1. Expand **OAuth Settings**.
    2. Click **Consumer Key and Secret**.
           <Frame>
             <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-consumer-key.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=96bb9b4ef58696c84d2cba34ca53e9df" alt="" width="2184" height="2310" data-path="assets/salesforce-consumer-key.png" />
           </Frame>
    3. Copy the **Consumer Key** and **Consumer Secret**.
  </Step>

  <Step title="Add your Consumer Key and Consumer Secret to Paragon">
    Now, go to the Paragon dashboard to add the Consumer Key and Secret:

    1. Navigate to your Salesforce integration and click **Configure** under the App Configuration section.
    2. Fill in your credentials:

           <Frame>
             <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-install-url.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=d42e3c5372735e351f6c3e1bb9e04302" alt="" width="2748" height="1676" data-path="assets/salesforce-install-url.png" />
           </Frame>

       | Field                              | Value                                                                                                                                                                                  |
       | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
       | **Consumer Key**                   | The Consumer Key you copied from your External Client App                                                                                                                              |
       | **Consumer Secret**                | The Consumer Secret you copied from your External Client App                                                                                                                           |
       | **Salesforce Package Install URL** | Leave blank for now. In the next section, you will package your app and get an Install URL to provide here.                                                                            |
       | **Permissions**                    | Add any additional permissions you included beyond the defaults from your External Client App. The default scopes will provide the same level of access that the authorizing user has. |
  </Step>
</Steps>

### Packaging Your App

To distribute your External Client App to users, you'll create a Second-Generation Managed Package (2GP) using the Salesforce CLI.

<Steps>
  <Step title="Install the Salesforce CLI">
    Install the Salesforce CLI using npm:

    ```bash theme={null}
    npm install -g @salesforce/cli
    ```

    Or see [Salesforce's CLI installation guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm) for other installation methods.
  </Step>

  <Step title="Create a Salesforce Project">
    Create a new Salesforce DX project:

    ```bash theme={null}
    sf project generate --name YourProjectName
    ```
  </Step>

  <Step title="Authorize Your Dev Hub">
    From your project directory, authenticate with your Partner Business Org:

    ```bash theme={null}
    sf org login web --set-default-dev-hub --alias DevHub
    ```

    This will open a browser window for you to log in.
  </Step>

  <Step title="Configure the Namespace">
    Open `sfdx-project.json` in your project and set the `namespace` field to the namespace you created in the previous step:

    ```json theme={null}
    {
      "packageDirectories": [
        {
          "path": "force-app",
          "default": true
        }
      ],
      "namespace": "YOUR_NAMESPACE",
      "sfdcLoginUrl": "https://login.salesforce.com",
      "sourceApiVersion": "59.0"
    }
    ```
  </Step>

  <Step title="Create the Package">
    Create a managed package for your app:

    ```bash theme={null}
    sf package create \
      --name "Your App Display Name" \
      --package-type Managed \
      --path force-app \
      --target-dev-hub DevHub \
      --error-notification-username your-email@example.com
    ```
  </Step>

  <Step title="Retrieve External Client App Metadata">
    Pull your ECA configuration into the project:

    ```bash theme={null}
    sf project retrieve start --metadata ExternalClientApplication ExtlClntAppOauthSettings --target-org DevHub
    ```

    After running these commands, you'll see new files in:

    * `force-app/main/default/externalClientApps/`
    * `force-app/main/default/extlClntAppOauthSettings/`

    <Info>
      If you have multiple External Client Apps and only want to package specific ones, delete the XML files for apps you don't want to include.
    </Info>
  </Step>

  <Step title="Create and Promote a Package Version">
    Create a new package version:

    ```bash theme={null}
    sf package version create \
      --package "Your App Display Name" \
      --installation-key-bypass \
      --wait 15 \
      --code-coverage
    ```

    This command will take a few minutes. Once complete, you'll receive a **Subscriber Package Version ID**. Save this ID for later use.

    Promote the version to released status:

    ```bash Replace YOUR_SUBSCRIBER_PACKAGE_VERSION_ID with your Subscriber Package Version ID: theme={null}
    sf package version promote --package YOUR_SUBSCRIBER_PACKAGE_VERSION_ID --no-prompt
    ```
  </Step>

  <Step title="Get Your Installation Link">
    Using the Subscriber Package Version ID, you can now construct the installation URL for your package at the version you promoted in the last step.

    The installation URL is constructed as follows (for Sandbox accounts, replace `login.salesforce.com` with `test.salesforce.com`):

    ```text Replace YOUR_SUBSCRIBER_PACKAGE_VERSION_ID with your Subscriber Package Version ID: theme={null}
    https://login.salesforce.com/packaging/installPackage.apexp?p0=YOUR_SUBSCRIBER_PACKAGE_VERSION_ID
    ```

    The Connect Portal can automatically guide users through the package installation process prior to connecting their Salesforce account.

    Add your **Salesforce Package Install URL** to the App Configuration section of your Salesforce integration to enable this:

    <Frame>
      <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-install-url.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=d42e3c5372735e351f6c3e1bb9e04302" alt="" width="2748" height="1676" data-path="assets/salesforce-install-url.png" />
    </Frame>

    In the Connect Portal, the installation URL will automatically be set to the one matching the account type the user selects.

    <Warning>
      **When changing OAuth scopes in the future**, you must:

      * Retrieve the ECA metadata (Step 6)
      * Create a new package version (Step 7, *this will update the Subscriber Package Version ID*)
      * Update the App Install URL in Paragon (Step 8)

      Users will need to reinstall your package and reconnect their Salesforce account to receive the updated scopes.
    </Warning>
  </Step>
</Steps>

### Testing Your Setup

To validate your setup, ensure that all details are correctly configured in the App Configuration section of your Salesforce integration:

* [Consumer Key and Consumer Secret](#add-your-salesforce-app-to-paragon)
* [Salesforce Package Install URL](#packaging-your-app:~:text=Get%20Your%20Installation%20Link)

Then, open the Connect Portal for Salesforce inside of your app (or click Test Connect Portal in the Salesforce integration) to try a connection flow.

<Warning>
  **You cannot use the same Developer Edition Org that you used to create a Namespace to test package installation.** Use your Partner Business Org or a separate Developer Edition Org to test package installation.
</Warning>

1. The Connect Portal will first prompt you to select a Salesforce Account Type.

   <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-account-type.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=89811a62ebb7ec8120047c6fca6faab1" style={{ width: '400px' }} width="1618" height="1084" data-path="assets/salesforce-account-type.png" />

   * If you are using a Developer Edition Org, select **Production** option. You can only select **Sandbox** for orgs that login with `test.salesforce.com` instead of `login.salesforce.com`.

2. Next, the Connect Portal will provide instructions for installing your package. Click **Install** to begin the process.

   <img src="https://mintcdn.com/paragon/Ua16mzmXrWI7rHYX/assets/salesforce-package-installation.png?fit=max&auto=format&n=Ua16mzmXrWI7rHYX&q=85&s=8bff00ee20acd231acd78724cc4dbe2d" style={{ width: '400px' }} width="1616" height="1644" data-path="assets/salesforce-package-installation.png" />

3. After following the prompts to install the package, click **I've installed the package** to continue.

4. Finally, the Connect Portal will prompt you to connect your Salesforce account. Click **Connect** to continue and authorize your app via an OAuth flow.

## Connecting to Salesforce

<Info>
  **Note**: Salesforce API access is limited to Enterprise, Unlimited, and Performance Editions of Salesforce, as well as Professional Editions with the paid API add-on. [See Salesforce documentation](https://help.salesforce.com/s/articleView?id=000326486\&type=1) for more information.
</Info>

Once your users have connected their Salesforce account, you can use the Paragon SDK to access the Salesforce API on behalf of connected users.

See the Salesforce [REST API documentation](https://developer.salesforce.com/docs/) for their full API reference.

Any Salesforce API endpoints can be accessed with the Paragon SDK as shown in this example.

```javascript theme={null}
// You can find your project ID in the Overview tab of any Integration

// Authenticate the user
paragon.authenticate(<ProjectId>, <UserToken>);
          
// Create an Account
await paragon.request("salesforce", "/sobjects/Account", { 
  method: "POST",
  body: { "Name": "Express Logistics and Transport" }
});


// Query Accounts
await paragon.request("salesforce", "/query/Account", { 
  method: "GET",
  body: { "q": "SELECT+name+from+Account" }
});
  
```

## Building Salesforce workflows

Once your Salesforce account is connected, you can add steps to perform the following actions:

* Create Record
* Update Record
* Get Record by ID
* Search Records
* Search Records by SOQL Query
* Deploy Custom Field
* Deploy Custom Object
* Get Records by List View

You can also use the [Salesforce Request](/workflows/requests#making-integration-requests) step to access any of Salesforce's API endpoints without the authentication piece.

When creating or updating records in Salesforce, you can reference data from previous steps by typing `{{` to invoke the variable menu.

<Frame>
  <img src="https://mintcdn.com/paragon/EuLlf5VxgsSnEq57/assets/Creating%20a%20Salesforce%20Lead%20in%20Paragon.png?fit=max&auto=format&n=EuLlf5VxgsSnEq57&q=85&s=1161d0267917f4333dbcafde8c86e718" alt="" width="1005" height="872" data-path="assets/Creating a Salesforce Lead in Paragon.png" />
</Frame>

## Working with Salesforce Custom Objects and Custom Fields

It's common that different Salesforce instances may be configured with different Custom Objects or Custom Fields. Paragon provides the ability for your users to choose their own Custom Object mapping. You can also deploy Custom Objects or Custom Fields that may be specific to your application to your users' Salesforce instances.

<Frame>
  <img src="https://mintcdn.com/paragon/fpLCYjVDKL_JwtxK/assets/choosing-a-salesforce-custom-object-mapping-in-paragon.gif?s=f90edbaca5077b5328c107bf3a6277ac" alt="" width="948" height="655" data-path="assets/choosing-a-salesforce-custom-object-mapping-in-paragon.gif" />
</Frame>

### Custom Object Mapping

To allow your users to choose their own Custom Object Mapping, add the **Custom Object Mapping** user setting in your Connect Portal Editor. You should give this setting a descriptive user-setting name, for example, if you're mapping contacts from your app to Salesforce, you might call this "Map Contacts to this object".

<Frame>
  <img src="https://mintcdn.com/paragon/cqJFSKyZXJDp3p3z/assets/Screen%20Shot%202021-05-19%20at%2010.34.55%20AM.png?fit=max&auto=format&n=cqJFSKyZXJDp3p3z&q=85&s=95b33061f8fc999153a5b86d4cd63016" alt="" width="2334" height="1880" data-path="assets/Screen Shot 2021-05-19 at 10.34.55 AM.png" />
</Frame>

Below, **add a label for each object property that should be mapped from your app to a Salesforce object field**. In our contacts example, you might add labels for "First Name", "Last Name", and "Email".

In your Connect Portal, your users will be prompted to select an object from their Salesforce instance when enabling this workflow. For each of the object properties you labeled, your users will be prompted to select which object field that property should be mapped to.

In the workflow editor, you can now access your user's custom object mapping in the variable menu. For example:

<Frame>
  <img src="https://mintcdn.com/paragon/lKoo2WTuuveEy7P1/assets/accessing-custom-object-mapping-in-the-variable-menu-in-paragon.gif?s=bf64e5e13d0624ba968b2aff63a6e73c" alt="" width="699" height="956" data-path="assets/accessing-custom-object-mapping-in-the-variable-menu-in-paragon.gif" />
</Frame>

### Deploying Custom Objects or Custom Fields

To deploy Custom Objects or Custom Fields to your users' Salesforce instances, use the **Deploy Custom Object** or **Deploy Custom Field** Salesforce workflow actions.

Both **Deploy Custom Object** and **Deploy Custom Field** will upsert by default, for example, if you run **Deploy Custom Field** on an existing Custom Field, it will apply any relevant updates to the Custom Field.

<Frame>
  <img src="https://mintcdn.com/paragon/fpLCYjVDKL_JwtxK/assets/deploying-custom-objects-and-custom-fields-to-salesforce-in-paragon.gif?s=91ee400547abd6670cdda1868e3e26a1" alt="" width="1143" height="846" data-path="assets/deploying-custom-objects-and-custom-fields-to-salesforce-in-paragon.gif" />
</Frame>

### Referencing Custom Objects and Fields

You can reference any Salesforce object (provided that the object exists in your user's Salesforce instance) when creating, updating, or searching records by selecting **Custom Object** under **Record Type** and referencing the **Object Name**.

Similarly, you can reference any Salesforce field (provided that the field exists on the target object) when creating or updating records.

The Object Name for Custom Objects and the Field Name for Custom Fields are both suffixed with `__c`.

<Frame>
  <img src="https://mintcdn.com/paragon/cqJFSKyZXJDp3p3z/assets/Screen%20Shot%202021-05-19%20at%201.07.08%20PM.png?fit=max&auto=format&n=cqJFSKyZXJDp3p3z&q=85&s=122ac0cc4213ea8d11522c2d9c9198f1" alt="" width="1272" height="1306" data-path="assets/Screen Shot 2021-05-19 at 1.07.08 PM.png" />
</Frame>

## Using Webhook Triggers

Webhook triggers can be used to run workflows based on events in your users' Salesforce account. For example, you might want to trigger a workflow whenever new contacts are created in Salesforce to sync your users' Salesforce contacts to your application in real-time.

<Frame>
  <img src="https://mintcdn.com/paragon/cqJFSKyZXJDp3p3z/assets/Salesforce%20Triggers%20in%20Paragon%20Connect.png?fit=max&auto=format&n=cqJFSKyZXJDp3p3z&q=85&s=305a6a57db7bcdc9782ca669530e8be9" alt="" width="1772" height="1546" data-path="assets/Salesforce Triggers in Paragon Connect.png" />
</Frame>

You can find the full list of Webhook Triggers for Salesforce below:

* **New Record**
* **Record Deleted**
* **Record Updated**
* **Campaign Member Added**
* **Campaign Opportunity Added**

## User-Configured OAuth

{integration_0} supports [User-Configured OAuth](/resources/user-configured-oauth), which lets your end users connect their own OAuth app instead of the one you've configured for your Paragon integration. This is useful when your customers want to use their own OAuth app for security or scope customization.

To enable this, pass `"user-configured-oauth"` as one of the values in the `accountType` array when launching the install flow:

```js theme={null}
// Allow user-configured OAuth as an option (defaults to hidden)
paragon.installIntegration("salesforce", {
  accountType: paragon
    .getAccountTypeOptions("salesforce")
    .map((option) => option.id),
});

// Only allow user-configured OAuth accounts to connect
paragon.installIntegration("salesforce", {
  accountType: ["user-configured-oauth"],
});
```

See the [User-Configured OAuth reference](/resources/user-configured-oauth) for full details on how it works and what your users will be prompted for.

## Deprecation of Connected Apps (September 2025)

<Warning>
  The following section only applies to customers who built a Salesforce integration before September 2025. If you are following the [Setup Guide](#setup-guide) for the first time and already have an External Client App and Package, you can ignore everything in this section.
</Warning>

Starting in September 2025, Salesforce began restricting Connected Apps that are not installed (via a package) on a Salesforce organization from being authorized. Connected Apps are deprecated and should be replaced with External Client Apps as explained in the Setup Guide.

This change can result in errors for new users of your Salesforce integration (or existing users trying to re-authorize). The error appears in the OAuth flow as:

> OAUTH\_APPROVAL\_ERROR\_GENERIC: An unexpected error has occurred during authentication. Please try again.

See the [official Salesforce announcement](https://help.salesforce.com/s/articleView?id=005132365\&type=1) for more details.

### For Existing Users of Your Connected App

For existing users of your Salesforce Connected App (users that have connected your before September 2025), **connections will continue to work as expected unless they are prompted to re-authorize at some point in the future.**

If they are prompted to re-authorize, they will see the error referenced above.

To prevent this from happening, instruct your users to contact their Salesforce admin to install your app with the following instructions:

<Accordion title="Installing the Connected App (for existing users)">
  1. In Salesforce, visit **Setup → Apps → Connected Apps → Connected Apps OAuth Usage**:

       <Frame>
         <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-connected-apps-oauth-usage.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=daf445e7dd6209ec308f084856821fc0" alt="" width="1361" height="362" data-path="assets/salesforce-connected-apps-oauth-usage.png" />
       </Frame>

  2. Click **Install** next to your app's name.

     If the Salesforce admin cannot find your app in the list, they will not be able to use your Connected App. See the next section for more details.
</Accordion>

### For New Users of Your Connected App

New users of your Salesforce integration will see an error when authorizing your Connected App through the Connect Portal until they have installed your app.

Instead, follow the [Setup Guide](#setup-guide) from the beginning to set up a new External Client App and package it in a Second-Generation Managed Package (2GP) for new users to connect.

Paragon provides an option to [migrate your Connected App credentials](#migrating-from-a-connected-app-to-an-external-client-app) to a new External Client App without disrupting existing connections (more in the next section).

#### Alternative: Enable Uninstalled App Permission

As an alternative to packaging, you can instruct Salesforce admins to temporarily bypass the new policy and allow your app to authorize without being installed with a package.

<Accordion title="How to Enable Uninstalled App Permission">
  Provide the following instructions to new users of your Salesforce integration:

  1. **Create a Permission Set**
     * Ask an admin of your Salesforce organization to create or edit a permission set for at least one user that should be able to authorize.
     * Click **Setup → Permission Sets**.
     * Click **New** or select an existing permission set.
     * Name your permission set (e.g., "\[Your App's Name] Integration Access").

  2. **Enable Uninstalled App Permission**
     * In the permission set, click **System Permissions**.
     * Click **Edit**.
     * Search for and select **Approve Uninstalled Connected Apps**.
     * Click **Save**.

  3. **Assign Permission Set to Users**
     * Click **Manage Assignments**.
     * Click **Add Assignments**.
     * Select at least one user who needs Salesforce integration access.
     * Click **Assign**.

  4. **Authorize Your Account**
     * Once created, visit the Salesforce integration settings in your app to connect your Salesforce account (the account that was added to the permission set above).

  5. **Install the Connected App**
     * After authorization, ask your Salesforce admin to visit **Setup → Apps → Connected Apps → Connected Apps OAuth Usage**:

         <Frame>
           <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-connected-apps-oauth-usage.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=daf445e7dd6209ec308f084856821fc0" alt="" width="1361" height="362" data-path="assets/salesforce-connected-apps-oauth-usage.png" />
         </Frame>

     * Click **Install** next to your app's name.

  6. **Remove Permission Set (Optional)**
     * Once the Connected App is installed, the Permission Set created above can be removed without impact to your connection.

  After these instructions are followed, both the access/refresh token pair issued during Step 4 and other token pairs issued in subsequent authorizations by other users will work as expected.
</Accordion>

### Migrating from a Connected App to an External Client App

To migrate from using a Connected App to using an External Client App, navigate to your Salesforce integration in the Paragon dashboard and click **Configure**.

If you do not have a Package Install URL set, you will see an option to migrate your app:

<Frame caption="Click the 'migrate to a new Salesforce app' link.">
  <img src="https://mintcdn.com/paragon/iVBZWRnCPNxEt85V/assets/salesforce-migrate-app.png?fit=max&auto=format&n=iVBZWRnCPNxEt85V&q=85&s=e984642b021371866e0ed8f014fcbec0" alt="" width="1254" height="792" data-path="assets/salesforce-migrate-app.png" />
</Frame>

Next, you will be prompted to enter new details representing the External Client App you are migrating to. Follow the [Setup Guide](#setup-guide) from the beginning to get all of the details required for this step.

Once you have entered the details, click **Next** to confirm the migration to an External Client App. After the migration is complete:

* The Connected App configuration will become read-only, and any changes to the App Configuration will apply to the new External Client App.
* Your existing users will remain connected to your old Connected App, and their requests, workflows, or syncs will not be disrupted.
* New users of your Salesforce integration will be prompted to install the new External Client App, using the package installation process described in [Testing Your Setup](#testing-your-setup).
