> ## 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.

# Shopify

> Connect to your users' Shopify accounts.

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/shopify" proxy={true} managedSync={false} authType="OAuth2" />

You can find your Shopify application credentials by visiting your [Shopify Partner Dashboard](https://partners.shopify.com/organizations).

You'll need the following information to set up your Shopify App with Paragon:

* Client ID
* Client Secret
* Scopes Requested
* Shopify Developer account, also known as a Shopify Partner account. You can create one [here](https://developers.shopify.com/).
* Shopify development store. Learn more about creating a development store [here](https://help.shopify.com/en/partners/dashboard/managing-stores/development-stores#create-a-development-store-for-testing-apps-or-themes).
* Shopify application. Learn more about creating a Shopify application [here](https://shopify.dev/tutorials/authenticate-a-public-app-with-oauth#generate-credentials-from-your-partner-dashboard).

### Add the Redirect URL to your Shopify app

Paragon provides a redirect URL to send information to your Shopify app. To add the redirect URL to your Shopify app:

1. Log in to your Shopify [Partner Dashboard](https://partners.shopify.com/current/resources) and select your app.

2. Navigate to **App setup > URLs > Allowed redirection URL(s)**

3. Add your app's Initial Redirect URL to "**App URL**". While testing your integration, you can use your app's root URL. Once you [set up an Initial Redirect](/resources/integrations/shopify#initial-redirect) to go live, you will need to change this to the URL of your Initial Redirect.

4. Add your app's Redirect Callback URL to "**Allowed redirection URL(s)**". While testing your integration, you can use `https://passport.useparagon.com/oauth`. Once you [set up a Redirect Callback](/resources/integrations/shopify#setting-up-redirect-pages-in-your-app) to go live, you will need to change this to the URL of your Redirect Callback.

5. Press the **Save** button at the top of the page to save your changes.

<Info>
  **Note:** You'll need a Shopify application to connect your application to Paragon. Learn more about creating a Shopify application [here](https://shopify.dev/tutorials/authenticate-a-public-app-with-oauth#generate-credentials-from-your-partner-dashboard).
</Info>

<Frame>
  <img src="https://mintcdn.com/paragon/jCM_Y_j0HttScr1R/assets/Enabling%20OAuth%20for%20your%20Shopify%20app.gif?s=83db3bed54faeea58ab56d42b5849a6a" alt="" width="889" height="650" data-path="assets/Enabling OAuth for your Shopify app.gif" />
</Frame>

### Add a development store to your Shopify app

1. Log in to your Shopify [Partner Dashboard](https://partners.shopify.com/current/resources).

2. Click **Apps** on the sidebar.

3. Select your Shopify application.

4. In the **Test your app** section, press the **Select store** button.

5. Choose the development store you'd like to connect to.

<Info>
  **Note:** You'll need to create a development store if you don't already have one. Learn more about creating a Shopify development store [here](https://help.shopify.com/en/partners/dashboard/managing-stores/development-stores#create-a-development-store-for-testing-apps-or-themes).
</Info>

### Add your Shopify app to Paragon

1. Select **Shopify** from the **Integrations Catalog**.

2. Under **Integrations > Connected Integrations > Shopify > App Configuration > Configure**, fill out your credentials from the end of [Step 1](/resources/integrations/shopify#1-add-the-redirect-url-to-your-shopify-app) in their respective sections:

* **Client ID:** Found under Apps > Client credentials > Client ID on your Shopify app page.
* **Client Secret:** Found under Apps > Client credentials > Client Secret on your Shopify app page.
* **Permissions:** Select the scopes you've requested for your application. For a list of recommended scopes, please view this integration within your Paragon dashboard. [View dashboard.](https://dashboard.useparagon.com) A complete list of Shopify's scopes is [here](https://shopify.dev/docs).

Press the purple "**Save Changes**" button to save your credentials.

<Info>
  **Note:** You should only add the scopes you've requested in your application page to Paragon.
</Info>

<Frame>
  <img src="https://mintcdn.com/paragon/fpLCYjVDKL_JwtxK/assets/configuring-shopify-client-credentials-in-paragon.png?fit=max&auto=format&n=fpLCYjVDKL_JwtxK&q=85&s=42dcc93c8efcc0354a9f03da4c82dfeb" alt="" width="1342" height="1156" data-path="assets/configuring-shopify-client-credentials-in-paragon.png" />
</Frame>

## Connecting to Shopify

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

See the Shopify [REST API documentation](https://shopify.dev/docs/admin-api/rest/reference) for their full API reference.

Any Shopify 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 Customer
await paragon.request("shopify", "/admin/api/2020-10/customers.json", { 
  method: "POST",
  body: { 
    "first_name": "John",
    "last_name": "Norman",
    "email": "example@example.com",
    "phone": "+16135551111",
    "note": "Creating customer for testing",
    "tags": ["tag1","tag2"]
   }
});


// Query Customers
await paragon.request("shopify", "/admin/api/2020-10/customers.json", { 
  method: "GET"
});
  
```

## Building Shopify workflows

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

* Get Customers
* Search Customers
* Create Customer
* Update Customer
* Get Orders
* Create Order
* Update Order
* Get Abandoned Carts
* Get Products
* Create Product
* Update Product

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

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

<Frame>
  <img src="https://mintcdn.com/paragon/PqlWbzgmbhNFByFv/assets/A%20Shopify%20Workflow%20in%20Paragon.png?fit=max&auto=format&n=PqlWbzgmbhNFByFv&q=85&s=0ace343ec117f28871943bfdc3fe37a8" alt="" width="1228" height="888" data-path="assets/A Shopify Workflow in Paragon.png" />
</Frame>

## Using Webhook Triggers

<Info>
  **Requirement for using Shopify triggers:** Configuring triggers for Shopify events that involve customer data requires you as the Shopify app owner to request access to protected data. Navigate to **Apps > Your App > API Access > Access Requests** to request access before using the Paragon Shopify trigger. [Learn more here.](https://shopify.dev/docs/apps/launch/protected-customer-data)
</Info>

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

<Frame>
  <img src="https://mintcdn.com/paragon/HSp5hB8tE4Z6e44m/assets/Shopify%20Webhook%20Triggers%20in%20Paragon%20Connect.png?fit=max&auto=format&n=HSp5hB8tE4Z6e44m&q=85&s=df8e711ce0a78c7b1b65c0c7f62f2a5d" alt="" width="976" height="788" data-path="assets/Shopify Webhook Triggers in Paragon Connect.png" />
</Frame>

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

* **New Order**
* **Order Updated**
* **New Customer**
* **Customer Updated**
* **New Product**
* **Product Updated**
* **Customer Data Request**
* **Customer Data Erasure Request**
* **Shop Data Erasure Request**

## Publishing your Shopify app

<Info>
  **Required for publishing:** In order to list your app on the Shopify App Store, you must implement the following additional features in your integration:

  * [Setting up Redirect Pages](/resources/integrations/shopify#setting-up-redirect-pages-in-your-app)
  * [Subscribing to mandatory privacy webhooks](/resources/integrations/shopify#subscribing-to-mandatory-privacy-webhooks)

  For more information, see [Shopify's documentation on publishing requirements](https://shopify.dev/docs/apps/store/requirements).
</Info>

### Setting up Redirect Pages in your app

Your Shopify integration requires two types of pages hosted in your application to support an installation flow that *begins* in the Shopify App Store (i.e., a user searches the Shopify App Store for your published app and clicks **Add app**).

Here is an annotated version of the [Shopify OAuth flow](https://shopify.dev/docs/apps/auth/oauth) diagram outlining what pages you will need to implement:

<Frame>
  <img src="https://mintcdn.com/paragon/HSp5hB8tE4Z6e44m/assets/Shopify%20OAuth%20Flow%20(1).png?fit=max&auto=format&n=HSp5hB8tE4Z6e44m&q=85&s=d6fe862370286a8b00393d2a3723dabc" alt="" width="3338" height="2925" data-path="assets/Shopify OAuth Flow (1).png" />
</Frame>

The pages you will need to implement include:

* **Initial Redirect**: This page will take in a `shop` query parameter and redirect to Shopify's OAuth flow.
* **Redirect Callback**: This page will receive the OAuth authorization code after the Shopify user grants consent and call `paragon.completeInstall` to save the user's account connection.

For an example implementation of the redirect pages using React (based on our Next.js sample app), see:

* [Example Initial Redirect](https://github.com/ethanlee16/paragon-connect-nextjs-example/blob/redirect-page/pages/integrations/index.js#L13-L20)
* [Example Redirect Callback](https://github.com/ethanlee16/paragon-connect-nextjs-example/blob/redirect-page/pages/integrations/shopify.js)

#### Initial Redirect

The Initial Redirect should be implemented as follows:

* Accept and read the query parameter `shop`. If the query parameter is present, redirect to the following URL to start the Shopify OAuth flow:

```
https://{shopQueryParam}/admin/oauth/authorize?client_id={SHOPIFY_CLIENT_ID}&redirect_uri={REDIRECT_CALLBACK_URL}&scope={SHOPIFY_SCOPES}
```

* The `SHOPIFY_CLIENT_ID` should match the Client ID that you use in your Shopify integration settings.
* The `REDIRECT_CALLBACK_URL` should be the URL of the Redirect Callback page in your app.
* The `SHOPIFY_SCOPES` should match the scopes that you use in your Shopify integration settings.

#### Redirect Callback

The Redirect Callback should be implemented as follows:

* Import the Paragon SDK and authenticate a user.

  * **Note**: If a user is not yet logged into your app, you can redirect to a login form, while preserving the intended URL to redirect to upon successful login. In other words, after logging in, your user should see your Redirect Page.

* Accept and read query parameters, which will be:

  * `code` and `shop` in case of a successful installation

  * `error` in case of an unsuccessful installation or denied consent

* If the `code` query parameter is present,

  * Read the `shop` query parameter and capture the shop name in the pattern `{shop}.myshopify.com`. See the regular expression used below.

  * Call `paragon.completeInstall` to complete the OAuth exchange and save a new connected Shopify account.

```javascript theme={null}
let params = new URLSearchParams(window.location.search);
let authorizationCode = params.get("code");
let [, shopName] = params.get("shop").match(/^([a-zA-Z0-9][a-zA-Z0-9\-]*).myshopify.com/);

if (authorizationCode && shopName) {
   paragon.completeInstall("shopify", {
    authorizationCode: authorizationCode,
    redirectUrl: "https://your-app.url/shopify-redirect",
    integrationOptions: {
      SHOP_NAME: shopName,
    }
  }).then(() => {
     // Redirect to your app's integrations page
  });
} else {
  let error = params.get("error");
  // Handle error
}
```

* If the `error` query parameter is present,

  * Show this error in your app and allow your user to retry the process.

#### Updating your app's redirect and app URLs

1. Log in to your Shopify [Partner Dashboard](https://partners.shopify.com/current/resources) and select your app.

2. Navigate to **App setup > URLs > Allowed redirection URL(s)**

3. Set your **App URL** to your app's Initial Redirect URL.

4. Add your app's Redirect Callback URL to **Allowed redirection URL(s).**

### Subscribing to mandatory privacy webhooks

Shopify requires you to subscribe to 3 privacy webhooks to request or erase personal data that your integration may store in your application.

Paragon's Shopify integration allows you to subscribe and take action on these webhooks via workflows.

#### Setting the Shopify Webhook URL

To get started, visit the Paragon dashboard and navigate to your Shopify integration.

* Click on the **Settings** tab and copy the **Webhook URL** value.

<Frame>
  <img src="https://mintcdn.com/paragon/fpLCYjVDKL_JwtxK/assets/copying-the-shopify-webhook-url-in-paragon.png?fit=max&auto=format&n=fpLCYjVDKL_JwtxK&q=85&s=bc62838476cc3031ab3fa209647d004f" alt="" width="2783" height="1762" data-path="assets/copying-the-shopify-webhook-url-in-paragon.png" />
</Frame>

Next, log in to [Shopify Partners](https://partners.shopify.com) and navigate to **Apps**.

* Select the app that you are using in the environment or project you have opened in Paragon.
* Navigate to **Configuration** (under the Build section) and scroll to **Compliance Webhooks**.
* For each of the endpoints (Customer data request endpoint, Customer data erasure endpoint, Shop data erasure endpoint), paste in the Webhook URL value you copied from the Paragon dashboard.
* Click **Save and release** at the top to save your changes.

<Frame>
  <img src="https://mintcdn.com/paragon/lKoo2WTuuveEy7P1/assets/adding-the-webhook-url-to-shopify-compliance-webhooks.png?fit=max&auto=format&n=lKoo2WTuuveEy7P1&q=85&s=1c93fca2f02625e8625808bed258dd4e" alt="" width="2821" height="724" data-path="assets/adding-the-webhook-url-to-shopify-compliance-webhooks.png" />
</Frame>

#### Creating workflows to respond to privacy webhooks

Next, create 3 workflows that listen for these triggers and take action on events received:

* Customer data request
* Customer data erasure
* Shop data erasure

You can create new workflows in the Paragon dashboard, from the **Overview** tab of your Shopify integration and click **Create Workflow**.

For each of the new workflows you create, select a **Shopify** trigger and select one of the Shopify privacy webhook events as the Trigger Event:

<Frame>
  <img src="https://mintcdn.com/paragon/mM8ZO0IfeKOuG22m/assets/selecting-a-shopify-privacy-webhook-trigger-event-in-paragon.png?fit=max&auto=format&n=mM8ZO0IfeKOuG22m&q=85&s=8ba1198088dc2ccd84825a05a038426e" alt="" width="810" height="287" data-path="assets/selecting-a-shopify-privacy-webhook-trigger-event-in-paragon.png" />
</Frame>

Define the steps under the workflow to respond to the event type you selected.

From [Shopify's documentation](https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks), here is how you should handle each event type:

* **Customer data request**: If your app has been granted access to [customer or order data](https://shopify.dev/docs/api/usage/access-scopes#authenticated-access-scopes), then it will receive a data request webhook. The webhook contains the resource IDs of the customer data that you need to provide to the store owner. It's your responsibility to provide this data to the store owner directly.

  * **Note**: This request does not require the data to be provided in a response to the webhook. This process happens outside of Shopify and should be provided to the user who connected this Shopify account directly, e.g. through email, within 30 days of receiving the request.

* **Customer data erasure**: Shopify store owners can request that data is deleted on behalf of a customer. When this happens, Shopify sends a Customer Data Erasure event to the apps installed on that store so that you can erase any data for a certain customer of a store from your database.

* **Shop data erasure**: 48 hours after a store owner uninstalls your app, Shopify sends a Shop Data Erasure event. This webhook provides the store's `shop_id` and `shop_domain` so that you can erase data for that store from your database.

<Accordion title="Example Implementation">
  Example implementation

  Add a Request step under the Trigger to send the privacy event information to your API. We recommend including the following values in the request body for your reference:

  * `{{1.result}}`: This is the full event payload received from Shopify. You will see an example of the event in your workflow Test Data. See [Shopify's documentation on event payloads](https://shopify.dev/docs/apps/webhooks/configuration/mandatory-webhooks#customers-data_request) for more details.
  * `{{userSettings.userId}}`: This is the User ID of the Connected User that received the event. You can use this ID to relate the event to a user in your application.

  <Frame>
    <img src="https://mintcdn.com/paragon/fpLCYjVDKL_JwtxK/assets/configuring-a-request-step-for-shopify-privacy-webhooks-in-paragon.png?fit=max&auto=format&n=fpLCYjVDKL_JwtxK&q=85&s=7b58c43bd8f02ee12254fbb66df51199" alt="" width="951" height="1218" data-path="assets/configuring-a-request-step-for-shopify-privacy-webhooks-in-paragon.png" />
  </Frame>
</Accordion>

Finally, configure your 3 workflows to be [hidden from the Connect Portal and enabled by default](/connect-portal/connect-portal-customization#workflows):

* Click the context menu in the Workflow Editor toolbar and click **Edit Connect Portal Workflow Settings**.
* Switch on **Default to enabled** and **Hide workflow from Portal for all users**.

<Frame>
  <img src="https://mintcdn.com/paragon/EuLlf5VxgsSnEq57/assets/Default%20Enabled%20%20Hide%20Workflow%20On.png?fit=max&auto=format&n=EuLlf5VxgsSnEq57&q=85&s=ed5c9a64906b875fc1d0f93c1c8a1d69" alt="" width="1002" height="266" data-path="assets/Default Enabled  Hide Workflow On.png" />
</Frame>

* Repeat for each workflow that has a Shopify privacy event trigger.

Having both options on will mean that this workflow will run for all users of your Shopify integration, and users will not see or need to configure the workflow from your Connect Portal.

#### Testing and validating privacy webhooks

To test your privacy webhook implementation end-to-end:

* Verify that each of your workflows are [deployed](/workflows/building-workflows#deploying-workflows).
* In your application, connect a Shopify store to the Connect Portal. Remember the store and account that you have connected.
* In the [Shopify Admin](https://admin.shopify.com) page for the same store, request or erase a customer's data ([see Shopify documentation](https://help.shopify.com/en/manual/privacy-and-security/privacy/processing-customer-data-requests)). These actions will trigger the "Customer data request" and "Customer data erasure" events, respectively.
* In the Paragon dashboard, visit the [Monitoring > Workflows](/workflows/viewing-workflow-executions) page and verify that your workflow has executed.
