Shopify PunchOut Integration
Voraussetzungen
- Shopify Basic Plan oder höher
- Admin-Zugriff auf Shopify
- PunchFlow Account (Registrierung)
Schnellstart (3 Minuten)
Keine App-Installation nötig!
PunchFlow verbindet sich direkt über die Shopify Admin API. Sie müssen keine App installieren - alles läuft über die Standard-API.
Schritt 1: Private App erstellen
- Shopify Admin öffnen
- Apps → App-Entwicklung
- "Private App erstellen" klicken
- Konfiguration:
Name: PunchFlow Connector
Admin API-Berechtigungen:
✅ Products (Read)
✅ Customers (Read/Write)
✅ Orders (Read/Write)
✅ Draft Orders (Read/Write)
✅ Inventory (Read)
✅ Price Rules (Read) - App erstellen und Admin API Access Token kopieren
Schritt 2: PunchFlow konfigurieren
# Via API
curl -X POST "https://api.punchflow.de/api/v1/connectors?merchant_id=<YOUR_MERCHANT_ID>" \
-H "Authorization: Bearer YOUR_PUNCHFLOW_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "shopify",
"name": "Shopify Connector",
"api_url": "https://ihr-shop.myshopify.com",
"authentication": {
"auth_type": "api_key",
"api_key": "shpat_xxxxx"
},
"custom_settings": {
"api_version": "2024-01",
"location_id": "primary"
}
}'
Erweiterte Konfiguration
API-Konfiguration
# PunchFlow-Einstellungen (in app.punchflow.de)
shop_connection:
type: "shopify"
api_version: "2024-01"
rate_limit_buffer: 0.8 # 80% des Limits nutzen
# PunchOut-Einstellungen
punchout:
session_timeout: 3600 # Sekunden
allowed_protocols:
- cxml
- oci
auto_login: true
use_draft_orders: true # Draft Orders für PunchOut nutzen
# Produkt-Mapping
mapping:
product_id_field: "sku" # oder "handle", "barcode"
price_calculation: "compare_at_price"
stock_management: true
include_variants: true
# Kunden-Einstellungen
customers:
auto_create: true
tag_punchout_customers: true
default_tags:
- "b2b"
- "punchout"
# Logging
debug:
enabled: false
log_level: "info"
Webhook Konfiguration
# Webhooks für Echtzeit-Updates registrieren
curl -X POST "https://ihr-shop.myshopify.com/admin/api/2024-01/webhooks.json" \
-H "X-Shopify-Access-Token: shpat_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"webhook": {
"topic": "orders/create",
"address": "https://api.punchflow.de/webhooks/shopify/orders",
"format": "json"
}
}'
Workflow-Integration
1. Session-Start
sequenceDiagram
ERP->>PunchFlow: PunchOut Setup Request
PunchFlow->>Shopify: Customer anlegen/finden
Shopify->>PunchFlow: Customer ID
PunchFlow->>Shopify: Draft Order erstellen
Shopify->>PunchFlow: Draft Order ID + Invoice URL
PunchFlow->>ERP: Setup Response mit URL
ERP->>Shopify: User Redirect zum Shop
2. Draft Order Flow
PunchFlow nutzt Shopify Draft Orders für PunchOut-Warenkörbe:
// Draft Order erstellen
POST /admin/api/2024-01/draft_orders.json
{
"draft_order": {
"line_items": [],
"customer": {
"id": 123456789
},
"tags": "punchout,session:abc123",
"note_attributes": [
{
"name": "punchout_session_id",
"value": "abc123"
},
{
"name": "buyer_cookie",
"value": "xyz789"
}
]
}
}
3. Warenkorb-Transfer
// Draft Order zu cXML konvertieren
async function transferDraftOrder(draftOrderId, session) {
const draftOrder = await shopify.draftOrder.get(draftOrderId);
const cxml = buildPunchOutOrderMessage({
buyerCookie: session.buyerCookie,
lineItems: draftOrder.line_items.map(item => ({
sku: item.sku,
name: item.name,
quantity: item.quantity,
unitPrice: item.price,
currency: draftOrder.currency
})),
total: draftOrder.total_price
});
return cxml;
}
GraphQL API
Produkt-Abfragen
query GetProducts($first: Int!, $query: String) {
products(first: $first, query: $query) {
edges {
node {
id
handle
title
description
vendor
productType
tags
variants(first: 100) {
edges {
node {
id
sku
title
price
compareAtPrice
inventoryQuantity
barcode
}
}
}
images(first: 5) {
edges {
node {
url
altText
}
}
}
priceRangeV2 {
minVariantPrice {
amount
currencyCode
}
maxVariantPrice {
amount
currencyCode
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Kunden-Management
mutation CreateCustomer($input: CustomerInput!) {
customerCreate(input: $input) {
customer {
id
email
firstName
lastName
tags
}
userErrors {
field
message
}
}
}
# Input
{
"input": {
"email": "buyer@company.com",
"firstName": "Max",
"lastName": "Mustermann",
"tags": ["punchout", "b2b", "ariba"],
"metafields": [
{
"namespace": "punchflow",
"key": "buyer_id",
"value": "DUNS:123456789",
"type": "single_line_text_field"
}
]
}
}
Produkt-Synchronisation
Bulk Operations
mutation BulkProductExport {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
handle
title
variants {
edges {
node {
sku
price
inventoryQuantity
}
}
}
}
}
}
}
"""
) {
bulkOperation {
id
status
}
userErrors {
field
message
}
}
}
Inventory Sync
// Lagerbestand abrufen
async function syncInventory() {
const locations = await shopify.location.list();
const primaryLocation = locations[0];
const inventoryLevels = await shopify.inventoryLevel.list({
location_ids: primaryLocation.id
});
return inventoryLevels.map(level => ({
inventoryItemId: level.inventory_item_id,
available: level.available,
locationId: level.location_id
}));
}
B2B-Funktionen (Shopify Plus)
Wholesale Pricing
// B2B Preise über Price Rules
async function applyB2BPricing(customerId, lineItems) {
const priceRules = await shopify.priceRule.list({
customer_segment_id: customerId
});
return lineItems.map(item => {
const rule = priceRules.find(r =>
r.entitled_product_ids.includes(item.product_id)
);
if (rule) {
return {
...item,
price: applyDiscount(item.price, rule)
};
}
return item;
});
}
Company Accounts (Shopify Plus)
// Company Account erstellen
POST /admin/api/2024-01/companies.json
{
"company": {
"name": "Acme Corp",
"company_contacts": [
{
"customer": {
"email": "buyer@acme.com"
}
}
],
"locations": [
{
"name": "Headquarters",
"billing_address": {
"address1": "123 Business St",
"city": "Berlin",
"country": "DE"
}
}
]
}
}
Frontend-Anpassungen
Theme Customization
{% comment %} sections/punchout-cart.liquid {% endcomment %}
{% if customer.tags contains 'punchout' %}
<div class="punchout-banner">
<p>Sie befinden sich in einer PunchOut-Sitzung</p>
<p>Käufer: {{ customer.metafields.punchflow.buyer_id }}</p>
</div>
{% section 'punchout-cart-actions' %}
{% endif %}
JavaScript Integration
// assets/punchout.js
class PunchOutSession {
constructor() {
this.sessionId = this.getSessionId();
if (this.sessionId) {
this.init();
}
}
init() {
this.hidePaymentMethods();
this.addTransferButton();
this.modifyCheckout();
}
hidePaymentMethods() {
document.querySelectorAll('[data-payment-method]')
.forEach(el => el.style.display = 'none');
}
addTransferButton() {
const cartForm = document.querySelector('form[action="/cart"]');
if (!cartForm) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn punchout-transfer-btn';
btn.textContent = 'Warenkorb an ERP übertragen';
btn.onclick = () => this.transfer();
cartForm.appendChild(btn);
}
async transfer() {
const response = await fetch('/apps/punchflow/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-PunchOut-Session': this.sessionId
}
});
if (response.ok) {
const data = await response.json();
window.location.href = data.redirect_url;
}
}
}
new PunchOutSession();
App Proxy (Optional)
Einrichtung
# App Proxy in Shopify Partner Dashboard konfigurieren
Subpath prefix: /apps/punchflow
Proxy URL: https://api.punchflow.de/shopify/proxy
Endpoints
// PunchFlow App Proxy Endpoints
GET /apps/punchflow/session/:id // Session Status
POST /apps/punchflow/transfer // Cart Transfer
GET /apps/punchflow/products // Produkt-Suche (für ERP)
Testing
Test-Umgebung
# Development Store erstellen
# https://partners.shopify.com
# Test-Daten importieren
shopify product import test-products.csv
# Test-Session erstellen
curl -X POST "https://api.punchflow.de/api/v1/punchout/test" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"shop_url": "https://test-store.myshopify.com",
"protocol": "cxml"
}'
Automatische Tests
// tests/punchout.test.js
describe('PunchOut Flow', () => {
test('should create session', async () => {
const session = await punchflow.createSession({
shopUrl: 'https://test.myshopify.com',
buyerEmail: 'test@example.com'
});
expect(session.id).toBeDefined();
expect(session.draftOrderId).toBeDefined();
});
test('should transfer cart', async () => {
const cxml = await punchflow.transferCart({
sessionId: 'test-session',
draftOrderId: '123456'
});
expect(cxml).toContain('<PunchOutOrderMessage>');
expect(cxml).toContain('<BuyerCookie>');
});
});
Monitoring
Webhook Events
// Wichtige Events loggen
const webhookHandlers = {
'orders/create': async (order) => {
if (order.tags?.includes('punchout')) {
await logPunchOutOrder(order);
}
},
'draft_orders/update': async (draftOrder) => {
if (draftOrder.tags?.includes('punchout')) {
await syncPunchOutCart(draftOrder);
}
}
};
Rate Limiting
// Rate Limit Monitoring
class ShopifyRateLimiter {
constructor() {
this.callLimit = 40; // Standard: 40 calls/second
this.callsMade = 0;
}
async throttle(fn) {
if (this.callsMade >= this.callLimit * 0.8) {
await this.wait(1000);
this.callsMade = 0;
}
this.callsMade++;
return fn();
}
}
Troubleshooting
Häufige Probleme
Problem: API Rate Limit erreicht
# Rate Limit Status prüfen
curl -I "https://shop.myshopify.com/admin/api/2024-01/products.json" \
-H "X-Shopify-Access-Token: shpat_xxx"
# Response Headers:
# X-Shopify-Shop-Api-Call-Limit: 39/40
Problem: Draft Order nicht gefunden
// Draft Orders mit PunchOut-Tag suchen
const draftOrders = await shopify.draftOrder.list({
status: 'open',
tag: 'punchout'
});
Problem: Customer Metafields leer
// Metafields explizit abfragen
const customer = await shopify.customer.get(customerId, {
fields: 'id,email,metafields'
});
Performance-Optimierung
Caching-Strategie
// Redis Cache für Produkte
const cacheProduct = async (product) => {
await redis.setex(
`product:${product.handle}`,
3600,
JSON.stringify(product)
);
};
const getCachedProduct = async (handle) => {
const cached = await redis.get(`product:${handle}`);
return cached ? JSON.parse(cached) : null;
};
Bulk Operations nutzen
// Statt einzelner Requests: Bulk Queries
const bulkOperation = await shopify.graphql(`
mutation {
bulkOperationRunQuery(query: "{ products { edges { node { id title } } } }") {
bulkOperation { id status }
}
}
`);
// Poll bis fertig
while (true) {
const status = await shopify.bulkOperation.get(bulkOperation.id);
if (status.status === 'COMPLETED') {
const results = await fetch(status.url);
break;
}
await sleep(1000);
}
Weitere Ressourcen
Best Practices
- Draft Orders nutzen statt regulärer Carts
- Metafields für PunchOut-Daten verwenden
- Rate Limiting beachten und Bulk Operations nutzen
- Webhooks für Echtzeit-Updates
- Customer Tags für einfache Filterung
- App Proxy für sichere Frontend-Integration
Support
Bei Fragen oder Problemen:
- Email: shopify@punchflow.de
- Telefon: +49 30 123 456 78
- Chat: app.punchflow.de/chat