Webhooks

Receive real-time HTTP POST notifications whenever something happens in your TruCustom account — new stores, design saves, finalized customizations, completed bulk orders, and more. Build reactive integrations without polling the API.

Configure endpoints in your dashboard

Head to Dashboard → Webhooks to register an endpoint, pick which events to subscribe to, copy the signing secret, and inspect delivery history.

Quick Start

  1. Open Dashboard → Webhooks and click Add endpoint.
  2. Paste the HTTPS URL on your server that should receive events.
  3. Pick which events to subscribe to (or choose All events to auto-subscribe to anything we add later).
  4. Copy the signing secret — you’ll need it to verify the X-TruCustom-Signature header.
  5. Click Send test event to confirm your endpoint is reachable.

Payload Shape

Every webhook is a JSON POST with the following envelope:

{
  "id":          "9f1e8b2c-7d34-4f6a-9c8b-1a2b3c4d5e6f",
  "event":       "design.finalized",
  "account_id":  "01234567-89ab-cdef-0123-456789abcdef",
  "occurred_at": "2026-05-27T19:42:11Z",
  "api_version": "2026-05",
  "data":        { /* event-specific resource payload */ }
}

The id field is unique per delivery and is also sent in the X-TruCustom-Delivery header. Use it as your idempotency key — we may retry a delivery multiple times, but the id stays the same.

Request Headers

HeaderValue
Content-Typeapplication/json
User-AgentTruCustom-Webhooks/1.0
X-TruCustom-EventThe event ID, e.g. design.finalized
X-TruCustom-DeliveryUnique UUID for this delivery (idempotency key)
X-TruCustom-TimestampUnix timestamp (seconds) the request was signed
X-TruCustom-Signaturesha256=<hex> — HMAC of <timestamp>.<raw_body>

Event Catalog

Below is every event TruCustom can emit, grouped by resource. New events are added to this catalog as we ship features — subscribe to * to auto-receive any event added in the future.

Stores

Event Description
store.created A new store was connected to your account
store.updated Store settings, theme, or credentials were updated
store.deleted A store was disconnected and removed
store.connected A store's connection status flipped to `connected`
store.disconnected A store's connection status changed away from `connected`
store.synced A product sync against the platform completed successfully
store.sync_failed A product sync against the platform failed (the store is left in `error` state)
store.customizer_enabled The customizer was turned on store-wide (the widget renders on the storefront again)
store.customizer_disabled The customizer was turned off store-wide (the widget stops rendering on every product)

Products

Event Description
product.created A new customizable product was added
product.updated A product's metadata, customization config, or images changed
product.deleted A product was removed

Print Areas

Event Description
print_area.created A new customizable zone was defined on a product
print_area.updated A print area's bounds, dimensions, or background changed
print_area.deleted A print area was removed

Templates

Event Description
template.created A new design template was created
template.updated A template's name, canvas data, or active flag changed
template.deleted A template was deleted

Designs

Event Description
design.created A customer started a new design session
design.updated A design's canvas data was saved (autosave or explicit save)
design.finalized A customer completed customization (status flipped to `finalized`)
design.cart_added A finalized design was added to the storefront cart and locked
design.shared A customer enabled sharing on their design (a public share link was generated)
design.deleted A design was deleted

Assets

Event Description
asset.created A new asset (clipart, background, or logo) was uploaded
asset.updated An asset's metadata or file was updated
asset.deleted An asset was removed
font.created A new font was uploaded
font.updated A font's metadata or file was updated
font.deleted A font was removed

Bulk Orders

Event Description
bulk_order.created A new bulk order was created (multiple variants of one design)
bulk_order.updated A bulk order's items, quantities, or status changed
bulk_order.completed A bulk order's status flipped to `completed`

Print Files

Event Description
print_file.generated A high-res print file was rendered for a finalized design

Mockups

Event Description
mockup.generated A bulk marketing mockup ZIP was generated

Pricing

Event Description
pricing_rule.created A new pricing rule was added
pricing_rule.updated A pricing rule's scope, formula, or active flag changed
pricing_rule.deleted A pricing rule was removed
printing_method.created A new printing method was added (DTG, screen print, etc.)
printing_method.updated A printing method's pricing or settings changed
printing_method.deleted A printing method was removed

Webhooks

Event Description
webhook_endpoint.disabled An endpoint was auto-disabled after exceeding the consecutive-failure threshold

Example Payloads

store.created

{
  "id":          "f0a1b2c3-...",
  "event":       "store.created",
  "account_id":  "01234567-...",
  "occurred_at": "2026-05-27T19:42:11Z",
  "api_version": "2026-05",
  "data": {
    "id":                "abc-store-uuid",
    "account_id":        "01234567-...",
    "name":              "Acme T-Shirts",
    "platform":          "shopify",
    "domain":            "acme-tshirts.myshopify.com",
    "store_url":         "https://acme-tshirts.myshopify.com",
    "external_store_id": "62841091201",
    "connection_status": "pending",
    "active":            true,
    "created_at":        "2026-05-27T19:42:11Z",
    "updated_at":        "2026-05-27T19:42:11Z"
  }
}

design.finalized

{
  "id":          "9f1e8b2c-...",
  "event":       "design.finalized",
  "account_id":  "01234567-...",
  "occurred_at": "2026-05-27T19:42:11Z",
  "api_version": "2026-05",
  "data": {
    "id":            "design-uuid",
    "product_id":    "product-uuid",
    "store_id":      "store-uuid",
    "session_token": "abc123...",
    "status":        "finalized",
    "cart_locked":   false,
    "preview_url":   "https://app.trucustom.com/previews/design-uuid",
    "canvas_data":   { /* Fabric.js JSON, keyed by print_area_id */ },
    "metadata":      { "quantity": 1 }
  }
}

