Skip to content

Shopify Integration Guide

Table of Contents

  1. Overview
  2. Architecture
  3. Getting Started
  4. Setup & Configuration
  5. Order Fulfillment Integration
  6. Product Warranty Integration
  7. Webhook Integration
  8. API Reference
  9. Code Examples
  10. Best Practices
  11. Troubleshooting

Overview

The Pocketbook Shopify integration enables automated creation and delivery of blockchain-based certificates, warranties, and authenticity documents for physical products sold through Shopify stores. When customers purchase products, Pocketbook automatically mints blockchain certificates and fulfills orders with secure, verifiable documentation.

Key Features

1-Click OAuth Setup - Automatic credential provisioning during OAuth flow ✅ Automated Webhook Registration - Automatic setup of required Shopify webhooks ✅ Automatic Voucher Creation - Creates blockchain certificates when orders are placed ✅ Auto-Fulfillment - Automatically fulfills Shopify orders with certificate links ✅ Bulk Processing - Handle high-volume orders efficiently (up to 50,000 items per job) ✅ Secure Authentication - HMAC signature verification for all webhooks ✅ Idempotency - Prevent duplicate certificate creation ✅ Product Template Mapping - Pre-configure certificate templates for products ✅ Real-time Notifications - Webhooks notify your app when certificates are ready

Use Cases

  • Product Authenticity Certificates - Verify genuine products with blockchain-backed certificates
  • Warranty Certificates - Issue tamper-proof warranty documents
  • Limited Edition Items - Create certificates of authenticity for collectibles
  • Physical NFTs - Link physical products to blockchain ownership records
  • Event Tickets - Issue certificates for ticket purchases

Architecture

Integration Flow

┌─────────────────────────────────────────────────────────────────┐
│                    INTEGRATION ARCHITECTURE                      │
└─────────────────────────────────────────────────────────────────┘

1. MERCHANT SETUP
   ┌──────────────┐
   │   Merchant   │
   │   Creates    │──────► Enterprise Account on Pocketbook
   │   Account    │
   └──────────────┘


   ┌──────────────┐
   │   1-Click    │──────► Auto-provisions:
   │    OAuth     │        • API Key (pk_...)
   │   Connect    │        • Enterprise ID (ent_...)
   │              │        • Webhook Secret
   └──────────────┘


   ┌──────────────┐
   │   Webhooks   │──────► Auto-registers in Shopify:
   │ Auto-Register│        • orders/create
   │              │        • orders/paid
   │              │        • orders/cancelled
   │              │        • app/uninstalled
   └──────────────┘

2. ORDER PROCESSING (AUTOMATIC)
   ┌──────────────┐
   │   Customer   │
   │    Places    │──────► Order Created in Shopify
   │    Order     │
   └──────────────┘


   ┌──────────────┐
   │   Shopify    │──────► POST /api/shopify/webhooks
   │    Sends     │        Headers:
   │   Webhook    │        • X-Shopify-Hmac-Sha256
   │              │        • X-Shopify-Shop-Domain
   │              │        • X-Shopify-Topic: orders/paid
   └──────────────┘


   ┌──────────────┐
   │  Pocketbook  │──────► • Verify HMAC signature
   │   Verifies   │        • Check idempotency
   │  & Creates   │        • Create bulk mint job
   │   Vouchers   │        • Process on IPFS + blockchain
   └──────────────┘


   ┌──────────────┐
   │ Pocketbook   │──────► • Mint blockchain certificates
   │   Mints      │        • Generate voucher URLs
   │ Certificates │        • Transaction hash recorded
   └──────────────┘


   ┌──────────────┐
   │  Pocketbook  │──────► POST to Shopify app webhook
   │    Sends     │        Payload includes:
   │   Webhook    │        • voucher_map (by line item)
   │              │        • voucher_all (flat list)
   │              │        • Transaction hash
   └──────────────┘


   ┌──────────────┐
   │  Pocketbook  │──────► Shopify Admin API
   │    Auto-     │        • fulfillmentCreateV2 mutation
   │   Fulfills   │        • Tracking: tx hash + voucher URLs
   │    Order     │        • Customer notification sent
   └──────────────┘


   ┌──────────────┐
   │   Customer   │──────► Receives:
   │   Receives   │        • Fulfillment email
   │ Certificate  │        • Voucher URLs
   │              │        • Blockchain proof
   └──────────────┘

Data Flow

Shopify Order Data ──────► Pocketbook Bulk Mint API

                            ├─► Metadata validation
                            ├─► Idempotency check
                            ├─► IPFS upload
                            ├─► Blockchain minting


                      Voucher Created

                            ├─► Webhook notification
                            ├─► Auto-fulfillment


                      Customer receives certificate

Getting Started

Prerequisites

  1. Pocketbook Enterprise Account

    • Sign up at pocketbook.studio
    • Navigate to Enterprise Dashboard
    • Access the Integrations page
  2. Shopify Store

    • Shopify store with admin access
    • Products configured for certificate issuance
    • Optional: Shopify app for enhanced integration
  3. Technical Requirements

    • HTTPS endpoint for webhooks (if using custom app)
    • Basic understanding of REST APIs
    • Node.js/Python/Ruby for API integration (optional)

Quick Start (5 Minutes)

Step 1: Access Integrations Page

bash
# Navigate to Pocketbook Enterprise Dashboard
https://www.pocketbook.studio/enterprise/integrations

Step 2: Click "Connect Store" for Shopify

The 1-click integration automatically provisions:

  • Enterprise API Key (pk_...)
  • Enterprise ID (ent_...)
  • Webhook Secret (64-character hex)

Step 3: Enter Your Shopify Store Domain

Example: mystore.myshopify.com

Step 4: Authorize in Shopify

OAuth flow redirects you to Shopify to authorize the connection. Required scopes:

  • read_orders - Read order data
  • write_fulfillments - Create fulfillments
  • read_products - Access product information

Step 5: Automatic Setup Complete

Pocketbook automatically:

  1. Creates ShopifyIntegration record
  2. Registers webhooks in Shopify
  3. Encrypts and stores access token
  4. Configures auto-fulfillment settings

Setup & Configuration

The easiest way to integrate is using the built-in 1-click OAuth flow. This automatically handles all credential provisioning and webhook registration.

Frontend Implementation

typescript
// src/enterprise/pages/IntegrationsPage.tsx
import { integrationService } from '../services/integrationService';

