---
name: shopify-salesforce
description: Build a production-grade Shopify to Salesforce integration covering architecture, the step-by-step build with Apex code, field mapping, API and governor limits, security, monitoring, and the pitfalls to design out. Use when connecting, building, or debugging a Shopify and Salesforce integration.
---

# Shopify to Salesforce integration

> Cloudsheer's delivery playbook for Shopify on Salesforce, distilled from 5 client projects and 17 delivery tasks. Apply it as the reference approach when building this integration.

## Overview

Shopify product and order data synced with Salesforce. We have shipped it across 7 client projects and 17 build tasks.

The value is one source of truth: both systems stay current, records reconcile automatically, and every error is logged for review.

We build it the Salesforce-native way: a Connected App and Named Credentials so no secrets ever live in code, field mappings that respect your data model, and record-triggered automation that does the work.

Every Shopify build is delivered by a senior Salesforce architect on a fixed price, tested end to end in a sandbox, deployed to your org, and backed by 30 days of hypercare. You own the result: documented, source-controlled, and free of black-box middleware lock-in.

## Integration facts

**Connects via:**
- Shopify Admin API (GraphQL primary, REST legacy) called from Salesforce or middleware
- Shopify webhooks (HMAC-verified) to a Salesforce REST endpoint
- Managed package (e.g. eShopSync For Shopify) for no-code field mapping and scheduled/real-time sync

**Package:** eShopSync For Shopify (Webkul)

**Authentication:** Shopify: OAuth 2.0 access token in the X-Shopify-Access-Token header; webhooks verified via HMAC-SHA256 (X-Shopify-Hmac-Sha256). Salesforce: OAuth 2.0 connected app

**API type:** GraphQL

**API base:** `https://{shop}.myshopify.com/admin/api/{version}/graphql.json`

**Key endpoints:**
- `/admin/api/{version}/graphql.json`
- `/admin/api/{version}/orders.json`
- `/admin/api/{version}/products.json`
- `/admin/api/{version}/webhooks.json`

**Webhook and platform events:**
- `orders/create`
- `orders/updated`
- `orders/paid`
- `products/create`
- `products/update`
- `inventory_levels/update`
- `customers/create`

**Official docs:** https://shopify.dev/docs/api/admin-graphql/latest

## Prerequisites

- A Salesforce edition with API access (Enterprise, Unlimited, or Developer)
- A dedicated sandbox to build and test in
- A Shopify account on a plan with API access
- System Administrator access on both systems
- A dedicated integration user with a minimum-access permission set
- Agreement on the objects, fields, and sync direction for the Shopify data

## Architecture

Data flows left to right through four lanes:

1. **Sources:** Salesforce records, Shopify
2. **Integration layer:** Mapping and transforms, Push on trigger, Scheduled pull
3. **Salesforce:** Order, Related records, Reports
4. **Outcomes:** Both systems current, Auto-reconciled, Errors logged

## How it works at runtime

1. **Change in either system** `[Either system]`: A change in Salesforce or in Shopify starts the sync.
   - Note: `Salesforce side via flow or Change Data Capture; vendor side via webhook or scheduled pull.`
2. **Transform and route** `[In transit]`: The integration layer maps and transforms the payload between the two data models.
   - Note: `DataWeave or Apex converts between the Salesforce and vendor schemas.`
3. **Push and scheduled pull** `[In transit]`: Records push on trigger and pull on a schedule so both sides stay current.
   - Note: `An updatedSince delta filter keeps each batch small and current.`
4. **Reconciled both ways** `[Both systems]`: Records reconcile across systems, with every error logged for review.
   - Note: `Conflicts resolved by rule; failures land in an Error Log object for replay.`

## Step-by-step build

### Step 1: Plan the integration and prerequisites

Before any code, we lock down access and design how Shopify and Salesforce will talk.

- A Salesforce edition with API access (Enterprise, Unlimited, or Developer) and a dedicated sandbox track
- A Shopify account on a plan with API access, plus admin rights on both systems
- A dedicated Salesforce integration user with a minimum-access permission set, never a personal admin login
- Decide direction (inbound, outbound, or bidirectional) and cadence (real-time callouts vs scheduled batch)
- Budget the daily API request allocation and per-transaction callout limits up front

