Connect Shopify with Stripe
Implementation Guide
Overview: Connecting Shopify and Stripe
For merchants who have deployed Shopify as their storefront but use Stripe as their primary payment processor outside of Shopify Payments—or who need to synchronize Shopify order data with Stripe for subscription billing, revenue recognition, or financial reconciliation—a direct integration between the two platforms is essential. While Shopify natively supports Stripe as a payment gateway through the Shopify Payments infrastructure, there are numerous scenarios where a custom integration is required: hybrid subscription and one-time purchase models, multi-currency reconciliation, custom dunning logic, and direct integration with Stripe Radar for fraud scoring on orders that originate outside Shopify's standard checkout.
This guide covers the implementation of a Shopify-to-Stripe integration that handles new order events from Shopify webhooks, creates or updates Stripe Customer objects, generates PaymentIntents or confirms charges, and handles refund propagation from Shopify back to Stripe. The implementation uses a combination of Shopify's Admin Webhook API and Stripe's API v1 REST interface.
Core Prerequisites
On the Shopify side, you need a store with Owner or a Staff account that has the "Orders," "Customers," and "Store settings" permissions enabled. To register webhooks programmatically, you need a Custom App with the following Admin API access scopes: read_orders, write_orders, read_customers, write_customers, and read_fulfillments. Create the Custom App from Shopify Admin > Settings > Apps and sales channels > Develop apps. Generate an Admin API access token and store it securely; this token does not expire but should be rotated if compromised.
On the Stripe side, you need a Stripe account with Administrator access. Generate a Restricted API Key from the Stripe Dashboard under Developers > API Keys > Restricted keys. Grant the following permissions to the restricted key: PaymentIntents: Write, Customers: Write, Refunds: Write, and Charges: Read. Avoid using a live-mode Secret Key directly in your integration environment; always use restricted keys with the minimum required permissions.
For webhook signature validation on both platforms, ensure your receiving server has HTTPS with a valid TLS certificate. Shopify uses HMAC-SHA256 with your webhook secret to sign payloads via the X-Shopify-Hmac-Sha256 header. Stripe uses a similar mechanism with the Stripe-Signature header and your webhook endpoint's signing secret, obtainable from the Stripe Dashboard under Developers > Webhooks.
Top Enterprise Use Cases
The most impactful use case is Stripe Customer synchronization. Every Shopify customer who completes a purchase should have a corresponding Stripe Customer object created or updated, with their email, name, and Shopify customer ID stored in Stripe's metadata. This enables merchants to subsequently attach payment methods, create subscriptions, or issue credits directly in Stripe while maintaining a link back to the originating Shopify record.
A second use case is post-purchase subscription upsell. When a Shopify order contains a specific product tagged as a subscription trigger (e.g., a product with the tag subscription-eligible), the integration creates a Stripe Subscription on the customer's Stripe account, attaching the payment method used in the Shopify order via Stripe's payment_method attachment flow.
A third use case is financial reconciliation. Stripe serves as the source of truth for settled funds, while Shopify is the source of truth for order line items and fulfillment status. By writing Shopify order IDs, line item SKUs, and fulfillment status into Stripe PaymentIntent and Charge metadata, finance teams can join the two datasets in their data warehouse for accurate revenue recognition without relying on Shopify's sometimes-delayed payout reports.
Step-by-Step Implementation Guide
Registering Shopify Webhooks
Register a Shopify webhook for the orders/create topic using the Shopify Admin REST API:
curl -X POST https://[your-store].myshopify.com/admin/api/2024-01/webhooks.json \
-H "X-Shopify-Access-Token: YOUR_ADMIN_API_TOKEN" \
-H "Content-Type: application/json" \
-d {
"webhook": {
"topic": "orders/create",
"address": "https://your-integration-endpoint.com/webhooks/shopify/orders",
"format": "json"
}
}
Repeat this registration for orders/updated, refunds/create, and customers/create topics as needed. Each webhook registration returns a response containing the webhook ID and the api_version it was registered against. Store these webhook IDs so you can update or delete them programmatically when your API version is upgraded.
When your endpoint receives a Shopify webhook, validate the HMAC signature before processing. The validation logic in Node.js is:
const crypto = require('crypto');
function validateShopifyWebhook(rawBody, hmacHeader, secret) {
const digest = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(digest),
Buffer.from(hmacHeader)
);
}
The rawBody must be the raw, unparsed request body bytes. Do not parse the JSON before computing the HMAC; doing so may alter whitespace and cause signature validation to fail.
Handling the Order Payload and Creating a Stripe Customer
A Shopify orders/create webhook payload contains the full order object. The relevant fields for Stripe integration are:
{
"id": 5612345678901,
"email": "[email protected]",
"total_price": "149.99",
"currency": "USD",
"customer": {
"id": 7890123456789,
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+14155552671"
},
"line_items": [
{
"id": 1234567890123,
"title": "Pro Plan Annual",
"sku": "PRO-ANNUAL-2024",
"quantity": 1,
"price": "149.99",
"product_id": 8901234567890,
"vendor": "AcmeCo"
}
],
"payment_gateway": "stripe",
"financial_status": "paid"
}
After validating the webhook, search for an existing Stripe Customer with the customer's email using the Stripe Customer search API:
GET https://api.stripe.com/v1/[email protected]&limit=1
Authorization: Bearer rk_live_RESTRICTED_KEY
If a customer is found, use the existing cus_ ID. If not, create a new Stripe Customer:
POST https://api.stripe.com/v1/customers
Authorization: Bearer rk_live_RESTRICTED_KEY
Content-Type: application/x-www-form-urlencoded
email=customer%40example.com
&name=Jane+Doe
&phone=%2B14155552671
&metadata[shopify_customer_id]=7890123456789
&metadata[shopify_store]=your-store.myshopify.com
The metadata fields are critical for reconciliation. Always store the Shopify customer ID and store domain in Stripe Customer metadata. The Stripe API response will return the new Customer object with a cus_ prefixed ID. Store this cus_ ID back in the Shopify customer's metafields for future lookups, using a Shopify metafield with namespace stripe and key customer_id.
Writing Shopify Order Data to Stripe PaymentIntent Metadata
If the Shopify order was already charged through Shopify Payments (which uses Stripe under the hood), you may want to locate the existing Stripe Charge and update its metadata rather than creating a new PaymentIntent. The Shopify order object includes a payment_details field with a credit_card_wallet property and, for Stripe-backed payments, a transaction that contains the gateway transaction ID. Retrieve the Shopify transactions for the order:
GET https://[store].myshopify.com/admin/api/2024-01/orders/ORDER_ID/transactions.json
X-Shopify-Access-Token: YOUR_TOKEN
The authorization field in the transaction object contains the Stripe Charge ID or PaymentIntent ID (prefixed with ch_ or pi_). Use this to update the Stripe object with Shopify-specific metadata:
POST https://api.stripe.com/v1/payment_intents/pi_XXXXX
Authorization: Bearer rk_live_RESTRICTED_KEY
Content-Type: application/x-www-form-urlencoded
metadata[shopify_order_id]=5612345678901
&metadata[shopify_order_name]=%23SH-1042
&metadata[line_item_skus]=PRO-ANNUAL-2024
&metadata[fulfillment_status]=unfulfilled
Propagating Shopify Refunds to Stripe
When the refunds/create webhook fires, the payload includes the refund amount and the transactions associated with the original order. Extract the amount field and the parent charge/payment intent ID from the order transaction lookup, then issue a Stripe Refund:
POST https://api.stripe.com/v1/refunds
Authorization: Bearer rk_live_RESTRICTED_KEY
Content-Type: application/x-www-form-urlencoded
payment_intent=pi_XXXXX
&amount=14999
&reason=requested_by_customer
&metadata[shopify_refund_id]=SHOPIFY_REFUND_ID
Note that Stripe amounts are in the currency's smallest unit (cents for USD), so a $149.99 refund must be sent as 14999.
Common Pitfalls & Troubleshooting
A 402 Payment Required from Stripe on a PaymentIntent creation indicates the card was declined. This is not an integration error but an expected business condition. Your integration must handle this gracefully by updating the Shopify order with a note attribute indicating the Stripe decline reason and, if applicable, triggering a dunning notification workflow.
A 422 Unprocessable Entity from Stripe on a Customer creation attempt usually means a field value is malformed—most commonly the email field not being a valid email format, or a phone number not conforming to E.164 format. Sanitize all customer data extracted from Shopify before sending it to Stripe. Shopify permits phone numbers in many formats that Stripe will reject.
429 Too Many Requests from the Stripe API is governed by Stripe's rate limit of 100 read and 100 write requests per second in live mode, with burst allowances. Under high-order-volume conditions (e.g., flash sales), your webhook processor may receive hundreds of Shopify order events simultaneously. Implement a queue-based architecture—using a service like AWS SQS, Redis Queue, or Make's built-in queue functionality—to process Shopify webhooks serially rather than in parallel, keeping your Stripe API call rate within limits.
Shopify webhook delivery is not guaranteed to be exactly-once. The same order event may be delivered more than once. Implement idempotency by checking your datastore for the Shopify order ID before processing. Use Stripe's idempotency key feature on all write requests by passing the Idempotency-Key: shopify_order_ORDER_ID header; Stripe will return the same response for duplicate requests with the same idempotency key within 24 hours.