Skip to main content

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

  1. Shopify Admin öffnen
  2. Apps → App-Entwicklung
  3. "Private App erstellen" klicken
  4. Konfiguration:
    Name: PunchFlow Connector

    Admin API-Berechtigungen:
    Products (Read)
    Customers (Read/Write)
    Orders (Read/Write)
    Draft Orders (Read/Write)
    Inventory (Read)
    Price Rules (Read)
  5. 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

  1. Draft Orders nutzen statt regulärer Carts
  2. Metafields für PunchOut-Daten verwenden
  3. Rate Limiting beachten und Bulk Operations nutzen
  4. Webhooks für Echtzeit-Updates
  5. Customer Tags für einfache Filterung
  6. App Proxy für sichere Frontend-Integration

Support

Bei Fragen oder Problemen: