Guide to ShipHero Webhooks: Features and Best Practices
ShipHero webhooks notify your systems about fulfillment activity: an order is allocated, a shipment updates, inventory changes. If you're building on ShipHero, webhooks are how you react to warehouse and order events without polling the GraphQL API.
This guide covers how ShipHero webhooks work, the webhook types, how to verify the x-shiphero-hmac-sha256 header, the delivery behavior, and the best practices for production.
What are ShipHero webhooks?
ShipHero webhooks are HTTP POSTs delivered to a URL you register per webhook type via the GraphQL API. Each is signed with an HMAC-SHA256 of the raw JSON body, base64-encoded, in the x-shiphero-hmac-sha256 header. The key is the app's shared_signature_secret, returned once by the webhook_create mutation.
ShipHero webhook features
| Feature | Details |
|---|---|
| Configuration | GraphQL webhook_create mutation (name = webhook type, url, shop_name) |
| Signature header | x-shiphero-hmac-sha256 |
| Signature scheme | base64 HMAC-SHA256 over the raw body, keyed with shared_signature_secret |
| Secret | Returned once by webhook_create |
| Deduplication | X-Shiphero-Message-ID |
| Delivery | ~10s timeout (20s for Generate Label), up to 5 retries per trigger |
| While disabled | Events are discarded (not queued) |
| SDK | None |
Common webhook types
ShipHero webhook type names are Title Case strings (the name you pass to webhook_create):
| Type | Fires when |
|---|---|
Order Allocated | An order is allocated |
Shipment Update | A shipment updates |
Inventory Update | Inventory changes |
Order Canceled | An order is canceled |
Tote Complete | A tote is completed |
PO Update | A purchase order updates |
Other types include Return Update, Package Added, and Generate Label Webhook. Register one webhook per type; consult ShipHero's webhook reference for the full list.
Setting up ShipHero webhooks
Register via the GraphQL webhook_create mutation, passing the exact webhook type string as name, your url, and shop_name. The mutation returns the shared_signature_secret once, capture it. Note that ShipHero does not queue events while a webhook is disabled; they're discarded.
Securing ShipHero webhooks
Each delivery carries an x-shiphero-hmac-sha256 header: a base64 HMAC-SHA256 of the raw JSON body, keyed with your shared_signature_secret. This is a plain HMAC of the raw body (not the body concatenated with an account id or anything else). Compute over the exact raw bytes and compare in constant time.
const crypto = require("crypto");
const SECRET = process.env.SHIPHERO_SIGNATURE_SECRET; // shared_signature_secret
function verify(rawBody, signature) {
const expected = crypto
.createHmac("sha256", SECRET)
.update(rawBody)
.digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(signature || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.headers["x-shiphero-hmac-sha256"])) {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge fast (~10s timeout)
processQueue.add({
messageId: req.headers["x-shiphero-message-id"], // dedupe on this
body: JSON.parse(req.body),
});
});
The same check in Python:
import base64
import hashlib
import hmac
import os
SECRET = os.environ["SHIPHERO_SIGNATURE_SECRET"].encode()
def verify(raw_body: bytes, signature: str) -> bool:
digest = hmac.new(SECRET, raw_body, hashlib.sha256).digest()
expected = base64.b64encode(digest).decode()
return hmac.compare_digest(expected, signature or "")
ShipHero webhook limitations and pain points
Events are discarded while a webhook is disabled
The Problem: ShipHero doesn't queue events for a disabled webhook, they're dropped. A webhook that's disabled (or an endpoint down long enough to be disabled) loses events permanently.
Why It Happens: ShipHero discards rather than buffering for disabled webhooks.
Workarounds:
- Keep the webhook enabled and the endpoint healthy; reconcile via the GraphQL API for anything you can't afford to miss.
How Hookdeck Can Help: Hookdeck always accepts deliveries and absorbs downstream failures itself, keeping the webhook enabled while it durably queues events, so a downstream outage doesn't drop them.
The secret is returned once
The Problem: webhook_create returns the shared_signature_secret only once. Lose it and you can't verify without recreating the webhook.
Why It Happens: ShipHero shows the secret at creation only.
Workarounds:
- Capture and store the secret securely when you create the webhook.
How Hookdeck Can Help: Hookdeck verifies at the edge, so the secret lives in one place.
Title Case type names and per-type webhooks
The Problem: Webhook types are Title Case strings (Order Allocated, not order_allocated), and you register one webhook per type, so a typo or wrong casing fails registration or routing.
Why It Happens: ShipHero uses exact Title Case type strings.
Workarounds:
- Register with the exact type string and route on it; keep a list of the types you use.
How Hookdeck Can Help: Hookdeck consolidates multiple ShipHero webhooks into one ingestion point with routing.
Retries and duplicates
The Problem: Up to 5 retries per trigger (within a ~10s timeout, 20s for Generate Label) means duplicates.
Why It Happens: At-least-once delivery favors eventual delivery.
Workarounds:
- Dedupe on
X-Shiphero-Message-IDand make side effects idempotent.
How Hookdeck Can Help: Hookdeck deduplicates on the message id at the edge. See our guide to webhook idempotency.
Best practices
Verify the plain HMAC over the raw body
Compute base64 HMAC-SHA256 over the raw body with shared_signature_secret and compare in constant time. It's a plain HMAC, nothing concatenated.
Keep webhooks enabled and healthy
Since disabled webhooks drop events, keep the endpoint healthy and reconcile via the API for critical data.
Dedupe on the message id
Use X-Shiphero-Message-ID as an idempotency key across the up-to-5 retries.
Acknowledge fast, process asynchronously
Return 200 within the timeout and defer work to a queue. See why to process webhooks asynchronously.
Make ShipHero webhooks production-ready
Hookdeck verifies x-shiphero-hmac-sha256, deduplicates, and durably queues every fulfillment event
Conclusion
ShipHero webhooks verify with an x-shiphero-hmac-sha256 base64 HMAC over the raw body, keyed with the shared_signature_secret returned once at creation. Use the exact Title Case type names, keep webhooks enabled (disabled ones drop events), dedupe on X-Shiphero-Message-ID, and acknowledge within the timeout.
Hookdeck verifies the signature, keeps webhooks alive by absorbing downstream failures, deduplicates, and durably queues every event at the edge, so your app processes verified, unique fulfillment events without losing any.
Get started with Hookdeck for free and handle ShipHero webhooks reliably in minutes.