Guide to Treet Webhooks: Features and Best Practices
Treet runs branded resale and trade-in marketplaces, so a brand can take back and resell its own products. Its webhooks report shipping label activity on those bundles, which lets you store tracking numbers, tell the customer their trade-in is on its way, or start inventory and credit steps when it's delivered.
This guide covers how Treet webhooks are set up through support, the Shipping Label payload, opt-in HMAC signing with X-Treet-Signature, and the gaps in what Treet documents about delivery.
What are Treet webhooks?
Treet webhooks aren't configured in a dashboard. You email the Treet team at support@treet.co with the topic, the trigger and the URL to send to, and Treet sets the webhook up. Each webhook has a topic, which defines the payload, and a trigger, which is the event that sends it.
Treet offers one topic, Shipping Label, with two triggers: a label being created and a label's tracking status being updated.
Treet webhook features
| Feature | Details |
|---|---|
| Signature | base64 HMAC-SHA256 |
| Signature header | x-treet-signature |
| Body | JSON |
| Event routing | Body field trigger |
From the Treet webhooks introduction and HMAC page:
- Setup, changes and the signing secret all come from Treet support.
- Signing is optional. When you ask for it, Treet issues a secret key and adds
X-Treet-Signatureto every request. - The method, Content-Type, retries, timeout and expected response aren't documented.
Events and payloads
Route on the top-level trigger. topic is always label for now.
| Event routing | Body field trigger |
Triggers contain a /, so use a safe form (such as label.updated) if you turn one into a file name.
The Shipping Label payload page lists the fields for both triggers: shopId, bundleId, bundleType (TRADE_IN or MARKETPLACE), carrier (USPS), trackingNumber, status (SHIPPED or DELIVERED), updatedAt and items. Item fields such as variantId, sku and variantOptions come from Shopify. Only label/updated has an example:
{
"topic": "label",
"trigger": "label/updated",
"shopId": "boyish",
"bundleId": "12345678-1234-1234-1234-123456789105",
"bundleType": "TRADE_IN",
"carrier": "USPS",
"trackingNumber": "28735625627856237856287",
"status": "SHIPPED",
"updatedAt": "2024-01-01T12:00:00+00:00",
"items": [
{
"id": "11345678-1234-1234-1234-123456789101",
"variantId": "01345678-1234-1234-1234-123456789101",
"sku": "large-pink-shirt",
"stockImageUrl": "https://ucarecdn.com/405d9723-c637-4f50-bd11-a10aaf9ff4ce/",
"variantOptions": {
"size": "L",
"color": "pink"
},
"category": "shirts",
"title": "Large Pink Shirt"
}
]
}
There's no event ID. The combination of bundleId, trigger, status and updatedAt is the closest thing to one.
Setting up Treet webhooks
- Email support@treet.co asking for a Shipping Label webhook. Include the triggers you want (
label/created,label/updated) and your URL, for examplehttps://example.com/webhooks/treet. - In the same email, ask for HMAC signing. Treet will send a secret key.
- Store the key as
TREET_SECRET_KEY. Use it as-is, without hex- or base64-decoding it.
Signing is opt-in: ask Treet support for a secret key and use it as the verification secret. Without one, deliveries are unsigned and fail verification.
To route through Hookdeck, create a Treet source and give Treet its URL. If Treet has issued a key, set it as the source's verification secret. If you haven't asked for signing, leave verification off on the source, because the requests carry no signature to check. For local development:
npx hookdeck-cli listen 3000 treet --path /webhooks/treet
HTTP method
Treet calls its webhooks "HTTP callbacks" and doesn't name the method, though its own Node sample listens with app.post. Hookdeck's Treet source pre-selects POST without managing the method, so allow other methods on the source if you see them. The receiver below uses app.all, verifies whatever method arrives, and treats a missing body as empty bytes.
Securing Treet webhooks
X-Treet-Signature is the base64 HMAC-SHA256 of the raw request body, keyed with the secret key. The value has no prefix, and no timestamp is involved.
Treet's Node sample checks the value with assert.equal, which throws on a mismatch, so a bad signature becomes a 500 instead of a 401, and the comparison isn't constant-time. Decode and compare the digests with timingSafeEqual, as below.
Node.js verification
const crypto = require('node:crypto');
function decodeBase64(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
const bytes = Buffer.from(value, 'base64');
return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
? bytes : null;
}
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['x-treet-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
const received = decodeBase64(candidate);
if (!received) return false;
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler builds a key from the label's fields and queues the update.
const express = require('express');
const app = express();
app.all('/webhooks/treet', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.TREET_SECRET_KEY;
if (!secret) return res.sendStatus(500);
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
if (!verifySignature(body, req.headers, secret)) return res.sendStatus(401);
if (!body.length) return res.sendStatus(200);
let label;
try {
label = JSON.parse(body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!label || typeof label !== 'object' || Array.isArray(label)) return res.sendStatus(400);
if (typeof label.trigger !== 'string' || typeof label.bundleId !== 'string') return res.sendStatus(400);
const key = [label.bundleId, label.trigger, label.status, label.updatedAt].map(String).join('|');
const items = Array.isArray(label.items) ? label.items : [];
try {
if (await alreadyHandled(key)) return res.sendStatus(200);
await queue.add('treet-label', {
trigger: label.trigger,
bundleId: label.bundleId,
bundleType: label.bundleType,
trackingNumber: label.trackingNumber,
status: label.status,
skus: items.map((item) => item && item.sku).filter(Boolean),
});
await markHandled(key);
} catch {
return res.sendStatus(503);
}
res.sendStatus(200);
});
Swap in your own job queue for queue, and your own store for alreadyHandled and markHandled.
Hookdeck's Treet source checks the same base64 HMAC. Treet gets a 200 back either way, and a mismatch is recorded on the request as failed verification. With no timestamp in Treet's signature, Hookdeck applies no age limit either.
Make Treet webhooks production-ready. Hookdeck Event Gateway stores every label update Treet sends, so a delivered trade-in isn't missed while your service is down, whatever Treet's undocumented retry policy turns out to be.
Treet webhook limitations and pain points
Setup goes through support
The Problem: You can't add, change or test a webhook yourself.
Why It Happens: Treet has no self-serve webhook settings. Changes are made by the Treet team on request.
Workarounds:
- Ask for staging and production URLs at the same time.
- Use a stable URL, such as a Hookdeck source, so later routing changes don't need another support request.
How Hookdeck Can Help: With a Hookdeck source as the fixed URL, you change destinations in Hookdeck without contacting Treet.
Signing is opt-in
The Problem: Unless you ask, requests arrive with no signature.
Why It Happens: Treet adds X-Treet-Signature only after issuing a secret key on request.
Workarounds:
- Request signing when you request the webhook.
- Until you have a key, keep the URL private and check each payload's
bundleIdagainst your own records.
How Hookdeck Can Help: Once you have a key, Hookdeck's Treet source verifies every request and records failures.
Delivery behaviour is undocumented
The Problem: You don't know whether Treet retries a failed request or how long it waits.
Why It Happens: Treet's pages describe the payload and signing, not delivery.
Workarounds:
- Respond with
200once the update is queued. - Raise bundles whose status looks stuck with Treet support.
How Hookdeck Can Help: Hookdeck accepts each request at once and retries your service under a policy you choose.
No event ID and one example
The Problem: You have to build your own deduplication key, and label/created has no published body.
Why It Happens: The payload has no ID field, and only label/updated has an example.
Workarounds:
- Use
bundleId,trigger,statusandupdatedAttogether as the key. - Log the first
label/createdyou receive and compare it with the field table.
How Hookdeck Can Help: Hookdeck keeps each delivered body, so you can inspect real label/created payloads.
Best practices
Track status per bundle
Store the latest status per bundleId and ignore updates whose updatedAt is older than what you have, in case requests arrive out of order.
Act on DELIVERED
Credit the customer or release inventory when status becomes DELIVERED, not on SHIPPED.
Keep the handler short
Verify, queue and respond. Shopify lookups by variantId or sku belong in the worker. Our guide to asynchronous webhook processing covers why.
Make retries harmless
Record the composite key before acting, as described in the idempotency guide.
Conclusion
Treet sends Shipping Label webhooks for its resale and trade-in bundles, set up by email and optionally signed with a base64 HMAC-SHA256 in X-Treet-Signature. Ask for signing up front, deduplicate on the bundle and status fields, and plan your own retries, since Treet doesn't publish a delivery policy.
Hookdeck Event Gateway gives Treet webhooks a stable URL, verifies them once signing is on, and retries delivery to your systems.
Get started with Hookdeck for free and handle Treet webhooks reliably in minutes.