Skip to content

Commerce API Reference

Commerce API Reference

Version: 2.0 Last Updated: February 28, 2026 Status: Production Ready

Table of Contents


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_KEY

Webhook Signature Verification

Each provider uses different signature mechanisms:

ProviderHeaderAlgorithm
ShopifyX-Shopify-Hmac-Sha256HMAC-SHA256 (Base64)
StripeStripe-SignatureHMAC-SHA256 (Hex)

Base URLs

EnvironmentBase URL
Productionhttps://api.yourapp.com
Staginghttps://api-staging.yourapp.com
Developmenthttp://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.1
Host: api.yourapp.com
Content-Type: application/json
X-Shopify-Topic: orders/create
X-Shopify-Hmac-Sha256: base64_encoded_hmac_signature
X-Shopify-Webhook-Id: 1234567890
X-Shopify-Shop-Domain: pactapay.myshopify.com
X-Shopify-API-Version: 2024-04
Request 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 OK
Content-Type: application/json
{
"status": "ok"
}
Response (Duplicate)
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"detail": "Duplicate webhook"
}
Response (Invalid Signature)
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"detail": "Invalid HMAC signature"
}
Supported Shopify Events
Event TopicDescriptionAction
orders/createNew order createdGenerate QR code, initiate fulfillment
orders/updatedOrder details changedUpdate internal order status
orders/paidPayment confirmedTrigger fulfillment workflow
orders/fulfilledOrder shippedUpdate tracking information
orders/cancelledOrder cancelledRefund processing, inventory update
customers/data_requestGDPR data requestExport customer data within 30 days
customers/redactGDPR deletion requestAnonymize customer data within 30 days
shop/redactShop uninstall cleanupRemove 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 json
import hmac
import hashlib
import base64
from 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.1
Host: api.yourapp.com
Content-Type: application/json
Stripe-Signature: t=1614556800,v1=hex_signature,v0=fallback_signature
Request 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 OK
Content-Type: application/json
{
"status": "ok"
}
Response (Invalid Signature)
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"detail": "Invalid Stripe signature"
}
Supported Stripe Events
Event TypeDescriptionAction
payment_intent.succeededPayment completedRelease funds, fulfill order
payment_intent.payment_failedPayment failedNotify customer, retry logic
payment_intent.canceledPayment canceledCancel order, update status
charge.refundedRefund processedUpdate order, credit customer
checkout.session.completedCheckout session completedCreate order record
invoice.payment_succeededSubscription payment succeededExtend subscription
Implementation Example (Python)
import json
import os
import hmac
import hashlib
from 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 service
qr_service = QRCodeService(
base_url="https://yourapp.com",
box_size=10,
border=4,
error_correction=QRErrorCorrectionLevel.HIGH
)
# Generate order QR code
qr_bytes = qr_service.generate_order_qr(
order_id="ORD-2026-ABC123",
format=QRCodeFormat.PNG,
error_correction=QRErrorCorrectionLevel.HIGH
)
# Save to file
with 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 amount
qr_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&currency=USD

Generate Package Tracking QR Code

# Generate tracking QR code
qr_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 code
qr_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

ParameterTypeDefaultDescription
base_urlstringRequiredBase URL for generated links
box_sizeint10Size of each QR box in pixels
borderint4Border size in boxes
error_correctionenumHIGHError correction level

Error Correction Levels

LevelDamage ToleranceUse 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

CodeStatusDescription
200OKRequest successful
400Bad RequestInvalid request payload
401UnauthorizedInvalid signature or API key
404Not FoundResource not found
409ConflictDuplicate webhook (idempotency)
422Unprocessable EntityValidation error
500Internal Server ErrorServer error
503Service UnavailableProvider API unavailable

Application Error Codes

CodeMessageResolution
COMMERCE_001Invalid payment amountAmount must be positive integer in cents
COMMERCE_002Currency not supportedUse USD, EUR, GBP, MXN only
COMMERCE_003Provider API timeoutRetry with exponential backoff (1s, 2s, 4s)
COMMERCE_004Invalid webhook signatureVerify webhook secret matches provider dashboard
COMMERCE_005Payment already processedCheck idempotency, do not retry
COMMERCE_006Order not foundVerify order_id exists in system
COMMERCE_007GDPR request failedCheck data export configuration
COMMERCE_008QR generation failedValidate input parameters (length, format)
COMMERCE_009Webhook deduplication failedCheck Redis/cache service availability
COMMERCE_010PII redaction errorReview 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

src/hooks/useShopifyCheckout.ts
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

src/components/CheckoutButton.tsx
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

app/services/commerce/payment_service.rb
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; end
end

Controller Usage

app/controllers/checkout_controller.rb
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
end
end

Python Backend Integration

Service Layer

src/services/commerce_service.py
import os
import httpx
from 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):
pass

Testing & Development

Local Development Setup

1. Environment Variables

.env
SHOPIFY_SHOP_NAME=pactapay
SHOPIFY_API_SECRET=shpss_your_secret_key
SHOPIFY_ACCESS_TOKEN=shpat_your_access_token
STRIPE_SECRET_KEY=sk_test_your_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
COMMERCE_IDEMPOTENCY_ENABLED=true
COMMERCE_PII_REDACTION_ENABLED=true
WEBHOOK_DEDUPE_TTL_SECONDS=3600