async function handleConnectShopify() {
  try {
    setConnecting(true);

    // Auto-provision credentials (automatic)
    const provisionResult = await integrationService.autoProvisionShopifyCredentials();

    if (provisionResult.success && provisionResult.credentials) {
      console.log('Credentials auto-provisioned for 1-click integration');

      // Initiate OAuth with provisioned credentials
      const result = await integrationService.connectShopify(
        shopDomain,
        provisionResult.credentials
      );

      if (result.success && result.authUrl) {
        // Redirect to Shopify OAuth
        window.location.href = result.authUrl;
      }
    }
  } catch (error) {
    console.error('Failed to connect Shopify:', error);
  } finally {
    setConnecting(false);
  }
}

Backend OAuth Endpoints

Authorization Endpoint

http
POST /api/shopify/auth/authorize
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

{
  "shop": "mystore.myshopify.com",
  "apiKeyId": "abc123",           // Auto-provisioned
  "apiKey": "pk_live_...",        // Auto-provisioned
  "webhookSecret": "64_char_hex"  // Auto-provisioned
}

Response:

json
{
  "success": true,
  "authUrl": "https://mystore.myshopify.com/admin/oauth/authorize?client_id=...&scope=read_orders,write_fulfillments,read_products&redirect_uri=https://api.pocketbook.studio/api/shopify/auth/callback&state=encrypted_state"
}

OAuth Callback

http
GET /api/shopify/auth/callback?code=<auth_code>&hmac=<hmac>&shop=mystore.myshopify.com&state=<state>&timestamp=<timestamp>

Pocketbook automatically:

  1. Verifies HMAC signature
  2. Validates state parameter (CSRF protection)
  3. Exchanges code for access token
  4. Encrypts and stores token (AES-256-CBC)
  5. Registers webhooks in Shopify
  6. Redirects back to your app

Option 2: Manual API Key Setup

For custom integrations or if you prefer manual setup:

Step 1: Generate API Key

http
POST https://api.pocketbook.studio/api/enterprise/api-keys
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

{
  "name": "Shopify Integration",
  "scopes": ["bulk:mint", "voucher:create", "pos:voucher"],
  "rateLimit": {
    "requestsPerHour": 5000,
    "burstLimit": 100
  }
}

Response:

json
{
  "success": true,
  "apiKey": {
    "key": "pk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6...",  // ⚠️ ONLY SHOWN ONCE
    "keyPrefix": "pk_a1b2c3",
    "scopes": ["bulk:mint", "voucher:create", "pos:voucher"],
    "id": "673abc123def456",
    "createdAt": "2025-11-10T12:00:00Z"
  }
}

⚠️ Important: Store the API key securely. It's only shown once!

Step 2: Generate Webhook Secret

javascript
// Client-side generation (cryptographically random)
const generateWebhookSecret = () => {
  const bytes = new Uint8Array(32);
  window.crypto.getRandomValues(bytes);
  return Array.from(bytes)
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
};

const webhookSecret = generateWebhookSecret();
// Store securely: "a1b2c3d4e5f6...64_characters"

Step 3: Configure Webhook Subscription

http
POST https://api.pocketbook.studio/api/enterprise/webhooks
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

{
  "url": "https://your-shopify-app.com/webhooks/pocketbook",
  "events": [
    "bulk.job.completed",
    "bulk.job.failed",
    "voucher.created",
    "voucher.minted"
  ],
  "secret": "a1b2c3d4e5f6...64_character_webhook_secret",
  "enabled": true
}

Response:

json
{
  "success": true,
  "webhook": {
    "id": "wh_abc123",
    "url": "https://your-shopify-app.com/webhooks/pocketbook",
    "events": ["bulk.job.completed", "bulk.job.failed"],
    "enabled": true
  }
}

Environment Variables

Pocketbook Backend (.env)

bash
# Shopify OAuth
SHOPIFY_CLIENT_ID=your_shopify_partner_client_id
SHOPIFY_CLIENT_SECRET=your_shopify_partner_secret
SHOPIFY_ENCRYPTION_KEY=your_32_byte_encryption_key_min
SHOPIFY_SCOPES=read_orders,write_fulfillments,read_products

# Shopify App (1-Click Integration)
SHOPIFY_APP_SETUP_URL=https://your-shopify-app.com/setup

# API URLs
API_BASE_URL=https://api.pocketbook.studio
CLIENT_URL=https://www.pocketbook.studio

# Webhook Settings (optional)
SHOPIFY_VOUCHER_WAIT_TIMEOUT_MS=5000
SHOPIFY_VOUCHER_WAIT_INTERVAL_MS=400
SYNC_MINT_MAX_ITEMS=5

Shopify App (.env)

bash
# Pocketbook Integration (from merchant)
POCKETBOOK_API_KEY=pk_live_your_api_key_64_characters
POCKETBOOK_ENTERPRISE_ID=ent_your_enterprise_id
POCKETBOOK_WEBHOOK_SECRET=your_64_character_webhook_secret

# Pocketbook Endpoints
POCKETBOOK_API_URL=https://api.pocketbook.studio
POCKETBOOK_BULK_MINT_URL=https://api.pocketbook.studio/api/bulk-mint

Order Fulfillment Integration

Pocketbook can automatically fulfill Shopify orders when blockchain certificates are ready, including tracking numbers and voucher URLs.

How Auto-Fulfillment Works

  1. Order placed in Shopify
  2. Webhook received by Pocketbook (orders/paid)
  3. Vouchers created on blockchain
  4. Fulfillment created in Shopify via Admin API
  5. Customer notified with certificate links

Enabling Auto-Fulfillment

Auto-fulfillment is controlled by the autoFulfill setting in your ShopifyIntegration record.

Via Database

javascript
db.shopifyintegrations.updateOne(
  { shop: "mystore.myshopify.com" },
  { $set: { "settings.autoFulfill": true } }
)

Via API (Future Enhancement)

http
PATCH /api/shopify/integration/settings
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

{
  "shop": "mystore.myshopify.com",
  "settings": {
    "autoFulfill": true
  }
}

Fulfillment Implementation

The fulfillment service uses Shopify's GraphQL Admin API with REST fallback:

typescript
import { ShopifyFulfillmentService } from './services/ShopifyFulfillmentService';

// Create fulfillment service
const service = new ShopifyFulfillmentService(
  'mystore.myshopify.com',
  accessToken,
  '2024-10' // API version
);

// Create fulfillment with retry
const result = await service.createFulfillmentWithRetry(
  'gid://shopify/Order/123456789',
  [
    { lineItemGid: 'gid://shopify/LineItem/987654321', quantity: 2 }
  ],
  {
    voucherUrls: [
      'https://www.pocketbook.studio/view/v_abc123',
      'https://www.pocketbook.studio/view/v_def456'
    ],
    transactionHash: '0x123abc...',
    polygonscanUrl: 'https://polygonscan.com/tx/0x123abc...'
  }
);

