> ## 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" />

## Setup Guide

This guide walks you through creating a Shopify app and connecting it to Paragon.

Once you have finished your integration and are ready to list it on the Shopify App Store, jump to [Publishing your Shopify app](#publishing-your-shopify-app).

### Setting Up Your Shopify Environment

Before creating your app, you'll need a Shopify Partner account, a development store, and Shopify CLI.

<Info>
  **Already have a Partner account, development store, and Shopify CLI?** Skip to [Creating a Shopify App](#creating-a-shopify-app).
</Info>

#### 1. Create a Partner account

[Sign up](https://www.shopify.com/partners) for a Shopify Partner account. You will use this account to create apps in the [Dev Dashboard](https://dev.shopify.com/dashboard).

#### 2. Create a development store

In the Dev Dashboard, go to **Stores** and select **Create store**. See [Shopify's documentation on creating a development store](https://shopify.dev/docs/apps/build/dev-dashboard/stores/development-stores) for details.

#### 3. Install Shopify CLI

Install Shopify CLI using npm (requires [Node.js](https://nodejs.org/) 22.12 or higher):

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

Or see [Shopify's CLI installation guide](https://shopify.dev/docs/api/shopify-cli) for other installation methods.

### Creating a Shopify App

Shopify apps are configured in a `shopify.app.toml` file and deployed with Shopify CLI. You'll create the app, set OAuth URLs, scopes, and mandatory compliance webhooks, then deploy.

<Steps>
  <Step title="Create the app">
    Open the [Dev Dashboard](https://dev.shopify.com/dashboard) and select **Apps > Create app > Start from Dev Dashboard**. Name your app and select **Create**.

    <Tip>
      If you prefer to scaffold a local project first, run `shopify app init` instead. That command creates both the app and a `shopify.app.toml` file, so you can skip the next step.
    </Tip>
  </Step>

  <Step title="Link a local configuration file">
    In an empty directory (or your app project), run:

    ```bash theme={null}
    shopify app config link
    ```

    Select the app you just created. This generates a `shopify.app.toml` file you will use for the rest of setup.
  </Step>

  <Step title="Configure URLs, scopes, and compliance webhooks">
    Copy the **Webhook URL** from **Integrations > Shopify > Settings** in the Paragon dashboard. If Shopify is not in your catalog yet, add it first so the Settings tab is available.

    <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="Copying the Shopify webhook URL in Paragon" width="2783" height="1762" data-path="assets/copying-the-shopify-webhook-url-in-paragon.png" />
    </Frame>

    Open `shopify.app.toml` and set the following values. Keep any `client_id` that `shopify app config link` already wrote.

    ```toml shopify.app.toml theme={null}
    name = "Your App Name"
    application_url = "https://your-app.example.com"
    embedded = false

    [access_scopes]
    scopes = "read_products,write_products,read_customers,write_customers,read_orders,write_orders"

    [auth]
    redirect_urls = [
      "https://passport.useparagon.com/oauth"
    ]

    [webhooks]
    api_version = "2026-04"

    [[webhooks.subscriptions]]
    compliance_topics = ["customers/data_request", "customers/redact", "shop/redact"]
    uri = "YOUR_PARAGON_WEBHOOK_URL"
    ```

    | Setting              | Value                                                                                                                                                                  |
    | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | **application\_url** | While testing, use your app's root URL. When you [set up redirect pages](#setting-up-redirect-pages-in-your-app) to go live, change this to your Initial Redirect URL. |
    | **redirect\_urls**   | While testing, use `https://passport.useparagon.com/oauth`. When you go live, add your Redirect Callback URL.                                                          |
    | **scopes**           | The access scopes your app will request. Use the same list in Paragon in the next section.                                                                             |
    | **uri**              | The Webhook URL you copied from Paragon. Paragon routes all three compliance events from this single URL.                                                              |

    <Info>
      If your file already has a `[webhooks]` section, keep its `api_version` and add only the `[[webhooks.subscriptions]]` block.
    </Info>

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

    Learn more in [Shopify's app configuration](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration) and [privacy compliance](https://shopify.dev/docs/apps/build/compliance/privacy-law-compliance) docs.
  </Step>

  <Step title="Deploy the configuration">
    From the directory that contains `shopify.app.toml`, run:

    ```bash theme={null}
    shopify app deploy
    ```

    Shopify registers your OAuth URLs, scopes, and compliance webhook subscriptions when you deploy. Repeat this step whenever you change those values.
  </Step>

  <Step title="Install the app on your development store">
    In the Dev Dashboard, open your app, go to **Home**, and select **Install app**. Choose the development store you created earlier and install.

    You must be the store owner or have a staff account on the store to install.
  </Step>
</Steps>

### Add Your Shopify App to Paragon

After creating your Shopify app, enter its Client ID and Client Secret into Paragon.

<Steps>
  <Step title="Get your Client ID and Client Secret">
    In the Dev Dashboard, open your app and go to **Settings**. Copy the **Client ID** and **Client secret**.
  </Step>

  <Step title="Add your credentials to Paragon">
    1. Select **Shopify** from the **Integrations Catalog**.
    2. Under **Integrations > Connected Integrations > Shopify > App Configuration > Configure**, fill in your credentials:

           <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="Configuring Shopify client credentials in Paragon" width="1342" height="1156" data-path="assets/configuring-shopify-client-credentials-in-paragon.png" />
           </Frame>

       | Field             | Value                                                                                                                                                                                                                                                                |
       | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
       | **Client ID**     | The Client ID you copied from the Dev Dashboard                                                                                                                                                                                                                      |
       | **Client Secret** | The Client secret you copied from the Dev Dashboard                                                                                                                                                                                                                  |
       | **Permissions**   | The same scopes you set in `shopify.app.toml`. For a list of recommended scopes, view this integration in your [Paragon dashboard](https://dashboard.useparagon.com). A complete list of Shopify scopes is [here](https://shopify.dev/docs/api/usage/access-scopes). |

    Press **Save Changes** to save your credentials.

    <Info>
      You should only add the scopes you've requested in your Shopify app.
    </Info>
  </Step>
</Steps>

### Testing Your Setup

Open the Connect Portal for Shopify inside of your app, or click **Test Connect Portal** in the Shopify integration, and connect the development store you installed the app on.

## 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)
  * [Creating workflows to respond to privacy webhooks](/resources/integrations/shopify#creating-workflows-to-respond-to-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

When your redirect pages are ready, update `shopify.app.toml` and deploy:

1. Set `application_url` to your app's Initial Redirect URL.
2. Add your Redirect Callback URL to `redirect_urls`.
3. Run `shopify app deploy`.

```toml shopify.app.toml theme={null}
application_url = "https://your-app.url/integrations"

[auth]
redirect_urls = [
  "https://passport.useparagon.com/oauth",
  "https://your-app.url/shopify-redirect"
]
```

### Creating workflows to respond to privacy webhooks

Shopify requires apps listed on the Shopify App Store to handle three privacy events: customer data requests, customer data erasure, and shop data erasure.

If you followed the [Setup Guide](#creating-a-shopify-app), your app is already subscribed to those topics in `shopify.app.toml`. Paragon's Shopify integration lets you take action on the events with workflows.

Create 3 workflows that listen for these triggers:

* 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/build/compliance/privacy-law-compliance), 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/build/compliance/privacy-law-compliance#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 your `shopify.app.toml` compliance subscriptions have been deployed with `shopify app deploy`.
* 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.
