Automatically Generate Branded PDF Invoices from Shopify Orders (Webhook + API, No App)

Shopify does not send branded PDF invoices automatically. Order Printer templates sit behind the admin panel, and invoice apps come with their own layout…

Shopify does not send branded PDF invoices automatically [1]. Order Printer templates sit behind the admin panel, and invoice apps come with their own layout constraints. The clean path is a webhook you control: Shopify pushes the order payload to your endpoint, you map it to an HTML template, and render a PDF with one API call.

Set up the orders/create webhook and verify deliveries

Subscribe to the orders/create webhook through your Shopify admin or the GraphQL Admin API. This topic fires when a new order is placed [2].

Every delivery includes a base64-encoded HMAC-SHA256 signature in the X-Shopify-Hmac-SHA256 header, computed from the raw request body and your app's shared secret [2]. Verify this before processing. Run verification middleware before body parsers like express.json() because the signature requires the raw body string [3].

const crypto = require('crypto');

function verifyShopifyWebhook(body, hmac, secret) {
  const hash = crypto
    .createHmac('sha256', secret)
    .update(body, 'utf8')
    .digest('base64');
  return crypto.timingSafeEqual(
    Buffer.from(hash),
    Buffer.from(hmac)
  );
}

HMAC verification applies only to HTTPS deliveries [3].

Dedupe on X-Shopify-Webhook-Id to avoid double-processing the same order [2]. If a topic has multiple subscriptions, each delivery gets a different webhook ID but shares the same event ID [3].

Return a 2xx status immediately. Shopify enforces a one-second connection timeout and a five-second total timeout, treating anything outside the 200 range as an error [3]. Process the payload asynchronously after responding [2]. Shopify retries failed deliveries up to 8 times over 4 hours with exponential backoff [2], and after 8 consecutive failures, subscriptions created via the Admin API are automatically deleted with warning emails sent to your developer emergency address [3]. Rotating your client secret can take up to an hour before HMAC digests use the new secret [3].

Map the order payload to a reusable HTML invoice template

Create a template in MarkupGo using HTML and CSS with {{ variable }} placeholders for customer details, line items, totals, tax, and shipping [4]. Store the template ID from your dashboard for the API call.

