---
title: "Shopify Integration"
description: "The Shopify integration for Dub enables real-time conversion analytics and affiliate attribution by connecting Shopify merchants to workspaces. It manages the complete lifecycle of customer interac..."
last_updated: "2026-10-05T05:07:35.154101+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/external-integrations/shopify-integration"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/app/ee/api/shopify/pixel/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts)
- [apps/web/scripts/dev/simulate-shopify-conversion.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/simulate-shopify-conversion.ts)
- [apps/web/app/ee/api/appsflyer/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts)
- [apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/orders-paid.ts)
- [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/checkout-session-completed.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts)
- [apps/web/app/ee/api/shopify/integration/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts)
- [apps/web/lib/integrations/shopify/process-order.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/process-order.ts)
- [apps/web/lib/integrations/shopify/create-sale.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/create-sale.ts)
- [apps/web/app/ee/api/singular/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/singular/webhook/route.ts)
- [apps/web/lib/jobs/handlers/process-shopify-order-job.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/process-shopify-order-job.ts)
- [apps/web/app/api/dub/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/route.ts)
- [apps/web/app/ee/api/stripe/integration/webhook/invoice-paid.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts)
- [apps/web/app/ee/api/shopify/integration/callback/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/callback/route.ts)
- [apps/web/lib/discounts/discount-provider-shopify.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/discounts/discount-provider-shopify.ts)
- [apps/web/app/api/callback/plain/partner/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/plain/partner/route.ts)
- [apps/web/lib/integrations/shopify/create-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/create-lead.ts)
- [apps/web/app/api/dub/webhook/sale-created.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/sale-created.ts)
- [apps/web/lib/rewardful/import-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/rewardful/import-commissions.ts)
- [apps/web/lib/auth/track-dub-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/track-dub-lead.ts)
- [apps/web/lib/integrations/hubspot/track-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/hubspot/track-lead.ts)
- [apps/web/lib/integrations/shopify/schema.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/schema.ts)
- [apps/web/lib/api/conversions/track-sale.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/conversions/track-sale.ts)
- [apps/web/lib/dub.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dub.ts)
- [apps/web/scripts/programs/backfill-reuse-commission.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/programs/backfill-reuse-commission.ts)
- [apps/web/lib/integrations/shopify/checkout-cache.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/checkout-cache.ts)
- [apps/web/scripts/customers/upheal/sync-stripe-invoices.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/customers/upheal/sync-stripe-invoices.ts)
- [apps/web/scripts/stripe/backfill-discount-code-sales.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/stripe/backfill-discount-code-sales.ts)
- [apps/web/scripts/dev/data.json](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/data.json)
- [apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts)
</details>

## Overview

The Shopify integration for Dub enables real-time conversion analytics and affiliate attribution by connecting Shopify merchants to workspaces. It manages the complete lifecycle of customer interactions, including OAuth installation callbacks, client-side web pixel tracking, server-side webhook ingestion, asynchronous order processing, discount code attribution, and commission publishing.

