Skip to main content
Customei can POST JSON payloads to an HTTPS endpoint you control whenever an order event fires. Use webhooks to trigger fulfillment, notify a Slack channel, write to your data warehouse, or kick off any custom automation.

Events you can subscribe to

Every subscription listens to one or more of the following events. Events correspond to Shopify order lifecycle transitions:
EventFires when
order.createdA new order is placed.
order.updatedAny order field changes after creation.
order.paidThe order’s financial status moves to paid.
order.fulfilledThe order (or a line item) is fulfilled.
order.cancelledThe order is cancelled.
You can subscribe to any combination. Duplicate deliveries are possible — for example, paying a cancelled-then-restored order can fire both order.paid and order.updated. Always make your handler idempotent (see Delivery guarantees).

Create a subscription

1

Open Settings → Integrations → Webhooks

The list shows every webhook subscription on your account.
2

Click Add webhook

A form asks for the endpoint URL, events, and an optional description.
3

Enter your endpoint URL

Must be HTTPS. HTTP endpoints are rejected at save time. Plain IP addresses are allowed but not recommended — use a domain with a valid TLS certificate.
4

Pick events

Tick one or more events from the list.
5

Accept the data-handling notice

Webhook payloads include order and (anonymized) customer data. Confirm you’re responsible for handling them securely before creating the subscription.
6

Save and copy the secret

On create, Customei generates a secret used to sign every payload. Copy it now — it’s shown only once. Store it somewhere secure; you’ll use it to verify incoming payloads on your server.
If you lose the secret, you can’t retrieve it. Edit the subscription and rotate it — Customei issues a new secret and invalidates the old one.

Payload shape

Every delivery is a POST request with a JSON body:
{
  "event": "order.paid",
  "trigger": "order.paid",
  "timestamp": "2026-04-13T10:24:17.832Z",
  "data": {
    "order": {
      "id": "ord_01H8...",
      "shopifyOrderId": "5729183281729",
      "name": "#1042",
      "orderNumber": 1042,
      "financialStatus": "paid",
      "fulfillmentStatus": null,
      "currency": "USD",
      "subtotalPrice": "49.00",
      "totalPrice": "54.40",
      "email": "[email protected]",
      "tags": ["personalized", "priority"],
      "lineItems": [
        {
          "title": "Custom Name Mug",
          "sku": "MUG-CUSTOM-11OZ",
          "quantity": 1,
          "price": "49.00",
          "productionStatus": "READY",
          "properties": {
            "Your name": "Alex",
            "Color": "Cobalt"
          }
        }
      ]
    }
  }
}

Field notes

  • event — the specific event that matched this subscription (e.g. a single subscription listening to both order.paid and order.updated will get two separate deliveries when both fire).
  • trigger — the underlying order lifecycle change, mapped from Shopify’s topic. Useful when multiple events share a trigger.
  • timestamp — ISO-8601 in UTC.
  • data.order.id — Customei’s internal order ID (prefixed ord_). Use this as your primary key when deduplicating.
  • data.order.shopifyOrderId — Shopify’s numeric order ID. Handy for cross-referencing Shopify Admin.
  • data.order.lineItems[].properties — the personalization payload, flattened to key-value pairs per line item. For richer data (uploaded image URLs, bound layer metadata), query the GraphQL API using the id.
  • data.order.lineItems[].productionStatus — Customei’s internal print-file state: PENDING / GENERATING / READY / FAILED.
Prices are serialized as strings to preserve exact decimals.

Verify the signature

Every request includes two headers:
  • X-Pod-Timestamp — request timestamp in ISO-8601.
  • X-Pod-Signaturesha256=<hex-digest> HMAC over ${timestamp}.${rawBody} using your subscription secret.

Node.js example

import crypto from 'node:crypto';

function verifyPodWebhook(rawBody, timestamp, signature, secret) {
  const expected =
    'sha256=' +
    crypto
      .createHmac('sha256', secret)
      .update(`${timestamp}.${rawBody}`)
      .digest('hex');

  // Constant-time comparison to avoid timing attacks
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Always verify before parsing the body. If the signature doesn’t match, return 401 — Customei will retry and log the failure.

Python example

import hmac
import hashlib

def verify_pod_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Reject stale requests

Compare the timestamp against your server clock and reject anything more than 5 minutes off. This protects against replay attacks even if a signature is leaked.

Verification challenge on create

When you create a new subscription, Customei immediately sends a verification request to your URL — a POST with a special event: "_verification" payload. Your server must respond with 200 OK within 10 seconds. If it doesn’t, Customei refuses to activate the subscription and shows the error in the UI. This is a light cost to set up but saves you from shipping a broken subscription to production.

Delivery guarantees

Webhooks are delivered at-least-once. Your handler must be idempotent — receiving the same event twice should not double-charge, double-fulfill, or double-anything.
  • Timeouts. Your endpoint has 10 seconds to respond. Beyond that, Customei treats the delivery as failed.
  • Retries. Failed deliveries (non-2xx status, timeout, network error) are retried with exponential backoff starting at 30 seconds, for up to 5 attempts. After the fifth failure, the delivery is marked permanently failed and surfaced in the subscription’s delivery log.
  • Ordering. Deliveries are not guaranteed to arrive in chronological order, especially across retries. Use the timestamp field to reconstruct ordering yourself if you need it.
  • Deduplication. Use data.order.id + event as your idempotency key.

View delivery history

Each subscription has a Deliveries tab showing recent attempts, their status, response code, and response body (truncated). Click an individual delivery to replay the payload for debugging.
  • Succeeded — your endpoint returned 2xx.
  • Failed — at least one attempt failed; check the last response body for the reason.
  • Pending — queued for delivery but not sent yet.

Manually replay a delivery

From the delivery log, click Replay on any past delivery. Customei sends the exact same payload (including the original timestamp and a fresh signature) to your endpoint. Handy for reproducing bugs in staging.

Edit or pause a subscription

  • Edit — change the URL, events, or description. The secret stays the same unless you explicitly rotate it.
  • Pause — temporarily stop deliveries without deleting the subscription. Incoming events during a pause are dropped; they’re not queued for later delivery.
  • Delete — permanently removes the subscription and its delivery history.

Rotate the secret

From the subscription’s settings, click Rotate secret. Customei issues a new secret and shows it once — copy it and deploy it to your server before sending the next event. There’s no grace period — the old secret stops working immediately on rotation.

Limits

  • 10 active subscriptions per account. Contact Support if you need more.
  • 10-second handler timeout.
  • 5 retry attempts per delivery.
  • Payloads are capped at 1 MB. Events that would exceed this are truncated; line-item properties are the most common culprit.

Troubleshooting

  • I’m getting duplicate events. Normal under retry; dedupe on data.order.id + event.
  • The signature doesn’t match. Make sure you’re signing ${timestamp}.${rawBody}, not the parsed JSON. The body must be the exact bytes received, before any framework parses it.
  • Timestamps feel wrong. They’re UTC ISO-8601. Your server might be comparing against local time.
  • Verification challenge fails. Your endpoint needs to respond 2xx to the initial test POST with event: "_verification" — many merchants forget their auth middleware blocks un-signed requests and reject the challenge.

Next