if (result.success) {
  console.log('Fulfillment created:', result.fulfillmentId);
} else {
  console.error('Fulfillment failed:', result.error);
}

GraphQL Fulfillment Mutation

graphql
mutation fulfillmentCreate($fulfillment: FulfillmentV2Input!) {
  fulfillmentCreateV2(fulfillment: $fulfillment) {
    fulfillment {
      id
      status
      trackingInfo {
        number
        url
        company
      }
    }
    userErrors {
      field
      message
    }
  }
}

Variables:

json
{
  "fulfillment": {
    "lineItemsByFulfillmentOrder": [
      {
        "fulfillmentOrderId": "gid://shopify/Order/123456789",
        "fulfillmentOrderLineItems": [
          {
            "id": "gid://shopify/LineItem/987654321",
            "quantity": 2
          }
        ]
      }
    ],
    "trackingInfo": {
      "number": "0x123abc...",
      "url": "https://polygonscan.com/tx/0x123abc...",
      "company": "Pocketbook Certificates"
    },
    "notifyCustomer": true
  }
}

REST Fulfillment API (Fallback)

http
POST https://mystore.myshopify.com/admin/api/2024-10/orders/123456789/fulfillments.json
X-Shopify-Access-Token: shpat_...
Content-Type: application/json

{
  "fulfillment": {
    "line_items": [
      { "id": 987654321, "quantity": 2 }
    ],
    "tracking_number": "0x123abc...",
    "tracking_url": "https://polygonscan.com/tx/0x123abc...",
    "tracking_company": "Pocketbook Certificates",
    "notify_customer": true
  }
}

Automatic Retry Logic

The fulfillment service includes intelligent retry logic:

  1. Initial attempt with GraphQL API
  2. Fallback to REST if GraphQL fails
  3. Exponential backoff (1s, 2s, 4s, 8s, max 10s)
  4. Max 3 attempts per fulfillment
  5. Non-fatal errors - job succeeds even if fulfillment fails
typescript
// Retry configuration
const maxRetries = 3;
const baseDelay = 1000; // ms
const maxDelay = 10000; // ms

// Exponential backoff calculation
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);

Product Warranty Integration

Create blockchain-backed warranties and certificates for products sold on Shopify.

Voucher Creation Schema (2025-09-01)

Pocketbook uses a standardized schema for voucher creation from Shopify orders.

Required Fields

  • schemaVersion - Current version: "2025-09-01"
  • metadata - Array of certificate metadata
  • externalContext - Shopify order context

Metadata Structure

typescript
interface VoucherMetadata {
  // Required fields
  name: string;                    // Product name
  description: string;             // Product description
  imageUrl: string;                // HTTPS URL to product image

  // Optional Shopify fields
  sku?: string;                    // Product SKU
  vendor?: string;                 // Product vendor
  productType?: string;            // Product type/category
  brand?: string;                  // Brand name
  collection?: string;             // Collection name

  // Optional certificate fields
  serialNumber?: string;           // Unique serial number
  warrantyExpiry?: string;         // ISO date string
  rarity?: string;                 // e.g., "limited", "rare"
  condition?: string;              // e.g., "mint", "used"
  externalUrl?: string;            // Link to product page

  // Custom attributes
  [key: string]: any;              // Additional custom fields
}

External Context Structure

typescript
interface ExternalContext {
  source: "shopify";                // Must be "shopify"
  shop: string;                     // e.g., "mystore.myshopify.com"
  orderId: string;                  // GID format: "gid://shopify/Order/123"
  orderNumber?: string;             // e.g., "1234"
  orderName?: string;               // e.g., "#1234"

  lineItems: Array<{
    lineItemGid: string;            // GID: "gid://shopify/LineItem/456"
    quantity: number;               // Positive integer
  }>;

  customer?: {
    email?: string;
    firstName?: string;
    lastName?: string;
  };

  createdAt?: string;               // ISO date string
  financialStatus?: string;         // e.g., "paid", "pending"
  fulfillmentStatus?: string;       // e.g., "unfulfilled", "fulfilled"
}

Creating Vouchers from Orders

Endpoint

POST /api/bulk-mint

Headers

Content-Type: application/json
x-api-key: pk_live_your_api_key
Idempotency-Key: {shop}#{orderId}#{lineItemGid}#{random_hash}

Request Body (Single Item with Quantity)

The server automatically expands single metadata items by quantity:

json
{
  "schemaVersion": "2025-09-01",
  "metadata": [
    {
      "name": "Premium Trading Card",
      "description": "Limited edition trading card with authenticity certificate",
      "imageUrl": "https://cdn.shopify.com/s/files/1/0001/2345/products/card.jpg",
      "sku": "CARD-001",
      "vendor": "Trading Card Co",
      "productType": "Collectibles",
      "brand": "Premium Cards",
      "collection": "Autumn 2024",
      "serialNumber": "TC-2024-001",
      "rarity": "legendary",
      "condition": "mint"
    }
  ],
  "externalContext": {
    "source": "shopify",
    "shop": "mystore.myshopify.com",
    "orderId": "gid://shopify/Order/987654321",
    "orderNumber": "1234",
    "orderName": "#1234",
    "lineItems": [
      {
        "lineItemGid": "gid://shopify/LineItem/123456789",
        "quantity": 2
      }
    ],
    "customer": {
      "email": "customer@example.com",
      "firstName": "John",
      "lastName": "Doe"
    },
    "createdAt": "2025-11-10T12:00:00Z",
    "financialStatus": "paid",
    "fulfillmentStatus": "unfulfilled"
  }
}

Request Body (Multiple Line Items)

For orders with multiple products:

json
{
  "schemaVersion": "2025-09-01",
  "metadata": [
    {
      "name": "Limited Edition Poster",
      "description": "Signed poster with certificate of authenticity",
      "imageUrl": "https://cdn.shopify.com/poster.jpg",
      "sku": "POSTER-001"
    },
    {
      "name": "Holographic Sticker Pack",
      "description": "Set of 5 holographic stickers",
      "imageUrl": "https://cdn.shopify.com/stickers.jpg",
      "sku": "STICKER-PACK-001"
    },
    {
      "name": "Holographic Sticker Pack",
      "description": "Set of 5 holographic stickers",
      "imageUrl": "https://cdn.shopify.com/stickers.jpg",
      "sku": "STICKER-PACK-001"
    },
    {
      "name": "Holographic Sticker Pack",
      "description": "Set of 5 holographic stickers",
      "imageUrl": "https://cdn.shopify.com/stickers.jpg",
      "sku": "STICKER-PACK-001"
    }
  ],
  "externalContext": {
    "source": "shopify",
    "shop": "acme.myshopify.com",
    "orderId": "gid://shopify/Order/111222333",
    "lineItems": [
      { "lineItemGid": "gid://shopify/LineItem/LI1", "quantity": 1 },
      { "lineItemGid": "gid://shopify/LineItem/LI2", "quantity": 3 }
    ]
  }
}