### Step 2: Register the app and scope OAuth in Shopify

We create the connection on the Shopify side and collect exactly the access Salesforce needs.

- Create an OAuth app or scoped API token in Shopify to get the Client ID and Client Secret
- Grant the minimum OAuth scopes required, and note the API base URL and version
- Whitelist the Salesforce callback URL if the tool uses the authorization-code flow

### Step 3: Store secrets with External and Named Credentials

We use the modern Salesforce auth stack, so no secret ever lives in code or metadata.

- Create an External Credential (OAuth 2.0 or custom) with a named principal
- Create a Named Credential pointing at the Shopify base URL and enable the generated authorization header
- Grant the External Credential through a permission set, so only the integration user can call out
- This replaces legacy Remote Site Settings and hard-coded tokens entirely

> **Pro tip: Named Credentials, not code** Named Credentials keep the secret and endpoint out of your Apex and metadata, so nothing sensitive ships in a deployment or lands in version control.

### Step 4: Design the data model and external IDs

We make Shopify records land cleanly on the right Salesforce object with no duplicates.

- Map every Shopify field to the Order and related objects, documenting type, picklist values, and record types
- Add a unique, external-id, case-insensitive field on each object as the match key
- Define owner assignment, required-field defaults, and relationship lookups

### Step 5: Build inbound ingestion

If Shopify data flows in, we ingest it safely and idempotently.

- Expose an Apex REST resource (@RestResource) or subscribe to Shopify's push or Platform Events
- Authenticate and parse the payload, then Database.upsert on the external id in bulk
- Make it idempotent, so a retried or duplicate payload never creates a second record

Reference implementation (`snippets/InboundRecordApi.cls`):

```apex
@RestResource(urlMapping='/inbound/records/*')
global with sharing class InboundRecordApi {
  @HttpPost
  global static void upsertRecords() {
    List<Row> items = (List<Row>) JSON.deserialize(
      RestContext.request.requestBody.toString(), List<Row>.class);

    List<MyObject__c> rows = new List<MyObject__c>();
    for (Row r : items) {
      rows.add(new MyObject__c(External_Id__c = r.id, Name = r.name, Amount__c = r.amount));
    }
    upsert rows External_Id__c;         // bulk + idempotent on the external id
    RestContext.response.statusCode = 200;
  }
  global class Row { global String id; global String name; global Decimal amount; }
}
```

### Step 6: Build outbound sync

If Salesforce drives Shopify, we call out the right way.

- A record-triggered flow to invocable Apex, or an Apex trigger handing off to a Queueable
- Triggers cannot call out synchronously, so the HTTP callout runs asynchronously
- Serialize with JSON.serialize and call Shopify via callout:NamedCredential, handling every status code

Reference implementation (`snippets/SyncToServiceQueueable.cls`):

```apex
public class SyncToServiceQueueable implements Queueable, Database.AllowsCallouts {
  private List<Id> ids;
  public SyncToServiceQueueable(List<Id> ids) { this.ids = ids; }

  public void execute(QueueableContext ctx) {
    for (MyObject__c rec : [SELECT Id, Name, External_Id__c FROM MyObject__c WHERE Id IN :ids]) {
      HttpRequest req = new HttpRequest();
      req.setEndpoint('callout:Service_NC/v1/records'); // secret lives in the Named Credential
      req.setMethod('POST');
      req.setHeader('Content-Type', 'application/json');
      req.setBody(JSON.serialize(new Map<String,Object>{
        'externalId' => rec.External_Id__c, 'name' => rec.Name }));
      HttpResponse res = new Http().send(req);
      if (res.getStatusCode() != 200) ErrorLog.capture(rec.Id, res);
    }
  }
}
```

### Step 7: Engineer for scale and governor limits

We build it to survive real volume, not just a demo.

- Bulkify everything: no SOQL, DML, or callouts inside loops (100 SOQL and 150 DML per transaction)
- Use Queueable, Batchable, or Scheduled Apex for volume, and chain jobs for large syncs
- Add retry with backoff and a dead-letter Error_Log__c record for anything that fails

Reference implementation (`snippets/NightlySyncBatch.cls`):