Sources: [apps/web/lib/integrations/shopify/schema.ts:1-51](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/schema.ts#L1-L51), [apps/web/app/ee/api/shopify/integration/callback/route.ts:25-90](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/callback/route.ts#L25-L90), [apps/web/lib/jobs/handlers/process-shopify-order-job.ts:17-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/process-shopify-order-job.ts#L17-L68)

## Shopify OAuth Installation Callback

### Overview

The Shopify integration handshake is managed through the PATCH endpoint located at `apps/web/app/ee/api/shopify/integration/callback/route.ts`. This route handles workspace authentication, validates incoming payload schemas using Zod discriminated unions, updates project configuration in Prisma, and securely persists encrypted OAuth access tokens along with granted scopes.

Sources: [apps/web/app/ee/api/shopify/integration/callback/route.ts:1-90](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/callback/route.ts#L1-L90)

### Request Schema & Handshake Actions

Incoming requests are parsed via `requestSchema`, which defines a discriminated union on the `action` property supporting store connections and disconnections.

| Action | Required Fields | Field Type & Validation | Purpose |
| :--- | :--- | :--- | :--- |
| `connect` | `action`, `shopifyStoreId`, `accessToken`, `scope` | `z.literal("connect")`, `z.string().min(1)`, `z.string().min(1)`, `z.string().min(1)` | Links a Shopify store identifier and stores encrypted credentials and scopes. |
| `disconnect` | `action`, `shopifyStoreId` | `z.literal("disconnect")`, `z.literal(null)` | Removes the Shopify store association from the workspace project. |

Sources: [apps/web/app/ee/api/shopify/integration/callback/route.ts:11-23](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/callback/route.ts#L11-L23)

> [!IMPORTANT]
> Access to the callback route is strictly guarded by workspace middleware (`withWorkspace`). Callers must possess either the `owner` or `member` role and belong to workspaces subscribed to `business`, `advanced`, or `enterprise` plans.

Sources: [apps/web/app/ee/api/shopify/integration/callback/route.ts:86-90](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/callback/route.ts#L86-L90)

### Call-Chain Execution Walkthrough

When an authenticated PATCH request hits the callback handler, execution proceeds through validation, database updates, credential encryption, and integration record management:

1. `parseRequestBody(req)` — Extracts and parses the incoming HTTP request body.
2. `requestSchema.parse(...)` — Validates the payload against the discriminated union schema for `connect` or `disconnect` actions.
3. `prisma.project.update(...)` — Updates the workspace project record in the database, setting or clearing the `shopifyStoreId`.
4. `installIntegration(...)` (conditional on `body.action === "connect"`):
   - `encrypt(body.accessToken)` — Encrypts the raw Shopify OAuth access token prior to persistence.
   - Saves an `InstalledIntegration` record binding the user, workspace, `SHOPIFY_INTEGRATION_ID`, and credentials.
5. `prisma.installedIntegration.delete(...)` (conditional on `body.action === "disconnect"`):
   - Deletes the matching installed integration record using a composite unique key (`userId_integrationId_projectId`), silently catching any errors if absent.

Sources: [apps/web/app/ee/api/shopify/integration/callback/route.ts:26-70](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/callback/route.ts#L26-L70)

> [!WARNING]
> If a Prisma database operation throws error code `P2002` (indicating a unique constraint violation), the handler catches it and throws a `DubApiError` with the conflict code, signaling that the specified Shopify store is already tied to another project.

Sources: [apps/web/app/ee/api/shopify/integration/callback/route.ts:72-78](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/callback/route.ts#L72-L78)

## Web Pixel Event Ingestion

### Overview

Client-side checkout and click tracking are handled by the Shopify Web Pixel route at `apps/web/app/ee/api/shopify/pixel/route.ts`. This endpoint receives tracking payloads from the storefront web pixel, validates request inputs, enforces rate limits, verifies underlying click events, and caches checkout-to-click associations in Redis before evaluating whether to dispatch asynchronous order processing jobs.

Sources: [apps/web/app/ee/api/shopify/pixel/route.ts:1-82](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts#L1-L82)

### Request Validation & Schema

Incoming POST requests are parsed using a Zod schema that expects three optional string properties: `clickId`, `checkoutToken`, and `shopDomain`. 

| Field Name | Type | Nullable / Optional | Purpose |
| :--- | :--- | :--- | :--- |
| `clickId` | `string` | `.nullish()` | The unique click identifier associated with the affiliate visit. |
| `checkoutToken` | `string` | `.nullish()` | The Shopify checkout session token generated during checkout. |
| `shopDomain` | `string` | `.nullish()` | The domain name of the Shopify merchant store. |

Sources: [apps/web/app/ee/api/shopify/pixel/route.ts:14-18](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts#L14-L18)

> [!WARNING]
> Both `checkoutToken` and `clickId` are strictly required to proceed. If either field is missing or nullish, the route logs an error and immediately returns an HTTP "OK" response without performing further caching or job dispatch.

Sources: [apps/web/app/ee/api/shopify/pixel/route.ts:37-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts#L37-L45)

### Call-Chain Execution Walkthrough

When a valid pixel tracking request is received, execution proceeds through security checks, database lookups, and asynchronous cache updates:

1. `parseRequestBody(req)` & `inputSchema.parse(...)` — Extracts and validates the incoming JSON payload against expected fields.
2. `ratelimit().limit(...)` — Applies an Upstash rate limit keyed by `shopify-track-pixel:${ip}`, where the IP is derived via `ipAddress(req)` on Vercel or `LOCALHOST_IP` in local development.
3. `getClickEvent({ clickId })` — Queries Tinybird to verify that the specified `clickId` exists in the click event store.
4. `waitUntil(...)` — Schedules background execution on Vercel to decouple downstream cache persistence from the HTTP response:
   - `shopifyCheckoutCache.set({ checkoutToken, fields: { clickId } })` — Writes the checkout-to-click association into Redis using a pipeline (`hset`, `expire`, `hgetall`) with a TTL of 1 hour (`SHOPIFY_CHECKOUT_CACHE_TTL_SECONDS`).
   - `tryDispatchShopifyOrderJob({ checkoutToken, checkout })` — Inspects cached checkout state and attempts to trigger the order processing job if all required fields are present.

Sources: [apps/web/app/ee/api/shopify/pixel/route.ts:26-75](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/pixel/route.ts#L26-L75), [apps/web/lib/integrations/shopify/checkout-cache.ts:6-43](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/checkout-cache.ts#L6-L43), [apps/web/lib/integrations/shopify/checkout-cache.ts:61-128](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/checkout-cache.ts#L61-L128)

> [!TIP]
> The `tryDispatchShopifyOrderJob` helper uses an atomic Redis `hsetnx` operation on the `dispatched` field to claim the checkout. This prevents duplicate job dispatches when both the client-side web pixel event and the server-side orders-paid webhook arrive concurrently.

Sources: [apps/web/lib/integrations/shopify/checkout-cache.ts:93-101](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/checkout-cache.ts#L93-L101)

## Orders-Paid Webhook Dispatch

### Overview

Server-side webhook ingestion, HMAC validation, and job dispatch orchestration are handled by the Shopify webhook route at `apps/web/app/ee/api/shopify/integration/webhook/route.ts` and its topic handlers. This route processes incoming Shopify webhook events, verifies HMAC cryptographic signatures, validates workspace configurations, and routes payloads to specific handlers such as `ordersPaid` in `apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts`.

Sources: [apps/web/app/ee/api/shopify/integration/webhook/route.ts:25-155](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts#L25-L155), [apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts:11-125](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/orders-paid.ts#L11-L125)

### Webhook Route Processing & Validation

The POST endpoint begins by extracting raw request text, headers, and Shopify-specific metadata including `x-shopify-topic`, `x-shopify-hmac-sha256`, and `x-shopify-shop-domain`. 

| Header / Field | Type | Purpose |
| :--- | :--- | :--- |
| `x-shopify-topic` | `string` | Identifies the Shopify event topic (e.g., `orders/paid`, `app/uninstalled`). |
| `x-shopify-hmac-sha256` | `string` | The cryptographic HMAC signature provided by Shopify for request verification. |
| `x-shopify-shop-domain` | `string` | The shop domain used to look up the associated workspace in Prisma. |

Sources: [apps/web/app/ee/api/shopify/integration/webhook/route.ts:28-33](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts#L28-L33)

> [!WARNING]
> Unless running in local development (`isLocalDev`), the webhook route computes an HMAC SHA256 digest of the raw request body using `SHOPIFY_WEBHOOK_SECRET` and compares it to the incoming signature using `timingSafeCompare`. If verification fails, the route immediately returns an HTTP 401 response.

Sources: [apps/web/app/ee/api/shopify/integration/webhook/route.ts:39-54](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts#L39-L54)

### Relevant Webhook Topics

The system tracks specific mandatory compliance topics and order events, which are validated against a predefined `relevantTopics` set before workspace lookup and dispatch occur.

| Topic Name | Handler Function | Purpose |
| :--- | :--- | :--- |
| `orders/paid` | `ordersPaid` | Processes paid orders, checks customer records or discount codes, and queues jobs. |
| `app/uninstalled` | `appUninstalled` | Handles app uninstallation cleanup for the shop domain. |
| `customers/data_request` | `customersDataRequest` | Handles GDPR customer data requests. |
| `customers/redact` | `customersRedact` | Handles GDPR customer data erasure requests. |
| `shop/redact` | `shopRedact` | Handles shop data deletion requests. |

Sources: [apps/web/app/ee/api/shopify/integration/webhook/route.ts:15-23](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts#L15-L23), [apps/web/app/ee/api/shopify/integration/webhook/route.ts:96-126](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts#L96-L126)

### Call-Chain Execution Walkthrough

When an `orders/paid` webhook event is successfully authenticated and matched to a workspace, execution flows through the `ordersPaid` handler and checkout cache manager:

1. `POST` — The main webhook route receives the HTTP request, validates headers, verifies the HMAC signature, parses the event JSON, and dispatches control to `ordersPaid` based on the `orders/paid` topic.
2. `ordersPaid` — Parses the order using `shopifyOrderSchema`, checks for existing customer records or matching partner discount codes, and falls back to checking `note_attributes` for a `dubClickId` before writing to cache or queuing processing.
3. `tryDispatchShopifyOrderJob` — Evaluates whether the cached checkout contains all required fields (`order`, `workspaceId`, `clickId`) and is not already dispatched, then attempts an atomic Redis `hsetnx` claim on the `dispatched` field.
4. `delete` — Upon successful job dispatch, `shopifyCheckoutCache.delete(checkoutToken)` removes the temporary checkout entry from Redis.
5. `createKey` — The cache helper generates the underlying Redis key using `shopifyCheckoutCache.createKey(checkoutToken)`, formatted with the `shopify:checkout:` prefix.

Sources: [apps/web/app/ee/api/shopify/integration/webhook/route.ts:96-102](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts#L96-L102), [apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts:11-125](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/orders-paid.ts#L11-L125), [apps/web/lib/integrations/shopify/checkout-cache.ts:45-51](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/checkout-cache.ts#L45-L51), [apps/web/lib/integrations/shopify/checkout-cache.ts:61-128](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/checkout-cache.ts#L61-L128)

```mermaid
sequenceDiagram
    participant WebhookRoute as route.ts
    participant OrdersPaid as ordersPaid
    participant CacheModule as checkout-cache.ts
    participant Redis as Redis Cache

    WebhookRoute->>OrdersPaid: POST /api/shopify/integration/webhook (orders/paid)
    OrdersPaid->>CacheModule: tryDispatchShopifyOrderJob({ checkoutToken, checkout })
    CacheModule->>Redis: shopifyCheckoutCache.delete(checkoutToken)
    CacheModule->>CacheModule: shopifyCheckoutCache.createKey(checkoutToken)
```

Sources: [apps/web/app/ee/api/shopify/integration/webhook/route.ts:96-102](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts#L96-L102), [apps/web/app/ee/api/shopify/integration/webhook/orders-paid.ts:11-125](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/orders-paid.ts#L11-L125), [apps/web/lib/integrations/shopify/checkout-cache.ts:45-51](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/checkout-cache.ts#L45-L51), [apps/web/lib/integrations/shopify/checkout-cache.ts:61-128](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/checkout-cache.ts#L61-L128)

> [!NOTE]
> Unlike other webhook topics whose request logs are captured immediately within the main route handler using `waitUntil` and `captureWebhookLog`, `orders/paid` log capture is deferred and handled directly by `processShopifyOrderJob` after the order completes processing.

Sources: [apps/web/app/ee/api/shopify/integration/webhook/route.ts:142-152](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/route.ts#L142-L152)

## Background Order Reconciliation

### Overview

Background order reconciliation executes asynchronously through the queue worker handler to resolve orders against existing customer records, partner program discount codes, or web pixel click attribution identifiers. 

Sources: [apps/web/lib/jobs/handlers/process-shopify-order-job.ts:17-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/process-shopify-order-job.ts#L17-L20), [apps/web/lib/integrations/shopify/process-order.ts:10-18](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/process-order.ts#L10-L18)

### Call-Chain Execution Walkthrough

When `processShopifyOrderJob` picks up a queued order payload, execution flows through the worker handler and reconciliation router:

1. `processShopifyOrderJob` — The job handler fetches workspace configuration via `prisma.project.findUniqueOrThrow`, initializes request logging parameters, and invokes `processShopifyOrder`.
2. `processShopifyOrder` — Inspects incoming order properties, checking first for an existing customer record, then evaluating workspace discount codes, and finally falling back to click ID tracking.
3. `prisma.customer.findUnique` — Queries the database for an existing customer using the compound key `projectId_externalId`.
4. `getLeadEvent` — Retrieves the customer lead event from Tinybird when an existing customer match is confirmed.
5. `createShopifySale` — Records the sale event linked to resolved customer and lead data.

Sources: [apps/web/lib/jobs/handlers/process-shopify-order-job.ts:20-48](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/process-shopify-order-job.ts#L20-L48), [apps/web/lib/integrations/shopify/process-order.ts:10-58](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/process-order.ts#L10-L58)

```mermaid
sequenceDiagram
    participant JobHandler as processShopifyOrderJob
    participant ProcessOrder as processShopifyOrder
    participant Prisma as prisma.customer
    participant Tinybird as getLeadEvent
    participant Sale as createShopifySale

    JobHandler->>ProcessOrder: processShopifyOrder({ order, workspace, clickId })
    ProcessOrder->>Prisma: prisma.customer.findUnique()
    alt Existing Customer Found
        Prisma-->>ProcessOrder: customer
        ProcessOrder->>Tinybird: getLeadEvent({ customerId })
        Tinybird-->>ProcessOrder: leadData
        ProcessOrder->>Sale: createShopifySale(...)
    end
```

Sources: [apps/web/lib/jobs/handlers/process-shopify-order-job.ts:20-48](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/process-shopify-order-job.ts#L20-L48), [apps/web/lib/integrations/shopify/process-order.ts:10-58](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/process-order.ts#L10-L58)

### Lead and Sale Resolution Routing

The reconciliation engine routes orders through three distinct attribution branches depending on customer state, discount code usage, and pixel tracking data.

| Attribution Path | Condition | Resolution Action | Return Attribution Type |
| :--- | :--- | :--- | :--- |
| Existing Customer | `orderCustomer` exists and matching `customer` record found | Retrieves Tinybird lead event and records sale against existing customer | `existing_lead` |
| Discount Code | Customer missing, but `discountCodes` match workspace `defaultProgramId` discount codes | Attributes via discount code link and records sale | `discount_code` |
| Click Tracking | Customer and discount codes missing, but `clickId` is present | Creates Shopify lead via `createShopifyLead` and records sale | `click` |

Sources: [apps/web/lib/integrations/shopify/process-order.ts:26-140](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/process-order.ts#L26-L140)

> [!WARNING]
> When an existing customer record is located but the corresponding Tinybird lead event cannot be fetched (`!leadData`), `processShopifyOrder` explicitly throws an error rather than skipping the order. This ensures the background job retries instead of duplicating customer records.

Sources: [apps/web/lib/integrations/shopify/process-order.ts:45-51](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/process-order.ts#L45-L51)

## Affiliate Discount Code Attribution

### Overview

When an incoming Shopify order lacks an explicit click tracking identifier (`clickId`) or existing customer record, attribution falls back to affiliate discount codes applied at checkout. The `attributeViaDiscountCode` function handles matching discount codes to affiliate links, generating synthetic traffic records, and propagating conversion metrics across analytics and partner systems.

Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:19-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L19-L27)

### Synthetic Click Generation and Customer Creation

Because orders attributed via discount codes lack a pre-existing browser click event, the platform manufactures synthetic records to maintain relational integrity across analytics backends. The billing address country code is resolved against `COUNTRIES_TO_CONTINENTS` to build geographic context for a fake click.

```typescript
const clickEvent = await recordFakeClick({
  link,
  customer: {
    continent: billingAddressCountry
      ? COUNTRIES_TO_CONTINENTS[billingAddressCountry] ?? "Unknown"
      : "Unknown",
    country: billingAddressCountry ?? "Unknown",
    region: billingAddress?.province ?? "Unknown",
  },
});
```

Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:30-42](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L30-L42)

> [!IMPORTANT]
> The database customer record is created *before* the lead event is pushed to Tinybird. This ordering guarantees that a database constraint violation on `projectId_externalId` (P2002) will never leave behind an orphaned Tinybird click record.

Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:47-65](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L47-L65)

### Call-Chain Execution Walkthrough

The attribution flow executes through a series of distinct asynchronous operations, orchestrating local database persistence, analytics recording, and downstream webhooks:

1. `attributeViaDiscountCode` — Entry point receiving the `order`, `workspace`, and matched `link`.
2. `recordFakeClick` — Generates a synthetic click entry in Tinybird using extracted billing geography.
3. `prisma.customer.create` — Persists the newly attributed customer tied to `link.id`, `link.programId`, and `link.partnerId`.
4. `recordLead` — Transmits the manufactured lead event (`Checkout with discount code`) to Tinybird with a generated `nanoid(16)` event ID.
5. `prisma.link.update` — Increments lead counters on the associated link and updates `lastLeadAt`.
6. `queuePartnerCommissionCreation` — Enqueues a partner commission when `link.programId` and `link.partnerId` are present.
7. `Promise.allSettled` — Dispatches parallel notification tasks including workspace webhooks (`sendWorkspaceWebhook`), Google Ads conversion uploads (`queueGoogleAdsConversionUpload`), and partner postbacks (`sendPartnerPostback`).

Sources: [apps/web/lib/integrations/shopify/attribute-via-discount-code.ts:19-185](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/attribute-via-discount-code.ts#L19-L185)

### Shopify Discount Code Management and Retries

Discount provider operations manage the lifecycle of Shopify discount codes via GraphQL mutations. When creating discount codes using `discountCodeBasicCreate`, duplicate code collisions trigger an automated retry mechanism up to `MAX_ATTEMPTS` (3 attempts), appending a 2-character nanoid to the conflicting code string.

| Parameter | Type | Default / Mapping | Purpose |
| :--- | :--- | :--- | :--- |
| `recurringCycleLimit` | `number` | `discount.maxDuration === null ? 0 : discount.maxDuration === 0 ? 1 : discount.maxDuration` | Maps Dub subscription duration limits to Shopify billing cycles |
| `customerSelection` | Object | `{ all: true }` | Specifies that all customers are eligible for the discount code |
| `appliesOncePerCustomer` | boolean | `true` | Restricts redemption to a single use per customer profile |
| `shouldRetry` | boolean | `true` | Controls whether duplicate code creation errors trigger suffix retries |

Sources: [apps/web/lib/discounts/discount-provider-shopify.ts:35-170](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/discounts/discount-provider-shopify.ts#L35-L170)

> [!WARNING]
> If a discount code creation attempt fails with an unrecognized error code or exceeds `MAX_ATTEMPTS` during collision resolution, a `DiscountProviderError` with type `CREATE_FAILED` is thrown, aborting the transaction.

Sources: [apps/web/lib/discounts/discount-provider-shopify.ts:225-232](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/discounts/discount-provider-shopify.ts#L225-L232)

## Sale Record and Commission Publishing

### Overview

Once an order has been successfully attributed to a customer and link, the integration transitions to recording the financial transaction and publishing associated partner commissions. This step processes the monetary payload from the Shopify order, enforces idempotency checks via Redis cache keys, writes analytics records to Tinybird, updates link and customer aggregates, and asynchronously dispatches downstream workspace webhooks and partner postbacks.

Sources: [apps/web/lib/integrations/shopify/create-sale.ts:20-231](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/create-sale.ts#L20-L231)

### Call-Chain Execution Walkthrough

The sale record and commission publishing flow executes through a structured sequence of checks, data mutations, and asynchronous dispatches:

1. `createShopifySale` — Entry point receiving the normalized `order`, `customerId`, `workspaceId`, and `leadData`.
2. `redis.set` — Checks and sets a 7-day TTL idempotency lock using key format `dub_sale_events:linkId:${linkId}:invoiceId:${invoiceId}` with `nx: true` to prevent duplicate processing of the same invoice.
3. `prisma.customer.findUniqueOrThrow` — Retrieves the existing customer record and evaluates conversion status via `isFirstConversion`.
4. `Promise.all` — Executes atomic parallel persistence:
   - `recordSale(saleData)` — Writes the sale event to Tinybird.
   - `prisma.link.update` — Increments link sales counts, sale amounts, and conditionally increments conversions and `lastConversionAt`.
   - `prisma.project.update` — Increments workspace usage metrics.
   - `prisma.customer.update` — Updates customer sales aggregates and sets `firstSaleAt` if unpopulated.
   - `shopifyCheckoutCache.delete(checkoutToken)` — Clears the temporary checkout cache entry.
5. `queuePartnerCommissionCreation` — Enqueues partner commission creation when `link.programId` and `link.partnerId` are present, specifying `CommissionSource.shopify`.
6. `waitUntil` — Triggers background processing via `Promise.allSettled` for workflow execution (`executeWorkflows`), link stats synchronization (`syncPartnerLinksStats`), workspace webhooks (`sendWorkspaceWebhook`), and partner postbacks (`sendPartnerPostback`).

Sources: [apps/web/lib/integrations/shopify/create-sale.ts:20-227](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/create-sale.ts#L20-L227)

> [!IMPORTANT]
> The Redis idempotency check uses `nx: true` with a 7-day expiration (`ex: 60 * 60 * 24 * 7`) keyed on `linkId` and `invoiceId`. If the key already exists, a `ShopifyError` is thrown immediately to abort duplicate order processing.

Sources: [apps/web/lib/integrations/shopify/create-sale.ts:41-55](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/create-sale.ts#L41-L55)

### Sale Event Data Mapping Reference

The sale event object constructed before persistence contains specific properties mapped from the Shopify order and lead context:

| Property | Type | Source / Calculation | Purpose |
| :--- | :--- | :--- | :--- |
| `workspace_id` | string | Passed argument (`workspaceId`) | Associates the sale with the workspace project |
| `event_id` | string | `nanoid(16)` | Unique identifier for the sale event |
| `event_name` | string | `"Purchase"` | Standardized event identifier for analytics |
| `payment_processor`| string | `"shopify"` | Identifies the origin payment gateway |
| `amount` | number | `Math.round(Number(shopMoney.amount) * 100)` | Transaction total rounded to the nearest cent |
| `currency` | string | `shopMoney.currency_code.toLowerCase()` | Lowercased currency code (e.g., `"usd"`) |
| `invoice_id` | string | `order.confirmation_number` | Shopify confirmation number used as invoice ID |
| `metadata` | string | `JSON.stringify(order)` | Serialized full Shopify order payload |

Sources: [apps/web/lib/integrations/shopify/create-sale.ts:31-67](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/shopify/create-sale.ts#L31-L67)

## Related

- [Conversion and Event Tracking](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-and-tracking/conversion-and-event-tracking)
- [Commission Rules and Rewards](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/commission-rules-and-rewards)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