Response (Async Job)

For jobs with more than 5 items:

json
{
  "success": true,
  "jobId": "bulk_01JAXXX...",
  "status": "pending",
  "estimatedDuration": 120,
  "pollUrl": "https://api.pocketbook.studio/api/bulk-mint/bulk_01JAXXX..."
}

Response (Sync Processing)

For jobs with ≤5 items, returns immediately:

json
{
  "success": true,
  "jobId": "bulk_01JAXXX...",
  "status": "completed",
  "summary": {
    "totalItems": 2,
    "successCount": 2,
    "failureCount": 0
  },
  "vouchers": [
    {
      "id": "v_abc123",
      "url": "https://www.pocketbook.studio/view/v_abc123",
      "metadata": {
        "name": "Premium Trading Card",
        "sku": "CARD-001"
      }
    },
    {
      "id": "v_def456",
      "url": "https://www.pocketbook.studio/view/v_def456",
      "metadata": {
        "name": "Premium Trading Card",
        "sku": "CARD-001"
      }
    }
  ]
}

Idempotency

Idempotency prevents duplicate voucher creation if webhooks fire multiple times.

Idempotency Key Format

{shop}#{orderId}#{lineItemGid}#{random_hash}

Example:

mystore.myshopify.com#gid://shopify/Order/987654321#gid://shopify/LineItem/123456789#a1b2c3d4

Generating Idempotency Keys

javascript
const crypto = require('crypto');

function generateIdempotencyKey(shop, orderId, lineItemGid) {
  const randomHash = crypto.randomBytes(8).toString('hex');
  return `${shop}#${orderId}#${lineItemGid}#${randomHash}`;
}

// Usage
const key = generateIdempotencyKey(
  'mystore.myshopify.com',
  'gid://shopify/Order/987654321',
  'gid://shopify/LineItem/123456789'
);

Duplicate Detection

If you send the same idempotency key twice:

http
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "success": false,
  "error": "A job with this idempotency key already exists",
  "existingJobId": "bulk_01JAXXX...",
  "status": "completed"
}

Product Template Mapping (Optional)

Pre-configure certificate templates for specific products to automatically populate metadata.

Data Structure

javascript
{
  productTemplateMapping: {
    "gid://shopify/Product/123": {
      productId: "gid://shopify/Product/123",
      templateId: "template_abc",
      enabled: true,

      // Variant-specific templates
      variantMappings: {
        "gid://shopify/ProductVariant/456": "template_xyz"
      },

      // Field mapping from Shopify to template
      metadataMapping: {
        "product.title": "name",
        "product.description": "description",
        "variant.sku": "sku",
        "variant.price": "price",
        "product.vendor": "vendor"
      }
    }
  }
}

Helper Methods

typescript
import ShopifyIntegrationModel from './models/ShopifyIntegration';

// Get template for a product
const integration = await ShopifyIntegrationModel.findByShop('mystore.myshopify.com');
const template = integration.getTemplateForProduct('gid://shopify/Product/123');

// Set product template
await integration.setProductTemplate(
  'gid://shopify/Product/123',
  'template_abc',
  {
    "product.title": "name",
    "product.description": "description"
  }
);

Webhook Integration

Pocketbook uses webhooks for bidirectional event notifications between Shopify and your application.

Inbound Shopify Webhooks

Pocketbook receives webhooks from Shopify when orders are created, paid, or cancelled.

Registered Webhooks

Automatically registered during OAuth:

  • orders/create - New order created
  • orders/paid - Order payment completed
  • orders/cancelled - Order cancelled
  • app/uninstalled - App uninstalled from store

Webhook Endpoint

POST /api/shopify/webhooks

Webhook Headers

X-Shopify-Hmac-Sha256: <base64_signature>
X-Shopify-Shop-Domain: mystore.myshopify.com
X-Shopify-Topic: orders/paid
X-Shopify-API-Version: 2024-10

HMAC Verification

typescript
import crypto from 'crypto';

function verifyShopifyWebhook(rawBody: string, hmacHeader: string, secret: string): boolean {
  const expectedHmac = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('base64');

  // Constant-time comparison (prevents timing attacks)
  const hmacBuffer = Buffer.from(hmacHeader, 'base64');
  const expectedBuffer = Buffer.from(expectedHmac, 'base64');

  if (hmacBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(hmacBuffer, expectedBuffer);
}

// Usage in Express middleware
app.post('/api/shopify/webhooks',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body.toString('utf8');
    const hmac = req.headers['x-shopify-hmac-sha256'] as string;

    if (!verifyShopifyWebhook(rawBody, hmac, SHOPIFY_CLIENT_SECRET)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Process webhook
    const event = JSON.parse(rawBody);
    // ...
  }
);

Outbound Pocketbook Webhooks

Pocketbook sends webhooks to your Shopify app when voucher creation jobs complete.

Webhook Events

  • bulk.job.completed - Voucher creation completed successfully
  • bulk.job.failed - Voucher creation failed
  • voucher.created - Individual voucher created
  • voucher.minted - Individual voucher minted on blockchain

Webhook Payload (bulk.job.completed)

json
{
  "event": "bulk.job.completed",
  "jobId": "bulk_01JAXXX...",
  "status": "completed",
  "timestamp": "2025-11-10T12:05:00Z",

  "summary": {
    "totalItems": 2,
    "successCount": 2,
    "failureCount": 0,
    "processingTime": 45
  },

  "results": {
    "vouchers": [
      {
        "id": "v_abc123",
        "url": "https://www.pocketbook.studio/view/v_abc123",
        "referenceNumber": "V_ABC123EF56",
        "metadata": {
          "name": "Premium Trading Card",
          "sku": "CARD-001"
        }
      },
      {
        "id": "v_def456",
        "url": "https://www.pocketbook.studio/view/v_def456",
        "referenceNumber": "V_DEF456GH78",
        "metadata": {
          "name": "Premium Trading Card",
          "sku": "CARD-001"
        }
      }
    ],
    "transactionHash": "0x123abc...",
    "blockNumber": 12345678
  },

  // Shopify-specific context (from externalContext)
  "shop": "mystore.myshopify.com",
  "orderId": "gid://shopify/Order/987654321",
  "orderNumber": "1234",

  // Voucher mapping by line item
  "voucher_map": {
    "gid://shopify/LineItem/123456789": {
      "voucherIds": ["v_abc123", "v_def456"],
      "urls": [
        "https://www.pocketbook.studio/view/v_abc123",
        "https://www.pocketbook.studio/view/v_def456"
      ],
      "referenceNumbers": ["V_ABC123EF56", "V_DEF456GH78"]
    }
  },

  // Flat voucher list
  "voucher_all": [
    {
      "lineItemGid": "gid://shopify/LineItem/123456789",
      "voucherId": "v_abc123",
      "url": "https://www.pocketbook.studio/view/v_abc123",
      "referenceNumber": "V_ABC123EF56"
    },
    {
      "lineItemGid": "gid://shopify/LineItem/123456789",
      "voucherId": "v_def456",
      "url": "https://www.pocketbook.studio/view/v_def456",
      "referenceNumber": "V_DEF456GH78"
    }
  ]
}

Webhook Headers

Content-Type: application/json
X-Webhook-Signature: <hmac_sha256_hex>
X-Webhook-Event: bulk.job.completed
X-Webhook-Timestamp: 2025-11-10T12:05:00Z

Signature Verification

javascript
const crypto = require('crypto');

function verifyPocketbookWebhook(body, signature, secret) {
  const payload = JSON.stringify(body);
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSig, 'hex')
  );
}

