Commerce API Reference
Commerce API Reference
Version: 2.0 Last Updated: February 28, 2026 Status: Production Ready
Table of Contents
- Overview
- Authentication
- Base URLs
- Webhook Endpoints
- QR Code Generation
- Error Codes
- Request/Response Schemas
- Integration Examples
- Testing & Development
- Troubleshooting
Overview
The Commerce feature enables payment processing and order management through multiple providers (Shopify, Stripe, Mercado Pago). It follows a hexagonal architecture pattern with pluggable adapters, ensuring provider-agnostic core business logic.
Key Features:
- ✅ Multi-provider support (Shopify, Stripe, Mercado Pago)
- ✅ Webhook signature verification (HMAC)
- ✅ Automatic deduplication
- ✅ PII redaction for compliance
- ✅ GDPR-compliant data handling
- ✅ QR code generation for orders/payments
- ✅ Idempotency guards
Authentication
All API requests require authentication via API keys. Webhook endpoints use provider-specific signature verification.
API Key Authentication
Header:
Authorization: Bearer YOUR_API_KEYWebhook Signature Verification
Each provider uses different signature mechanisms:
| Provider | Header | Algorithm |
|---|---|---|
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 (Base64) |
| Stripe | Stripe-Signature | HMAC-SHA256 (Hex) |
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://api.yourapp.com |
| Staging | https://api-staging.yourapp.com |
| Development | http://localhost:8000 |
Webhook Endpoints
Shopify Webhooks
POST /api/webhooks/shopify
Receives webhook events from Shopify including orders, inventory updates, and GDPR compliance events.
Request Headers
POST /api/webhooks/shopify HTTP/1.1Host: api.yourapp.comContent-Type: application/jsonX-Shopify-Topic: orders/createX-Shopify-Hmac-Sha256: base64_encoded_hmac_signatureX-Shopify-Webhook-Id: 1234567890X-Shopify-Shop-Domain: pactapay.myshopify.comX-Shopify-API-Version: 2024-04Request Body (orders/create)
{ "id": 820982911946154508, "email": "customer@example.com", "created_at": "2026-02-28T10:30:00-05:00", "updated_at": "2026-02-28T10:30:00-05:00", "total_price": "199.00", "subtotal_price": "199.00", "total_tax": "0.00", "currency": "USD", "financial_status": "paid", "fulfillment_status": null, "line_items": [ { "id": 466157049, "variant_id": 447654529, "title": "Premium Plan - Monthly", "quantity": 1, "price": "199.00", "sku": "PREMIUM-MONTHLY", "fulfillment_status": null } ], "shipping_address": { "first_name": "John", "last_name": "Doe", "address1": "123 Main St", "city": "San Francisco", "province": "California", "country": "United States", "zip": "94102" }}Response (Success)
HTTP/1.1 200 OKContent-Type: application/json
{ "status": "ok"}Response (Duplicate)
HTTP/1.1 409 ConflictContent-Type: application/json
{ "detail": "Duplicate webhook"}Response (Invalid Signature)
HTTP/1.1 401 UnauthorizedContent-Type: application/json
{ "detail": "Invalid HMAC signature"}Supported Shopify Events
| Event Topic | Description | Action |
|---|---|---|
orders/create | New order created | Generate QR code, initiate fulfillment |
orders/updated | Order details changed | Update internal order status |
orders/paid | Payment confirmed | Trigger fulfillment workflow |
orders/fulfilled | Order shipped | Update tracking information |
orders/cancelled | Order cancelled | Refund processing, inventory update |
customers/data_request | GDPR data request | Export customer data within 30 days |
customers/redact | GDPR deletion request | Anonymize customer data within 30 days |
shop/redact | Shop uninstall cleanup | Remove all shop-related data |
GDPR Webhook Payloads
customers/data_request
{ "shop_id": 954889, "shop_domain": "pactapay.myshopify.com", "orders_requested": [299938, 280263], "customer": { "id": 191167, "email": "customer@example.com", "phone": "+15555551234" }}customers/redact
{ "shop_id": 954889, "shop_domain": "pactapay.myshopify.com", "customer": { "id": 191167, "email": "customer@example.com", "phone": "+15555551234" }, "orders_to_redact": [299938, 280263]}shop/redact
{ "shop_id": 954889, "shop_domain": "pactapay.myshopify.com"}Implementation Example (Python)
import jsonimport hmacimport hashlibimport base64from fastapi import APIRouter, Header, HTTPException, Request
router = APIRouter(prefix="/webhooks/shopify", tags=["webhooks"])
def verify_shopify_hmac(payload: bytes, signature: str, secret: str) -> bool: """Verify Shopify HMAC signature.""" computed = base64.b64encode( hmac.new(secret.encode(), payload, hashlib.sha256).digest() ).decode() return hmac.compare_digest(computed, signature)
@router.post("")async def handle_shopify_webhook( request: Request, x_shopify_topic: str = Header(...), x_shopify_hmac_sha256: str = Header(...), x_shopify_webhook_id: str = Header(None), x_shopify_shop_domain: str = Header(None)): # Read raw body body = await request.body()
# Verify signature secret = os.getenv("SHOPIFY_API_SECRET") if not verify_shopify_hmac(body, x_shopify_hmac_sha256, secret): raise HTTPException(status_code=401, detail="Invalid HMAC signature")
# Parse payload data = json.loads(body)
# Route by topic if x_shopify_topic == "orders/create": await handle_order_created(data) elif x_shopify_topic == "customers/data_request": await handle_gdpr_data_request(data) elif x_shopify_topic == "customers/redact": await handle_gdpr_redaction(data) elif x_shopify_topic == "shop/redact": await handle_shop_redaction(data)
return {"status": "ok"}Stripe Webhooks
POST /api/webhooks/stripe
Receives webhook events from Stripe for payment processing and checkout sessions.
Request Headers
POST /api/webhooks/stripe HTTP/1.1Host: api.yourapp.comContent-Type: application/jsonStripe-Signature: t=1614556800,v1=hex_signature,v0=fallback_signatureRequest Body (payment_intent.succeeded)
{ "id": "evt_1MqQX8LkdIwHu7ix7xjLQ6Pj", "object": "event", "api_version": "2024-04-10", "created": 1677594618, "type": "payment_intent.succeeded", "data": { "object": { "id": "pi_3MqQX8LkdIwHu7ix7xjLQ6Pj", "object": "payment_intent", "amount": 19900, "amount_received": 19900, "currency": "usd", "status": "succeeded", "metadata": { "pact_id": "pact_abc123", "type": "workflow_token", "fulfillment_cycle": "deferred" }, "payment_method": "pm_1MqQX8LkdIwHu7ix7xjLQ6Pj", "receipt_email": "customer@example.com", "transfer_group": "pact_abc123" } }}Response (Success)
HTTP/1.1 200 OKContent-Type: application/json
{ "status": "ok"}Response (Invalid Signature)
HTTP/1.1 401 UnauthorizedContent-Type: application/json
{ "detail": "Invalid Stripe signature"}Supported Stripe Events
| Event Type | Description | Action |
|---|---|---|
payment_intent.succeeded | Payment completed | Release funds, fulfill order |
payment_intent.payment_failed | Payment failed | Notify customer, retry logic |
payment_intent.canceled | Payment canceled | Cancel order, update status |
charge.refunded | Refund processed | Update order, credit customer |
checkout.session.completed | Checkout session completed | Create order record |
invoice.payment_succeeded | Subscription payment succeeded | Extend subscription |
Implementation Example (Python)
import jsonimport osimport hmacimport hashlibfrom fastapi import APIRouter, Header, HTTPException, Request
router = APIRouter(prefix="/webhooks/stripe", tags=["webhooks"])
def verify_stripe_signature(payload: bytes, signature: str, secret: str) -> bool: """Verify Stripe webhook signature.""" # Extract timestamp and signatures elements = dict(item.split('=') for item in signature.split(',')) timestamp = elements.get('t') v1_sig = elements.get('v1')
# Construct signed payload signed_payload = f"{timestamp}.{payload.decode()}" expected = hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest()
return hmac.compare_digest(expected, v1_sig)
@router.post("")async def handle_stripe_webhook( request: Request, stripe_signature: str = Header(...)): body = await request.body() secret = os.getenv("STRIPE_WEBHOOK_SECRET")
if not verify_stripe_signature(body, stripe_signature, secret): raise HTTPException(status_code=401, detail="Invalid Stripe signature")
data = json.loads(body) event_type = data.get("type") event_data = data.get("data", {}).get("object", {})
if event_type == "payment_intent.succeeded": pact_id = event_data.get("metadata", {}).get("pact_id") await handle_payment_success(pact_id, event_data) elif event_type == "payment_intent.payment_failed": await handle_payment_failure(event_data)
return {"status": "ok"}QR Code Generation
The Commerce feature includes a comprehensive QR code generation service for orders, payments, and tracking.
QR Code Service API
Generate Order Tracking QR Code
from commerce.infrastructure.qr_service import QRCodeService, QRCodeFormat, QRErrorCorrectionLevel
# Initialize serviceqr_service = QRCodeService( base_url="https://yourapp.com", box_size=10, border=4, error_correction=QRErrorCorrectionLevel.HIGH)
# Generate order QR codeqr_bytes = qr_service.generate_order_qr( order_id="ORD-2026-ABC123", format=QRCodeFormat.PNG, error_correction=QRErrorCorrectionLevel.HIGH)
# Save to filewith open("order_qr.png", "wb") as f: f.write(qr_bytes)Generated URL: https://yourapp.com/orders/ORD-2026-ABC123/track
Generate Payment QR Code
# Generate payment QR code with amountqr_bytes = qr_service.generate_payment_qr( order_id="ORD-2026-ABC123", amount=199.00, currency="USD", format=QRCodeFormat.PNG)Generated URL: https://yourapp.com/pay/ORD-2026-ABC123?amount=199.00¤cy=USD
Generate Package Tracking QR Code
# Generate tracking QR codeqr_bytes = qr_service.generate_tracking_qr( tracking_number="1Z999AA10123456784", format=QRCodeFormat.SVG)Generated URL: https://yourapp.com/tracking/1Z999AA10123456784
Generate Custom QR Code
# Generate custom data QR codeqr_bytes = qr_service.generate_custom_qr( data="https://custom-url.com/special-offer?code=SAVE20", format=QRCodeFormat.PNG, error_correction=QRErrorCorrectionLevel.MEDIUM)QR Code Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
base_url | string | Required | Base URL for generated links |
box_size | int | 10 | Size of each QR box in pixels |
border | int | 4 | Border size in boxes |
error_correction | enum | HIGH | Error correction level |
Error Correction Levels
| Level | Damage Tolerance | Use Case |
|---|---|---|
LOW (L) | 7% | Clean, large QR codes |
MEDIUM (M) | 15% | General purpose |
QUARTILE (Q) | 25% | Moderate damage risk |
HIGH (H) | 30% | Printed materials, damaged surfaces |
Output Formats
- PNG: Raster image, suitable for web/mobile
- SVG: Vector graphics, scalable for print
Error Codes
HTTP Status Codes
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request successful |
| 400 | Bad Request | Invalid request payload |
| 401 | Unauthorized | Invalid signature or API key |
| 404 | Not Found | Resource not found |
| 409 | Conflict | Duplicate webhook (idempotency) |
| 422 | Unprocessable Entity | Validation error |
| 500 | Internal Server Error | Server error |
| 503 | Service Unavailable | Provider API unavailable |
Application Error Codes
| Code | Message | Resolution |
|---|---|---|
COMMERCE_001 | Invalid payment amount | Amount must be positive integer in cents |
COMMERCE_002 | Currency not supported | Use USD, EUR, GBP, MXN only |
COMMERCE_003 | Provider API timeout | Retry with exponential backoff (1s, 2s, 4s) |
COMMERCE_004 | Invalid webhook signature | Verify webhook secret matches provider dashboard |
COMMERCE_005 | Payment already processed | Check idempotency, do not retry |
COMMERCE_006 | Order not found | Verify order_id exists in system |
COMMERCE_007 | GDPR request failed | Check data export configuration |
COMMERCE_008 | QR generation failed | Validate input parameters (length, format) |
COMMERCE_009 | Webhook deduplication failed | Check Redis/cache service availability |
COMMERCE_010 | PII redaction error | Review log redaction configuration |
Request/Response Schemas
Shopify Order Webhook Schema
interface ShopifyOrderWebhook { id: number; email: string; created_at: string; // ISO 8601 updated_at: string; // ISO 8601 total_price: string; // Decimal as string subtotal_price: string; total_tax: string; currency: string; // ISO 4217 financial_status: "pending" | "authorized" | "paid" | "refunded" | "voided"; fulfillment_status: "fulfilled" | "partial" | null; line_items: LineItem[]; shipping_address: Address; billing_address: Address;}
interface LineItem { id: number; variant_id: number; title: string; quantity: number; price: string; sku: string; fulfillment_status: "fulfilled" | null;}
interface Address { first_name: string; last_name: string; address1: string; address2?: string; city: string; province: string; country: string; zip: string; phone?: string;}Stripe Payment Intent Schema
interface StripePaymentIntent { id: string; // pi_* object: "payment_intent"; amount: number; // Cents amount_received: number; currency: string; status: | "requires_payment_method" | "requires_confirmation" | "requires_action" | "processing" | "succeeded" | "canceled"; metadata: { pact_id: string; type: string; fulfillment_cycle: string; }; payment_method: string; // pm_* receipt_email?: string; transfer_group?: string;}QR Code Service Schema
interface QRCodeRequest { order_id?: string; tracking_number?: string; data?: string; amount?: number; currency?: string; format: "PNG" | "SVG"; error_correction: "L" | "M" | "Q" | "H";}
interface QRCodeResponse { data: string; // Base64 encoded image format: string; url: string; // Generated URL embedded in QR}Integration Examples
React Client Integration
Hook for Shopify Checkout
import { useState } from "react";
interface CheckoutOptions { lineItems: Array<{ variantId: string; quantity: number; }>; returnUrl: string;}
export function useShopifyCheckout() { const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null);
async function createCheckout(options: CheckoutOptions) { setLoading(true); setError(null);
try { const response = await fetch("/api/commerce/checkout", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("token")}`, }, body: JSON.stringify(options), });
if (!response.ok) { throw new Error(`Checkout failed: ${response.statusText}`); }
const data = await response.json();
// Redirect to Shopify checkout window.location.href = data.checkout_url; } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); console.error("Checkout error:", err); } finally { setLoading(false); } }
return { createCheckout, loading, error };}Component Usage
import React from "react";import { useShopifyCheckout } from "../hooks/useShopifyCheckout";
export function CheckoutButton({ variantId }: { variantId: string }) { const { createCheckout, loading } = useShopifyCheckout();
const handleCheckout = () => { createCheckout({ lineItems: [{ variantId, quantity: 1 }], returnUrl: window.location.origin + "/checkout/success", }); };
return ( <button onClick={handleCheckout} disabled={loading} className="btn-primary"> {loading ? "Processing..." : "Buy Now"} </button> );}Rails Client Integration
Payment Service
module Commerce class PaymentService include HTTParty base_uri ENV['API_BASE_URL']
def initialize @options = { headers: { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{ENV['API_KEY']}" } } end
def create_checkout(line_items:, return_url:) response = self.class.post( '/api/commerce/checkout', @options.merge( body: { line_items: line_items, return_url: return_url }.to_json ) )
handle_response(response) end
private
def handle_response(response) case response.code when 200..299 JSON.parse(response.body) when 401 raise UnauthorizedError, 'Invalid API key' when 400 raise ValidationError, response.parsed_response['message'] else raise ApiError, "Unexpected error: #{response.code}" end end end
class ApiError < StandardError; end class UnauthorizedError < ApiError; end class ValidationError < ApiError; endendController Usage
class CheckoutController < ApplicationController def create service = Commerce::PaymentService.new
result = service.create_checkout( line_items: params[:line_items], return_url: checkout_success_url )
redirect_to result['checkout_url'] rescue Commerce::ValidationError => e flash[:error] = e.message redirect_back fallback_location: root_path rescue Commerce::ApiError => e logger.error "Checkout failed: #{e.message}" flash[:error] = 'Checkout failed. Please try again.' redirect_back fallback_location: root_path end
def success # Handle successful checkout return @order = Order.find_by(session_id: params[:session_id]) render :success endendPython Backend Integration
Service Layer
import osimport httpxfrom typing import Dict, Any, List
class CommerceService: """Service for interacting with commerce providers."""
def __init__(self): self.base_url = os.getenv("API_BASE_URL") self.api_key = os.getenv("API_KEY") self.client = httpx.AsyncClient( base_url=self.base_url, headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" }, timeout=30.0 )
async def create_checkout( self, line_items: List[Dict[str, Any]], return_url: str ) -> Dict[str, Any]: """Create checkout session.""" try: response = await self.client.post( "/api/commerce/checkout", json={ "line_items": line_items, "return_url": return_url } ) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: if e.response.status_code == 401: raise UnauthorizedError("Invalid API key") elif e.response.status_code == 400: raise ValidationError(e.response.json().get("message")) else: raise CommerceAPIError(f"Unexpected error: {e.response.status_code}")
async def get_order_status(self, order_id: str) -> Dict[str, Any]: """Get order status.""" response = await self.client.get(f"/api/commerce/orders/{order_id}") response.raise_for_status() return response.json()
async def close(self): """Close HTTP client.""" await self.client.aclose()
class CommerceAPIError(Exception): pass
class UnauthorizedError(CommerceAPIError): pass
class ValidationError(CommerceAPIError): passTesting & Development
Local Development Setup
1. Environment Variables
SHOPIFY_SHOP_NAME=pactapaySHOPIFY_API_SECRET=shpss_your_secret_keySHOPIFY_ACCESS_TOKEN=shpat_your_access_token
STRIPE_SECRET_KEY=sk_test_your_secret_keySTRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
COMMERCE_IDEMPOTENCY_ENABLED=trueCOMMERCE_PII_REDACTION_ENABLED=trueWEBHOOK_DEDUPE_TTL_SECONDS=36002. ngrok for Webhook Testing
# Start your local serveruvicorn main:app --reload --port 8000
# In another terminal, start ngrokngrok http 8000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)3. Configure Webhooks in Provider Dashboard
Shopify:
- Go to Settings > Notifications > Webhooks
- Add webhook:
https://abc123.ngrok.io/api/webhooks/shopify - Subscribe to events:
orders/create,orders/paid,customers/data_request, etc. - Copy webhook signing secret
Stripe:
- Go to Developers > Webhooks
- Add endpoint:
https://abc123.ngrok.io/api/webhooks/stripe - Select events:
payment_intent.succeeded,charge.refunded, etc. - Copy webhook secret
Manual Webhook Testing
Test Shopify Webhook with curl
#!/bin/bashWEBHOOK_URL="http://localhost:8000/api/webhooks/shopify"SECRET="your_shopify_secret"PAYLOAD='{"id":123456,"email":"test@example.com","total_price":"199.00"}'
# Generate HMACHMAC=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
curl -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -H "X-Shopify-Topic: orders/create" \ -H "X-Shopify-Hmac-Sha256: $HMAC" \ -H "X-Shopify-Webhook-Id: test-webhook-001" \ -H "X-Shopify-Shop-Domain: test.myshopify.com" \ -d "$PAYLOAD"Test Stripe Webhook with Stripe CLI
# Install Stripe CLIbrew install stripe/stripe-cli/stripe
# Loginstripe login
# Forward webhooks to local serverstripe listen --forward-to localhost:8000/api/webhooks/stripe
# Trigger test eventsstripe trigger payment_intent.succeededstripe trigger charge.refundedUnit Testing
Python Unit Tests
import pytestfrom fastapi.testclient import TestClientfrom main import app
client = TestClient(app)
def test_shopify_webhook_valid_signature(): """Test Shopify webhook with valid HMAC.""" payload = b'{"id":123,"total_price":"199.00"}' signature = generate_shopify_hmac(payload, "test_secret")
response = client.post( "/api/webhooks/shopify", content=payload, headers={ "X-Shopify-Topic": "orders/create", "X-Shopify-Hmac-Sha256": signature, "X-Shopify-Webhook-Id": "test-001" } )
assert response.status_code == 200 assert response.json() == {"status": "ok"}
def test_shopify_webhook_invalid_signature(): """Test Shopify webhook with invalid HMAC.""" payload = b'{"id":123}'
response = client.post( "/api/webhooks/shopify", content=payload, headers={ "X-Shopify-Topic": "orders/create", "X-Shopify-Hmac-Sha256": "invalid_signature" } )
assert response.status_code == 401
def test_webhook_deduplication(): """Test webhook deduplication prevents duplicate processing.""" payload = b'{"id":123}' signature = generate_shopify_hmac(payload, "test_secret") headers = { "X-Shopify-Topic": "orders/create", "X-Shopify-Hmac-Sha256": signature, "X-Shopify-Webhook-Id": "test-002" }
# First request succeeds response1 = client.post("/api/webhooks/shopify", content=payload, headers=headers) assert response1.status_code == 200
# Duplicate request returns 409 response2 = client.post("/api/webhooks/shopify", content=payload, headers=headers) assert response2.status_code == 409Rails RSpec Tests
require 'rails_helper'
RSpec.describe Webhooks::ShopifyController, type: :controller do describe 'POST #handle' do let(:valid_payload) { { id: 123, total_price: '199.00' }.to_json } let(:secret) { 'test_secret' }
before do allow(ENV).to receive(:[]).with('SHOPIFY_API_SECRET').and_return(secret) end
context 'with valid HMAC' do it 'processes webhook successfully' do hmac = generate_hmac(valid_payload, secret) request.headers['X-Shopify-Hmac-Sha256'] = hmac request.headers['X-Shopify-Topic'] = 'orders/create'
post :handle, body: valid_payload
expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)['status']).to eq('ok') end end
context 'with invalid HMAC' do it 'rejects webhook' do request.headers['X-Shopify-Hmac-Sha256'] = 'invalid' request.headers['X-Shopify-Topic'] = 'orders/create'
post :handle, body: valid_payload
expect(response).to have_http_status(:unauthorized) end end endendQR Code Testing
import pytestfrom commerce.infrastructure.qr_service import ( QRCodeService, QRCodeFormat, QRErrorCorrectionLevel)
@pytest.fixturedef qr_service(): return QRCodeService(base_url="https://test.com")
def test_generate_order_qr(qr_service): """Test order QR code generation.""" qr_bytes = qr_service.generate_order_qr( order_id="ORD-123", format=QRCodeFormat.PNG )
assert isinstance(qr_bytes, bytes) assert len(qr_bytes) > 0 assert qr_bytes[:8] == b'\x89PNG\r\n\x1a\n' # PNG magic bytes
def test_generate_payment_qr(qr_service): """Test payment QR code generation.""" qr_bytes = qr_service.generate_payment_qr( order_id="ORD-123", amount=199.00, currency="USD" )
assert isinstance(qr_bytes, bytes) assert len(qr_bytes) > 0
def test_invalid_order_id(qr_service): """Test validation for invalid order ID.""" with pytest.raises(ValueError, match="order_id must be a non-empty string"): qr_service.generate_order_qr(order_id="")
def test_invalid_amount(qr_service): """Test validation for invalid amount.""" with pytest.raises(ValueError, match="amount must be positive"): qr_service.generate_payment_qr( order_id="ORD-123", amount=-10.00 )Troubleshooting
Common Issues
Issue 1: Webhook Not Received
Symptoms:
- Webhook endpoint not being called
- Events show as “delivered” in provider dashboard but not processed
Solutions:
-
Verify endpoint is publicly accessible
Terminal window curl -I https://yourdomain.com/api/webhooks/shopify# Should return HTTP 405 Method Not Allowed (POST expected) -
Check provider dashboard delivery logs
- Shopify: Settings > Notifications > Webhooks > View delivery history
- Stripe: Developers > Webhooks > [Your endpoint] > Events
-
Use ngrok for local development
Terminal window ngrok http 8000# Update webhook URL in provider dashboard to ngrok HTTPS URL -
Check firewall and rate limiting
- Ensure webhook endpoint excluded from rate limits
- Whitelist provider IP ranges if using firewall
Issue 2: Invalid Signature Error
Symptoms:
- HTTP 401 Unauthorized
- “Invalid HMAC signature” or “Invalid Stripe signature”
Solutions:
-
Verify webhook secret matches
Terminal window # Check environment variableecho $SHOPIFY_API_SECRETecho $STRIPE_WEBHOOK_SECRET# Compare with provider dashboard -
Ensure raw body is used for verification
# ✅ Correct: Use raw bytesbody = await request.body()verify_signature(body, signature, secret)# ❌ Wrong: Parsed JSONdata = await request.json()verify_signature(data, signature, secret) -
Check signature extraction
# Stripe signature format: t=timestamp,v1=signature# Ensure proper parsingelements = dict(item.split('=') for item in signature.split(','))
Issue 3: Duplicate Webhooks
Symptoms:
- Same webhook processed multiple times
- HTTP 409 Conflict on subsequent requests
Solutions:
-
Enable deduplication
Terminal window COMMERCE_IDEMPOTENCY_ENABLED=trueWEBHOOK_DEDUPE_TTL_SECONDS=3600 -
Check Redis/cache service
Terminal window # Test Redis connectionredis-cli ping# Should return: PONG -
Use webhook IDs for deduplication
webhook_id = request.headers.get('x-shopify-webhook-id')if is_duplicate(webhook_id):return {"status": "already_processed"}
Issue 4: GDPR Webhooks Not Handled
Symptoms:
- 48-hour deadline warning from Shopify
- GDPR webhook delivery failures
Solutions:
-
Implement mandatory webhooks
# Required Shopify GDPR webhooks- customers/data_request- customers/redact- shop/redact -
Return 200 OK immediately
@router.post("")async def handle_gdpr_webhook(data: dict):# Queue for async processingqueue.enqueue(process_gdpr_request, data)# Return immediatelyreturn {"status": "ok"} -
Process async with retry
@retry(max_attempts=3, backoff=exponential)async def process_gdpr_request(data: dict):# Export or delete customer dataawait export_customer_data(data['customer'])
Issue 5: QR Code Generation Fails
Symptoms:
- ValueError during QR generation
- Empty or corrupted QR images
Solutions:
-
Validate input parameters
# Check order_id lengthif len(order_id) > 500:raise ValueError("order_id too long")# Sanitize special charactersorder_id = order_id.replace('\n', '').replace('\r', '') -
Verify qrcode library installed
Terminal window pip install qrcode[pil] -
Test with minimal example
import qrcodeqr = qrcode.QRCode()qr.add_data('test')qr.make()img = qr.make_image()img.save('test.png')
Debug Mode
Enable detailed logging for troubleshooting:
# Enable debug loggingimport logginglogging.basicConfig(level=logging.DEBUG)
# Commerce-specific loggerlogger = logging.getLogger('commerce')logger.setLevel(logging.DEBUG)Provider Status Pages
Check provider status if webhooks failing:
- Shopify: https://www.shopifystatus.com/
- Stripe: https://status.stripe.com/
Rate Limits
| Provider | Endpoint | Rate Limit |
|---|---|---|
| Shopify | Webhooks | 2 requests/second per shop |
| Stripe | Webhooks | 100 requests/second |
Best Practices:
- Implement exponential backoff for retries
- Use idempotency keys for POST requests
- Queue webhook processing for async handling
Security Best Practices
- Always verify webhook signatures - Never trust incoming webhooks without HMAC verification
- Use HTTPS only - Providers reject HTTP webhook endpoints
- Implement rate limiting - Protect against webhook flooding attacks
- Enable PII redaction - Comply with GDPR and data protection laws
- Use idempotency guards - Prevent duplicate webhook processing
- Rotate API keys regularly - Reduce risk of compromised credentials
- Monitor webhook failures - Set up alerts for signature validation failures
Support
For additional support:
- Documentation:
/ - Issues: GitHub Issues (foundry-meta repository)
- Community: Seed & Source Discord
- Email: support@seedsource.dev
Last Updated: February 28, 2026 Version: 2.0 Maintainer: Seed & Source Team