```apex
global class NightlySyncBatch implements Database.Batchable<SObject>, Database.AllowsCallouts {
  global Database.QueryLocator start(Database.BatchableContext bc) {
    return Database.getQueryLocator([SELECT Id, External_Id__c FROM MyObject__c WHERE Needs_Sync__c = true]);
  }
  global void execute(Database.BatchableContext bc, List<MyObject__c> scope) {
    ServiceClient.sync(scope);       // one callout per 200-record chunk stays under limits
  }
  global void finish(Database.BatchableContext bc) { /* chain the next job or log the run */ }
}
```

> **Watch out: governor limits** Salesforce caps SOQL, DML, and callouts per transaction. Bulkify everything and move volume to Queueable or Batch Apex, or the integration will fail at scale.

### Step 8: Lock down security and compliance

We give the integration exactly the access it needs and nothing more.

- A least-privilege permission set, field-level security, and sharing for the integration user
- Rotate secrets on a schedule, and add Shield Platform Encryption for sensitive fields where required

### Step 9: Test like production

We prove it works before it ships.

- Apex tests with Test.setMock(HttpCalloutMock) covering success, failure, and a 200-record bulk case
- At least 75 percent coverage, plus sandbox UAT and a parallel run against the live system

Reference implementation (`snippets/SyncToServiceTest.cls`):

```apex
@IsTest
private class SyncToServiceTest {
  private class Mock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest req) {
      HttpResponse res = new HttpResponse();
      res.setStatusCode(200); res.setBody('{"ok":true}');
      return res;
    }
  }
  @IsTest static void syncsInBulk() {
    Test.setMock(HttpCalloutMock.class, new Mock());
    List<MyObject__c> recs = new List<MyObject__c>();
    for (Integer i = 0; i < 200; i++)
      recs.add(new MyObject__c(Name = 'Row ' + i, External_Id__c = 'EXT-' + i));
    insert recs;

    Test.startTest();     // proves the callout is bulk-safe under governor limits
    System.enqueueJob(new SyncToServiceQueueable(new List<Id>(new Map<Id,MyObject__c>(recs).keySet())));
    Test.stopTest();
  }
}
```

### Step 10: Deploy, monitor, and hand over

We ship it safely and keep it healthy.

- Deploy via change sets or an SFDX and CI pipeline, and assign the permission sets
- Turn on monitoring and alerting on the Error Log, and optionally Event Monitoring
- Hand over with 30 days of hypercare and failure alerting

## Data model

| Object | Purpose | Key fields |
| --- | --- | --- |
| `Order` | The primary Salesforce record Shopify data maps onto. | `External_Id__c, Name, Status` |
| `Account` | Matched or created for the customer or company behind the record. | `Name, External_Id__c` |
| `Error_Log__c (custom)` | Captures every request, response, and failure so anything can be replayed. | `Payload__c, Status__c, Related_Id__c` |
| `Sync_Log__c (custom)` | Tracks the last-synced timestamp per record for delta pulls. | `Last_Synced__c, Direction__c` |

Salesforce objects typically in play: `Account`, `Contact`, `Order`, `OrderItem`, `Product2`, `PricebookEntry`, `Opportunity`

## Field mapping (example)

| Shopify | Salesforce | Notes |
| --- | --- | --- |
| Shopify record id | `Order.External_Id__c` | Unique external id, upsert key |
| Shopify number | `Order.Name` |  |
| Shopify customer | `Account` | Matched or created |
| Shopify amount / total | `Order.Total__c` |  |
| Shopify status | `Order.Status` | Picklist value mapping |
| Created / updated at | `LastModifiedDate` | Enables delta sync and audit |
| Owner or rep | `Order.OwnerId` | Assignment rules or a default owner |

Tailor the full mapping to the org. Always upsert on an external-id field so retries are idempotent.

## API and rate limits

### Shopify-specific

- REST Admin API: leaky-bucket 2 req/sec sustained, 40 burst (Standard)
- GraphQL Admin API: cost-based, 50 points/sec restore, 1000-point bucket
- Max 15 webhook subscriptions per topic per store
- REST Admin API is legacy as of Oct 2024; new public apps must be GraphQL-only from Apr 2025

### Salesforce platform