// Express handler
app.post('/webhooks/pocketbook', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const isValid = verifyPocketbookWebhook(
    req.body,
    signature,
    POCKETBOOK_WEBHOOK_SECRET
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process webhook
  const { voucher_map, shop, orderId } = req.body;

  // Update order metafields, display to merchant, etc.
  // ...

  res.status(200).json({ success: true });
});

API Reference

Base URL

Production: https://api.pocketbook.studio

Authentication

http
x-api-key: pk_live_your_api_key

JWT Bearer Token (For user sessions)

http
Authorization: Bearer <JWT_TOKEN>

Endpoints

1. Create Vouchers (Bulk Mint)

http
POST /api/bulk-mint
Content-Type: application/json
x-api-key: pk_live_...
Idempotency-Key: {shop}#{orderId}#{lineItemGid}#{hash}

{
  "schemaVersion": "2025-09-01",
  "metadata": [...],
  "externalContext": {...}
}

Rate Limits: 1000 requests/hour (configurable per API key)

Response (201):

json
{
  "success": true,
  "jobId": "bulk_01JAXXX...",
  "status": "pending" | "completed",
  "estimatedDuration": 120
}

Error Responses:

  • 400 - Invalid request (validation errors)
  • 401 - Invalid API key
  • 403 - Insufficient scopes
  • 409 - Idempotency conflict (duplicate request)
  • 429 - Rate limit exceeded (includes Retry-After header)
  • 5xx - Server error (retryable)

2. Get Job Status

http
GET /api/bulk-mint/:jobId
x-api-key: pk_live_...

Response (200):

json
{
  "success": true,
  "job": {
    "id": "bulk_01JAXXX...",
    "status": "pending" | "processing" | "completed" | "failed",
    "progress": {
      "current": 50,
      "total": 100,
      "percentage": 50
    },
    "summary": {
      "totalItems": 100,
      "successCount": 50,
      "failureCount": 0
    },
    "results": {
      "vouchers": [...]
    },
    "createdAt": "2025-11-10T12:00:00Z",
    "completedAt": "2025-11-10T12:02:30Z"
  }
}

3. OAuth Authorization

http
POST /api/shopify/auth/authorize
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

{
  "shop": "mystore.myshopify.com",
  "apiKeyId": "optional_provisioned_key_id",
  "apiKey": "optional_provisioned_key",
  "webhookSecret": "optional_webhook_secret"
}

Response (200):

json
{
  "success": true,
  "authUrl": "https://mystore.myshopify.com/admin/oauth/authorize?..."
}

4. Auto-Provision Credentials

http
POST /api/shopify/integration/auto-provision
Authorization: Bearer <JWT_TOKEN>

Response (200):

json
{
  "success": true,
  "credentials": {
    "apiKeyId": "abc123",
    "apiKey": "pk_live_...",
    "webhookSecret": "64_char_hex",
    "enterpriseId": "ent_..."
  }
}

5. Retrieve Setup Token Credentials

http
GET /api/shopify/integration/credentials?token=<setup_token>

Response (200):

json
{
  "success": true,
  "credentials": {
    "enterpriseId": "ent_...",
    "apiKeyId": "abc123",
    "apiKey": "pk_live_...",
    "webhookSecret": "64_char_hex",
    "shop": "mystore.myshopify.com"
  },
  "returnUrl": "https://www.pocketbook.studio/enterprise/integrations?shopify_connected=true"
}

Code Examples

Complete Integration Example (Node.js)

javascript
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');

const app = express();

// Configuration
const POCKETBOOK_API_KEY = process.env.POCKETBOOK_API_KEY;
const POCKETBOOK_WEBHOOK_SECRET = process.env.POCKETBOOK_WEBHOOK_SECRET;
const POCKETBOOK_API_URL = 'https://api.pocketbook.studio';

// ============================================
// 1. CREATE VOUCHERS FROM SHOPIFY ORDER
// ============================================

async function createVouchersForOrder(shopifyOrder) {
  try {
    // Generate idempotency key
    const randomHash = crypto.randomBytes(8).toString('hex');
    const idempotencyKey = `${shopifyOrder.shop}#${shopifyOrder.id}#${shopifyOrder.line_items[0].id}#${randomHash}`;

    // Build metadata from line items
    const metadata = shopifyOrder.line_items.map(item => ({
      name: item.name,
      description: item.title,
      imageUrl: item.image?.src || 'https://via.placeholder.com/500',
      sku: item.sku,
      vendor: item.vendor,
      productType: item.product_type
    }));

    // Build line items for context
    const lineItems = shopifyOrder.line_items.map(item => ({
      lineItemGid: `gid://shopify/LineItem/${item.id}`,
      quantity: item.quantity
    }));

    // Create vouchers
    const response = await axios.post(
      `${POCKETBOOK_API_URL}/api/bulk-mint`,
      {
        schemaVersion: '2025-09-01',
        metadata,
        externalContext: {
          source: 'shopify',
          shop: shopifyOrder.shop,
          orderId: `gid://shopify/Order/${shopifyOrder.id}`,
          orderNumber: shopifyOrder.order_number.toString(),
          orderName: shopifyOrder.name,
          lineItems,
          customer: {
            email: shopifyOrder.customer?.email,
            firstName: shopifyOrder.customer?.first_name,
            lastName: shopifyOrder.customer?.last_name
          },
          createdAt: shopifyOrder.created_at,
          financialStatus: shopifyOrder.financial_status,
          fulfillmentStatus: shopifyOrder.fulfillment_status
        }
      },
      {
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': POCKETBOOK_API_KEY,
          'Idempotency-Key': idempotencyKey
        }
      }
    );

    console.log('Voucher creation initiated:', response.data);
    return response.data;

  } catch (error) {
    console.error('Failed to create vouchers:', error.response?.data || error.message);
    throw error;
  }
}