bulk_order.completed

{
  "id":          "ab9c1d2e-...",
  "event":       "bulk_order.completed",
  "account_id":  "01234567-...",
  "occurred_at": "2026-05-27T19:42:11Z",
  "api_version": "2026-05",
  "data": {
    "id":             "bulk-order-uuid",
    "design_id":      "design-uuid",
    "status":         "completed",
    "total_quantity": 144,
    "items": [
      { "id": "item-uuid-1", "variant_options": { "size": "M", "color": "Black" }, "quantity": 48 },
      { "id": "item-uuid-2", "variant_options": { "size": "L", "color": "Black" }, "quantity": 96 }
    ]
  }
}

Signature Verification

Every request is signed with HMAC-SHA256 using your endpoint’s signing secret. Always verify the signature before trusting a payload — the X-TruCustom-Signature header is your only proof that the request actually came from TruCustom.

The signing string is:

<X-TruCustom-Timestamp>.<raw_request_body>

The signature is the lowercase hex HMAC-SHA256 digest of that string with your secret as the key, prefixed with sha256=.

Ruby (Rails)

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def trucustom
    body      = request.raw_post
    timestamp = request.headers["X-TruCustom-Timestamp"].to_s
    signature = request.headers["X-TruCustom-Signature"].to_s
    secret    = ENV.fetch("TRUCUSTOM_WEBHOOK_SECRET")

    expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{body}")
    return head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, signature)

    # Reject deliveries older than 5 minutes to neuter replay attacks
    return head :unauthorized if Time.current.to_i - timestamp.to_i > 300

    event = JSON.parse(body)
    case event["event"]
    when "design.finalized" then handle_design_finalized(event["data"])
    when "store.created"    then handle_store_created(event["data"])
    end

    head :ok
  end
end

Node.js (Express)

const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/webhooks/trucustom',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const timestamp = req.headers['x-trucustom-timestamp'];
    const signature = req.headers['x-trucustom-signature'];
    const secret    = process.env.TRUCUSTOM_WEBHOOK_SECRET;

    const expected = 'sha256=' + crypto
      .createHmac('sha256', secret)
      .update(`${timestamp}.${req.body}`)
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
      return res.status(401).send('bad signature');
    }
    if (Math.floor(Date.now() / 1000) - parseInt(timestamp, 10) > 300) {
      return res.status(401).send('stale request');
    }

    const event = JSON.parse(req.body.toString('utf8'));
    console.log('Got TruCustom event:', event.event, event.id);

    res.status(200).send('ok');
  }
);

Python (Flask)

import hmac, hashlib, time, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['TRUCUSTOM_WEBHOOK_SECRET']

@app.post('/webhooks/trucustom')
def trucustom():
    raw = request.get_data()
    ts  = request.headers.get('X-TruCustom-Timestamp', '')
    sig = request.headers.get('X-TruCustom-Signature', '')

    expected = 'sha256=' + hmac.new(
        SECRET.encode(),
        f"{ts}.{raw.decode()}".encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, sig):
        abort(401)
    if int(time.time()) - int(ts) > 300:
        abort(401)

    event = request.get_json()
    return ('', 200)

Responding to Webhooks

Your endpoint should respond with any 2xx status code within 30 seconds:

  • 200 OK — webhook received and processed
  • 202 Accepted — webhook received, will process asynchronously
  • 204 No Content — received, no body

Anything else (3xx, 4xx, 5xx) or a timeout counts as a failure and triggers retries.

Retry Policy

If your endpoint returns a non-2xx status (or times out), TruCustom retries with exponential backoff:

  • Attempt 1 (initial delivery)
  • Attempt 2: 1 minute later
  • Attempt 3: 5 minutes later
  • Attempt 4: 30 minutes later
  • Attempt 5: 2 hours later
  • Attempt 6: 12 hours later (final attempt)

After the final attempt fails, the delivery is marked failed in your dashboard. The next event still gets attempted — one bad delivery does not block subsequent ones.

Auto-Disable

If 5 consecutive deliveries exhaust their retries (i.e. fail completely), TruCustom auto-disables the endpoint to avoid hammering a broken server. When this happens:

  • The endpoint flips to auto-disabled and stops receiving events.
  • A webhook_endpoint.disabled meta-event is sent to other active endpoints in the same account so you get notified through a different path.
  • The endpoint shows a red banner in your dashboard with a Re-enable button.

Re-enabling resets the consecutive-failure counter and resumes deliveries on the next event.

Best Practices

  • Verify the signature on every request — it’s your only proof of authenticity.
  • Respond fast. Acknowledge with 200 immediately, queue any heavy work for a background job.
  • Be idempotent. Use X-TruCustom-Delivery as a dedupe key — retries deliver the same envelope id.
  • Reject stale deliveries. Compare X-TruCustom-Timestamp against now() and reject anything more than ~5 minutes old to neuter replay attacks.
  • Subscribe to * if you want to future-proof your integration. New events get auto-included.
  • Use a separate endpoint per environment. Don’t share the same staging URL across production stores — you’ll get cross-talk and noisy retries.

Managing Endpoints via API

You can also manage endpoints programmatically through the REST API:

GET    /api/v1/webhook_endpoints        # list
POST   /api/v1/webhook_endpoints        # create
GET    /api/v1/webhook_endpoints/:id    # show (includes signing secret)
PATCH  /api/v1/webhook_endpoints/:id    # update
DELETE /api/v1/webhook_endpoints/:id    # delete

All endpoints require an API key — see Authentication for details.