- Salesforce caps API calls per 24 hours by edition and license count. We budget the daily allocation up front so the integration never starves other tools.
- Per transaction, Apex allows 100 SOQL queries, 150 DML statements, and 100 callouts. We bulkify everything and move volume async to stay well under them.
- Shopify enforces its own rate limits. We honour them, and back off with jitter on HTTP 429 responses instead of hammering the API.
- A synchronous callout can run for up to 120 seconds. Anything longer runs in Queueable or Batch Apex, never inline.

## Security checklist

- Secrets stored in Named Credentials and permission sets, never in code or metadata
- A least-privilege integration user, with field-level security and sharing scoped tight
- All traffic over TLS, with signature verification on inbound events
- Shield Platform Encryption available for sensitive fields
- A full audit trail: every request and response logged for traceability
- Every automation runs as a dedicated integration user, so actions are attributable and revocable
- Sandbox-first delivery and change-set deployment keep production changes reviewed and controlled

## Monitoring and reliability

- Every request and response is logged to a custom Error Log object, tagged with the related record id.
- Failed calls retry with exponential backoff; anything still failing lands in a dead-letter queue for review.
- Idempotency keys guarantee a retried or duplicate event never double-posts a record.
- A dashboard surfaces failures, latency, and volume so problems are caught before users notice.
- Optional email or Slack alerts fire on repeated failures or a stalled sync.

## Testing and deployment

- Apex unit tests with HttpCalloutMock cover the success path, failure handling, and a 200-record bulk case, at 75 percent or higher coverage.
- The full flow is validated in a sandbox against real sample data and the edge cases that matter.
- A parallel run reconciles the integration against your live system before cutover.
- Everything deploys through change sets or an SFDX and CI pipeline, under version control.
- Permission sets, sharing, and Named Credentials are configured in production, then we run 30 days of monitored hypercare.

## Pitfalls to design out

- **Duplicate records on retry:** Upsert on a unique external-id field so retried payloads are idempotent.
- **Hitting governor limits at volume:** Bulkify and move work to Queueable or Batch Apex; never call out inside a loop.
- **Callouts failing when a token expires:** Use Named Credentials so Salesforce refreshes the OAuth token automatically.
- **No visibility when it breaks:** We log every call and surface failures on a dashboard with alerts, so an issue never goes unnoticed.
- **Reporting drifts from reality:** External-id keys and a delta timestamp keep Salesforce and the source reconciled, so reports stay trustworthy.

### Shopify-specific gotchas

- REST-to-GraphQL migration is mandatory for new or updated apps
- You must verify the webhook HMAC-SHA256 signature or accept spoofed payloads
- Orders can be created without a customer record, breaking Account/Contact upsert logic
- GraphQL cost throttling returns THROTTLED errors that require backoff, not simple 429s

## FAQ

**How do you authenticate Shopify with Salesforce?**

We connect Shopify using secure named credentials and store every secret in Salesforce Named Credentials with a permission set, so nothing is hard-coded or shipped in metadata.

**Does the Shopify integration handle bulk volume?**

Yes. All Apex is bulkified, volume moves to Queueable or Batch Apex, and we respect the Salesforce governor limits (SOQL, DML, and callout caps per transaction).

**How do you prevent duplicate records?**

We upsert on a unique external-id field, so a retried or duplicate payload is idempotent and never creates a second Order.

**How is the integration tested and deployed?**

Apex tests with HttpCalloutMock cover the success, failure, and a 200-record bulk case (75 percent plus coverage). We deploy via change sets or an SFDX and CI pipeline.

**What happens if Shopify or Salesforce is briefly down?**

Failed calls retry with backoff and land in an Error Log object with alerting, so nothing is lost and any event can be replayed.

**Inbound, outbound, or both?**

We build whichever direction you need: an Apex REST endpoint for inbound, record-triggered flows or Queueable callouts for outbound, or both for a two-way sync.

---

Maintained by [Cloudsheer](https://www.cloudsheer.com). Full illustrated guide: [Shopify technical guide](https://www.cloudsheer.com/integrations/shopify/technical-guide). Want it built for you at a fixed price? [Book a free 30-minute call](https://cal.com/cloudsheer-consulting/30min?overlayCalendar=true).