// ============================================
// 2. RECEIVE POCKETBOOK WEBHOOK
// ============================================

function verifyWebhookSignature(body, signature, secret) {
  const payload = JSON.stringify(body);
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSig, 'hex')
  );
}

app.post('/webhooks/pocketbook',
  express.json(),
  async (req, res) => {
    try {
      // Verify signature
      const signature = req.headers['x-webhook-signature'];
      if (!verifyWebhookSignature(req.body, signature, POCKETBOOK_WEBHOOK_SECRET)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      const event = req.body;

      if (event.event === 'bulk.job.completed') {
        console.log('Vouchers ready for order:', event.orderId);

        // Extract voucher URLs by line item
        const vouchersByLineItem = event.voucher_map;

        // Update Shopify order with voucher links
        for (const [lineItemGid, vouchers] of Object.entries(vouchersByLineItem)) {
          console.log(`Line item ${lineItemGid}:`);
          vouchers.urls.forEach((url, index) => {
            console.log(`  Voucher ${index + 1}: ${url}`);
          });
        }

        // Optional: Add voucher URLs to order metafields
        await updateShopifyOrderMetafields(event.shop, event.orderId, vouchersByLineItem);

        // Optional: Send email to customer with voucher links
        await sendVoucherEmailToCustomer(event);
      }

      // Return 200 immediately
      res.status(200).json({ received: true });

    } catch (error) {
      console.error('Webhook processing error:', error);
      res.status(400).json({ error: 'Bad request' });
    }
  }
);

// ============================================
// 3. UPDATE SHOPIFY ORDER METAFIELDS
// ============================================

async function updateShopifyOrderMetafields(shop, orderId, vouchersByLineItem) {
  // Extract numeric order ID from GID
  const orderIdMatch = orderId.match(/Order\/(\d+)/);
  if (!orderIdMatch) return;

  const numericOrderId = orderIdMatch[1];

  // Build metafield value
  const voucherData = Object.entries(vouchersByLineItem).map(([lineItemGid, vouchers]) => ({
    lineItem: lineItemGid,
    vouchers: vouchers.urls
  }));

  // Update metafield via Shopify Admin API
  const response = await axios.post(
    `https://${shop}/admin/api/2024-10/orders/${numericOrderId}/metafields.json`,
    {
      metafield: {
        namespace: 'pocketbook',
        key: 'certificate_urls',
        value: JSON.stringify(voucherData),
        type: 'json'
      }
    },
    {
      headers: {
        'X-Shopify-Access-Token': SHOPIFY_ACCESS_TOKEN,
        'Content-Type': 'application/json'
      }
    }
  );

  console.log('Updated order metafields:', response.data);
}

// ============================================
// 4. POLL JOB STATUS (ALTERNATIVE TO WEBHOOKS)
// ============================================

async function pollJobStatus(jobId) {
  const maxAttempts = 60; // 5 minutes with 5s interval
  const pollInterval = 5000; // 5 seconds

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      const response = await axios.get(
        `${POCKETBOOK_API_URL}/api/bulk-mint/${jobId}`,
        {
          headers: {
            'x-api-key': POCKETBOOK_API_KEY
          }
        }
      );

      const job = response.data.job;

      if (job.status === 'completed') {
        console.log('Job completed:', job.results);
        return job;
      }

      if (job.status === 'failed') {
        console.error('Job failed:', job.error);
        throw new Error('Job failed');
      }

      // Still processing
      console.log(`Job ${jobId} status: ${job.status} (${job.progress?.percentage || 0}%)`);

      // Wait before next poll
      await new Promise(resolve => setTimeout(resolve, pollInterval));

    } catch (error) {
      console.error('Error polling job status:', error.message);
      throw error;
    }
  }

  throw new Error('Job polling timeout');
}

app.listen(3000, () => {
  console.log('Shopify integration server running on port 3000');
});

Python Example

python
import os
import hmac
import hashlib
import json
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

# Configuration
POCKETBOOK_API_KEY = os.getenv('POCKETBOOK_API_KEY')
POCKETBOOK_WEBHOOK_SECRET = os.getenv('POCKETBOOK_WEBHOOK_SECRET')
POCKETBOOK_API_URL = 'https://api.pocketbook.studio'

# ============================================
# CREATE VOUCHERS FROM SHOPIFY ORDER
# ============================================

def create_vouchers_for_order(shopify_order):
    """Create blockchain certificates for a Shopify order"""

    # Generate idempotency key
    random_hash = os.urandom(8).hex()
    idempotency_key = f"{shopify_order['shop']}#{shopify_order['id']}#{shopify_order['line_items'][0]['id']}#{random_hash}"

    # Build metadata from line items
    metadata = [
        {
            'name': item['name'],
            'description': item['title'],
            'imageUrl': item.get('image', {}).get('src', 'https://via.placeholder.com/500'),
            'sku': item.get('sku'),
            'vendor': item.get('vendor'),
            'productType': item.get('product_type')
        }
        for item in shopify_order['line_items']
    ]

    # Build line items for context
    line_items = [
        {
            'lineItemGid': f"gid://shopify/LineItem/{item['id']}",
            'quantity': item['quantity']
        }
        for item in shopify_order['line_items']
    ]

    # Create vouchers
    response = requests.post(
        f'{POCKETBOOK_API_URL}/api/bulk-mint',
        json={
            'schemaVersion': '2025-09-01',
            'metadata': metadata,
            'externalContext': {
                'source': 'shopify',
                'shop': shopify_order['shop'],
                'orderId': f"gid://shopify/Order/{shopify_order['id']}",
                'orderNumber': str(shopify_order['order_number']),
                'orderName': shopify_order['name'],
                'lineItems': line_items,
                'customer': {
                    'email': shopify_order.get('customer', {}).get('email'),
                    'firstName': shopify_order.get('customer', {}).get('first_name'),
                    'lastName': shopify_order.get('customer', {}).get('last_name')
                },
                'createdAt': shopify_order['created_at'],
                'financialStatus': shopify_order['financial_status'],
                'fulfillmentStatus': shopify_order['fulfillment_status']
            }
        },
        headers={
            'Content-Type': 'application/json',
            'x-api-key': POCKETBOOK_API_KEY,
            'Idempotency-Key': idempotency_key
        }
    )

    response.raise_for_status()
    return response.json()