Design for B2B fields that apps often omit: VAT ID, purchase order number, custom line-item columns, and brand fonts. A single template handles every order when you pass dynamic data through the JSON context field.

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: 'Inter', sans-serif; margin: 40px; color: #1a1a1a; }
    .header { display: flex; justify-content: space-between; margin-bottom: 48px; }
    .vat { color: #666; font-size: 13px; }
    table { width: 100%; border-collapse: collapse; margin-top: 24px; }
    th { text-align: left; padding: 12px 0; border-bottom: 2px solid #e5e5e5; }
    td { padding: 12px 0; border-bottom: 1px solid #f0f0f0; }
    .totals { margin-top: 32px; text-align: right; }
    .total { font-size: 20px; font-weight: 600; }
  </style>
</head>
<body>
  <div class="header">
    <div>
      <h1>INVOICE</h1>
      <p class="vat">VAT ID: {{ seller_vat }}</p>
    </div>
    <div>
      <p>Invoice #{{ order_number }}</p>
      <p>{{ date }}</p>
      <p class="vat">PO: {{ purchase_order }}</p>
    </div>
  </div>
  
  <p><strong>Bill to:</strong><br>
  {{ customer_name }}<br>
  {{ customer_email }}<br>
  VAT ID: {{ customer_vat }}</p>
  
  <table>
    <thead>
      <tr>
        <th>Item</th>
        <th>Qty</th>
        <th>Unit Price</th>
        <th>Total</th>
      </tr>
    </thead>
    <tbody>
      {{#each line_items}}
      <tr>
        <td>{{ title }}</td>
        <td>{{ quantity }}</td>
        <td>{{ price }}</td>
        <td>{{ line_total }}</td>
      </tr>
      {{/each}}
    </tbody>
  </table>
  
  <div class="totals">
    <p>Subtotal: {{ subtotal }}</p>
    <p>Tax ({{ tax_rate }}%): {{ tax_amount }}</p>
    <p>Shipping: {{ shipping }}</p>
    <p class="total">Total: {{ total }}</p>
  </div>
</body>
</html>

Pull order_number, email, total_price, tax_lines, line_items, and nested customer fields into the structure your template expects.

Render the PDF via the API and handle expiration

POST the template ID and context object to MarkupGo's /pdf endpoint [5]. Include your API key in the x-api-key header [6].

const response = await fetch('https://api.markupgo.com/api/v1/pdf', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.MARKUPGO_API_KEY
  },
  body: JSON.stringify({
    source: {
      type: 'template',
      data: {
        id: process.env.MARKUPGO_INVOICE_TEMPLATE_ID,
        context: {
          order_number: order.name,
          date: new Date().toISOString().split('T')[0],
          customer_name: `${order.customer.first_name} ${order.customer.last_name}`,
          customer_email: order.email,
          customer_vat: order.customer?.default_address?.company || 'N/A',
          seller_vat: process.env.SELLER_VAT_ID,
          purchase_order: order.note || 'N/A',
          line_items: order.line_items.map(item => ({
            title: item.title,
            quantity: item.quantity,
            price: item.price,
            line_total: (item.quantity * item.price).toFixed(2)
          })),
          subtotal: order.subtotal_price,
          tax_rate: order.tax_lines[0]?.rate * 100 || 0,
          tax_amount: order.total_tax,
          shipping: order.total_shipping_price_set?.shop_money?.amount || 0,
          total: order.total_price
        }
      }
    },
    options: {
      properties: {
        size: { width: 210, height: 297 },
        margins: { top: 10, bottom: 10, left: 10, right: 10 }
      },
      // see Markupgo's API reference for additional options
    },
    expiration: { expiresInSeconds: 604800 }
  })
});

const task = await response.json();
const pdfUrl = task.url;

Override template options per request if needed: margins, header, footer, page size [5][4]. PDF options also include PDF/UA accessibility, metadata, cookies, and wait conditions [4].

Set expiresInSeconds so invoice PDFs containing customer PII auto-delete after your chosen window, from 60 seconds up to 90 days [5]. Expired files are swept every minute, so a PDF may remain reachable for roughly 60 seconds past its requested expiration [5]. Deleting counts as a normal delete with no credit refund.

Email the PDF or attach the hosted URL

Send via your existing email provider or transactional service; no Shopify app required.

// Example with any HTTP-based email service
await fetch('https://api.your-email-service.com/send', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${process.env.EMAIL_API_KEY}` },
  body: JSON.stringify({
    to: order.email,
    subject: `Invoice ${order.name} from ${process.env.STORE_NAME}`,
    attachments: [{
      url: pdfUrl,
      filename: `invoice-${order.name}.pdf`
    }]
  })
});

For EU stores, pair the expiration setting with your data retention policy to limit PII storage without building extra infrastructure.

Cost check against order volume

MarkupGo's Lite plan costs $29/month and includes 2,000 credits and 5 templates [4]. Plus runs $49/month for 10,000 credits and 30 templates; Pro is $99/month for 20,000 credits and unlimited templates [4]. The free trial grants 100 credits every month indefinitely for testing the full flow [4].

A store doing 100 orders per month uses roughly 100 credits for invoice generation, a small slice of the Lite plan's 2,000 monthly credits [4].

Next steps

Add payment QR codes to the same template using MarkupGo's existing UPI, EPC, or SEPA guide. Connect the same webhook payload to Make.com or n8n for multi-step automation beyond email. Apply this pattern to packing slips, refund notices, or B2B quote PDFs using the same template system and expiration controls.

Sources

  1. Can I make an invoice and send it to customers? (community.shopify.com)
  2. Order webhooks (shopify.dev)
  3. Verify webhook deliveries (shopify.dev)
  4. markupgo.com
  5. markupgo.com
  6. markupgo.com