# 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](https://apidocs.treet.co/webhooks/introduction) and [HMAC page](https://apidocs.treet.co/webhooks/hmac):

* 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-Signature` to 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](https://apidocs.treet.co/webhooks/shipping-label/payload) 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:

```json
{
  "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

1. Email support@treet.co asking for a Shipping Label webhook. Include the triggers you want (`label/created`, `label/updated`) and your URL, for example `https://example.com/webhooks/treet`.
2. In the same email, ask for HMAC signing. Treet will send a secret key.
3. 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:

```bash
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

```javascript
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.

```javascript
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](/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 `bundleId` against 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 `200` once 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`, `status` and `updatedAt` together as the key.
* Log the first `label/created` you 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](/webhooks/guides/why-implement-asynchronous-processing-webhooks) covers why.

### Make retries harmless

Record the composite key before acting, as described in the [idempotency guide](/webhooks/guides/implement-webhook-idempotency).

## 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](https://hookdeck.com) gives Treet webhooks a stable URL, verifies them once signing is on, and retries delivery to your systems.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Treet webhooks reliably in minutes.