# ============================================
# RECEIVE POCKETBOOK WEBHOOK
# ============================================

def verify_webhook_signature(body, signature, secret):
    """Verify HMAC signature from Pocketbook webhook"""
    payload = json.dumps(body, separators=(',', ':'))
    expected_sig = hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_sig)

@app.route('/webhooks/pocketbook', methods=['POST'])
def pocketbook_webhook():
    """Handle incoming webhook from Pocketbook"""

    # Verify signature
    signature = request.headers.get('X-Webhook-Signature')
    if not verify_webhook_signature(request.json, signature, POCKETBOOK_WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    event = request.json

    if event['event'] == 'bulk.job.completed':
        print(f"Vouchers ready for order: {event['orderId']}")

        # Extract voucher URLs by line item
        vouchers_by_line_item = event['voucher_map']

        # Process vouchers
        for line_item_gid, vouchers in vouchers_by_line_item.items():
            print(f"Line item {line_item_gid}:")
            for i, url in enumerate(vouchers['urls'], 1):
                print(f"  Voucher {i}: {url}")

    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)

Best Practices

1. Security

Always verify webhook signatures using constant-time comparison ✅ Store API keys securely using environment variables or secret managers ✅ Use HTTPS for all webhook endpoints in production ✅ Implement rate limiting on your webhook endpoints ✅ Rotate API keys periodically for enhanced security

2. Error Handling

Handle 409 conflicts gracefully (idempotency key already used) ✅ Implement exponential backoff for retries on 5xx errors ✅ Log all webhook events for debugging and audit trails ✅ Return 200 OK quickly from webhook handlers (process async) ✅ Monitor failed jobs and implement alerting

3. Performance

Use async processing for webhook handlers ✅ Batch small orders when possible (sync response for ≤5 items) ✅ Poll job status only as fallback to webhooks ✅ Cache ShopifyIntegration records to reduce DB queries ✅ Use connection pooling for database and HTTP clients

4. Testing

Test with development stores before production deployment ✅ Verify HMAC signatures in all environments ✅ Test error scenarios (network failures, invalid data, etc.) ✅ Monitor webhook delivery and retry logic ✅ Load test with realistic order volumes

5. Monitoring

Track job completion rates and average processing time ✅ Monitor webhook delivery success rates ✅ Alert on fulfillment failures that exceed threshold ✅ Log idempotency conflicts to detect potential issues ✅ Dashboard metrics for vouchers created, orders fulfilled, errors


Troubleshooting

Problem: Webhooks Not Firing from Shopify

Symptoms:

  • Orders placed but no vouchers created
  • No webhook events received at Pocketbook endpoint

Diagnosis:

bash
# Check if webhooks are registered in Shopify Admin
# Navigate to: Settings → Notifications → Webhooks

# Or via API:
curl -X GET "https://mystore.myshopify.com/admin/api/2024-10/webhooks.json" \
  -H "X-Shopify-Access-Token: shpat_..."

Solutions:

  1. Re-register webhooks:
bash
curl -X POST https://api.pocketbook.studio/api/shopify/webhooks/register \
  -H "Authorization: Bearer <JWT_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"shop": "mystore.myshopify.com"}'
  1. Verify webhook URL is accessible:
bash
# Test from external network
curl -X POST https://api.pocketbook.studio/api/shopify/webhooks \
  -H "Content-Type: application/json" \
  -d '{"test": true}'
  1. Check HMAC secret:
javascript
// Verify SHOPIFY_CLIENT_SECRET matches Shopify Partner Dashboard
console.log('Using secret:', process.env.SHOPIFY_CLIENT_SECRET);
  1. Enable raw body middleware:
javascript
// Ensure raw body is preserved for HMAC verification
app.use('/api/shopify/webhooks', express.raw({ type: 'application/json' }));

Problem: Fulfillment Failing

Symptoms:

  • Vouchers created successfully
  • Shopify orders remain unfulfilled
  • Error: "fulfillmentCreateV2 failed"

Diagnosis:

javascript
// Check fulfillment settings
db.shopifyintegrations.findOne({ shop: "mystore.myshopify.com" })
// Verify: settings.autoFulfill === true

// Check Shopify access token scopes
// Must include: write_fulfillments

Solutions:

  1. Enable auto-fulfill:
javascript
db.shopifyintegrations.updateOne(
  { shop: "mystore.myshopify.com" },
  { $set: { "settings.autoFulfill": true } }
)
  1. Verify OAuth scopes:
javascript
// Required scope: write_fulfillments
// Re-authorize if missing:
// Go to Integrations page → Disconnect → Reconnect
  1. Check order fulfillability:
graphql
# Query Shopify to check if order can be fulfilled
query {
  order(id: "gid://shopify/Order/123456789") {
    fulfillable
    fulfillmentOrders(first: 5) {
      edges {
        node {
          id
          status
          lineItems(first: 10) {
            edges {
              node {
                id
                remainingQuantity
              }
            }
          }
        }
      }
    }
  }
}
  1. Manually retry fulfillment:
bash
curl -X POST https://api.pocketbook.studio/api/shopify/fulfillment/retry \
  -H "Authorization: Bearer <JWT_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "shop": "mystore.myshopify.com",
    "orderId": "gid://shopify/Order/123456789"
  }'

Problem: Duplicate Vouchers Created

Symptoms:

  • Same order creates multiple sets of vouchers
  • Multiple webhook events for same order

Diagnosis:

javascript
// Check for idempotency key conflicts
db.bulkjobs.find({
  'externalContext.orderId': 'gid://shopify/Order/123456789'
})

// Should see only one job per unique idempotency key

Solutions:

  1. Ensure idempotency key is included:
javascript
// Always include Idempotency-Key header
const idempotencyKey = `${shop}#${orderId}#${lineItemGid}#${randomHash}`;

axios.post('/api/bulk-mint', payload, {
  headers: {
    'Idempotency-Key': idempotencyKey
  }
});
  1. Check webhook handler idempotency:
javascript
// Store processed webhook IDs to prevent duplicate processing
const processedWebhooks = new Set();