2. ngrok for Webhook Testing

Terminal window
# Start your local server
uvicorn main:app --reload --port 8000
# In another terminal, start ngrok
ngrok http 8000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)

3. Configure Webhooks in Provider Dashboard

Shopify:

  1. Go to Settings > Notifications > Webhooks
  2. Add webhook: https://abc123.ngrok.io/api/webhooks/shopify
  3. Subscribe to events: orders/create, orders/paid, customers/data_request, etc.
  4. Copy webhook signing secret

Stripe:

  1. Go to Developers > Webhooks
  2. Add endpoint: https://abc123.ngrok.io/api/webhooks/stripe
  3. Select events: payment_intent.succeeded, charge.refunded, etc.
  4. Copy webhook secret

Manual Webhook Testing

Test Shopify Webhook with curl

test_shopify_webhook.sh
#!/bin/bash
WEBHOOK_URL="http://localhost:8000/api/webhooks/shopify"
SECRET="your_shopify_secret"
PAYLOAD='{"id":123456,"email":"test@example.com","total_price":"199.00"}'
# Generate HMAC
HMAC=$(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

Terminal window
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:8000/api/webhooks/stripe
# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger charge.refunded

Unit Testing

Python Unit Tests

tests/test_webhooks.py
import pytest
from fastapi.testclient import TestClient
from 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 == 409

Rails RSpec Tests

spec/controllers/webhooks/shopify_controller_spec.rb
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
end
end

QR Code Testing

tests/test_qr_service.py
import pytest
from commerce.infrastructure.qr_service import (
QRCodeService, QRCodeFormat, QRErrorCorrectionLevel
)
@pytest.fixture
def 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:

  1. Verify endpoint is publicly accessible

    Terminal window
    curl -I https://yourdomain.com/api/webhooks/shopify
    # Should return HTTP 405 Method Not Allowed (POST expected)
  2. Check provider dashboard delivery logs

    • Shopify: Settings > Notifications > Webhooks > View delivery history
    • Stripe: Developers > Webhooks > [Your endpoint] > Events
  3. Use ngrok for local development

    Terminal window
    ngrok http 8000
    # Update webhook URL in provider dashboard to ngrok HTTPS URL
  4. 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:

  1. Verify webhook secret matches

    Terminal window
    # Check environment variable
    echo $SHOPIFY_API_SECRET
    echo $STRIPE_WEBHOOK_SECRET
    # Compare with provider dashboard
  2. Ensure raw body is used for verification

    # ✅ Correct: Use raw bytes
    body = await request.body()
    verify_signature(body, signature, secret)
    # ❌ Wrong: Parsed JSON
    data = await request.json()
    verify_signature(data, signature, secret)
  3. Check signature extraction

    # Stripe signature format: t=timestamp,v1=signature
    # Ensure proper parsing
    elements = 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:

  1. Enable deduplication

    Terminal window
    COMMERCE_IDEMPOTENCY_ENABLED=true
    WEBHOOK_DEDUPE_TTL_SECONDS=3600
  2. Check Redis/cache service

    Terminal window
    # Test Redis connection
    redis-cli ping
    # Should return: PONG
  3. 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:

  1. Implement mandatory webhooks

    # Required Shopify GDPR webhooks
    - customers/data_request
    - customers/redact
    - shop/redact
  2. Return 200 OK immediately

    @router.post("")
    async def handle_gdpr_webhook(data: dict):
    # Queue for async processing
    queue.enqueue(process_gdpr_request, data)
    # Return immediately
    return {"status": "ok"}
  3. Process async with retry

    @retry(max_attempts=3, backoff=exponential)
    async def process_gdpr_request(data: dict):
    # Export or delete customer data
    await export_customer_data(data['customer'])

Issue 5: QR Code Generation Fails

Symptoms:

  • ValueError during QR generation
  • Empty or corrupted QR images

Solutions:

  1. Validate input parameters

    # Check order_id length
    if len(order_id) > 500:
    raise ValueError("order_id too long")
    # Sanitize special characters
    order_id = order_id.replace('\n', '').replace('\r', '')
  2. Verify qrcode library installed

    Terminal window
    pip install qrcode[pil]
  3. Test with minimal example

    import qrcode
    qr = 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 logging
import logging
logging.basicConfig(level=logging.DEBUG)
# Commerce-specific logger
logger = logging.getLogger('commerce')
logger.setLevel(logging.DEBUG)

Provider Status Pages

Check provider status if webhooks failing:


Rate Limits

ProviderEndpointRate Limit
ShopifyWebhooks2 requests/second per shop
StripeWebhooks100 requests/second

Best Practices:

  • Implement exponential backoff for retries
  • Use idempotency keys for POST requests
  • Queue webhook processing for async handling

Security Best Practices

  1. Always verify webhook signatures - Never trust incoming webhooks without HMAC verification
  2. Use HTTPS only - Providers reject HTTP webhook endpoints
  3. Implement rate limiting - Protect against webhook flooding attacks
  4. Enable PII redaction - Comply with GDPR and data protection laws
  5. Use idempotency guards - Prevent duplicate webhook processing
  6. Rotate API keys regularly - Reduce risk of compromised credentials
  7. 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