app.post('/webhooks/shopify', async (req, res) => {
  const webhookId = req.headers['x-shopify-webhook-id'];

  if (processedWebhooks.has(webhookId)) {
    return res.status(200).json({ message: 'Already processed' });
  }

  // Process webhook
  // ...

  processedWebhooks.add(webhookId);
  res.status(200).json({ success: true });
});
  1. Verify webhook deduplication in Shopify:
bash
# Shopify may retry webhooks on timeout
# Ensure your endpoint responds within 30 seconds
# Return 200 OK before long-running processing

Problem: API Rate Limiting

Symptoms:

  • HTTP 429 Too Many Requests
  • Header: Retry-After: 60

Diagnosis:

bash
# Check current rate limit
curl -X GET https://api.pocketbook.studio/api/enterprise/api-keys/<KEY_ID> \
  -H "Authorization: Bearer <JWT_TOKEN>"

# Response includes:
# "rateLimit": {
#   "requestsPerHour": 1000,
#   "burstLimit": 100
# }

Solutions:

  1. Implement exponential backoff:
javascript
async function createVouchersWithRetry(payload, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await axios.post('/api/bulk-mint', payload, {
        headers: { 'x-api-key': API_KEY }
      });
    } catch (error) {
      if (error.response?.status === 429) {
        const retryAfter = parseInt(error.response.headers['retry-after']) || 60;
        console.log(`Rate limited, waiting ${retryAfter}s...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      } else {
        throw error;
      }
    }
  }
  throw new Error('Max retries exceeded');
}
  1. Request rate limit increase:
bash
# Contact support or update via API
curl -X PATCH https://api.pocketbook.studio/api/enterprise/api-keys/<KEY_ID> \
  -H "Authorization: Bearer <JWT_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "rateLimit": {
      "requestsPerHour": 5000,
      "burstLimit": 200
    }
  }'
  1. Batch small orders:
javascript
// Combine multiple small orders into single bulk-mint request
// Reduces API calls while maintaining per-order tracking

Problem: Invalid HMAC Signature

Symptoms:

  • Webhook returns 401 Unauthorized
  • Log: "HMAC verification failed"

Diagnosis:

javascript
// Check raw body preservation
console.log('Raw body type:', typeof req.body);
console.log('Raw body:', req.body);

// Should be Buffer or string, NOT parsed JSON object

Solutions:

  1. Use raw body middleware:
javascript
// CORRECT: Preserve raw body for HMAC verification
app.use('/api/shopify/webhooks',
  express.raw({ type: 'application/json' })
);

// WRONG: JSON parsing corrupts body for HMAC
app.use(express.json());
  1. Verify secret matches:
bash
# Pocketbook uses SHOPIFY_CLIENT_SECRET
echo $SHOPIFY_CLIENT_SECRET

# Should match Shopify Partner Dashboard → App → API credentials
  1. Check HMAC header format:
javascript
// Shopify sends base64-encoded HMAC
const hmacHeader = req.headers['x-shopify-hmac-sha256'];
console.log('HMAC format:', hmacHeader); // Should be base64 string

// Pocketbook uses hex-encoded HMAC
const hmacHeader = req.headers['x-webhook-signature'];
console.log('HMAC format:', hmacHeader); // Should be hex string

Problem: Setup Token Expired or Invalid

Symptoms:

  • Error: "Invalid or expired setup token"
  • 1-click integration fails during credential retrieval

Diagnosis:

javascript
// Check setup token expiration
db.shopify_setup_tokens.findOne({ setupToken: "abc123..." })

// Response includes:
// {
//   expiresAt: ISODate("2025-11-10T12:10:00.000Z"),
//   used: false
// }

Solutions:

  1. Start connection process again:
bash
# Setup tokens expire after 10 minutes
# If expired, initiate new OAuth flow
  1. Verify token is not already used:
javascript
// Setup tokens are one-time use
// Check if token was already consumed:
db.shopify_setup_tokens.findOne({ setupToken: "abc123...", used: true })

// If used: true, start new OAuth flow
  1. Check SHOPIFY_APP_SETUP_URL:
bash
# Verify environment variable is set correctly
echo $SHOPIFY_APP_SETUP_URL
# Should point to your Shopify app's setup page

Common Error Codes

CodeErrorSolution
400Invalid request bodyVerify schemaVersion, metadata, and externalContext fields
401Invalid API keyCheck x-api-key header and key validity
403Insufficient scopesAPI key needs bulk:mint or voucher:create scope
409Idempotency conflictSame idempotency key already used (not necessarily an error)
429Rate limit exceededImplement exponential backoff, check Retry-After header
500Internal server errorRetry with exponential backoff, contact support if persists
502/503Service unavailableTemporary outage, retry after delay

Getting Help

Documentation Resources

Support Channels

Debugging Tips

  1. Enable verbose logging:
bash
export DEBUG=pocketbook:*
npm start
  1. Check service status:
bash
curl https://api.pocketbook.studio/health
  1. Test API connectivity:
bash
curl -X GET https://api.pocketbook.studio/api/enterprise/api-keys \
  -H "x-api-key: pk_live_..." -v
  1. Inspect webhook payloads:
javascript
// Log all incoming webhooks to file
const fs = require('fs');

app.post('/webhooks/pocketbook', (req, res) => {
  fs.appendFileSync(
    'webhooks.log',
    JSON.stringify({ timestamp: new Date(), body: req.body }) + '\n'
  );
  // ...
});

Summary

The Pocketbook Shopify integration provides a complete solution for automated blockchain certificate creation and delivery:

1-Click OAuth Setup - Automatic credential provisioning ✅ Bidirectional Webhooks - Real-time event notifications ✅ Automatic Fulfillment - Orders fulfilled with certificate links ✅ Secure & Scalable - HMAC verification, idempotency, rate limiting ✅ Production Ready - Handles up to 50,000 items per job

Integration Checklist

  • [ ] Create Pocketbook Enterprise account
  • [ ] Connect Shopify store via OAuth
  • [ ] Verify webhooks are registered in Shopify
  • [ ] Test voucher creation with sample order
  • [ ] Enable auto-fulfillment (optional)
  • [ ] Configure webhook endpoint in your app
  • [ ] Verify webhook signature verification
  • [ ] Test complete order-to-fulfillment flow
  • [ ] Monitor job completion rates
  • [ ] Set up alerting for failures

Next Steps

  1. Review API Documentation: /docs/api/overview
  2. Set up webhooks: /docs/guides/webhook-integration
  3. Explore advanced features: Product template mapping, custom metadata fields
  4. Deploy to production: Follow security best practices and monitoring guidelines

Need help? Contact support@pocketbook.studio or join our developer Slack community.

Released under the MIT License.