Webhooks & API — Get Notified When Items Sell

Webhooks let FLUF POST to your own server the moment something happens in your account — most usefully, when an item sells on any connected marketplace. Instead of re-entering sales into your own system by hand, your system finds out immediately.

This is a developer feature: you'll need somewhere to receive an HTTP request.

Base URL: https://fluf.io/wp-json/fc/api/v1

Get an API token

  1. Go to Settings → Developer (open it)
  2. Under API tokens, give the token a name (e.g. "Inventory sync") and click Create token
  3. Copy it — it's shown once, and starts with fluf_pat_

Treat it like a password. You can revoke it on the same screen at any time; anything using it stops working immediately. Your token needs an active FLUF plan.

Register your endpoint

curl -X POST "https://fluf.io/wp-json/fc/api/v1/webhooks" \
  -H "Authorization: Bearer fluf_pat_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.example.com/hooks/fluf",
    "events": ["new_sale"],
    "description": "Inventory sync"
  }'
{
  "id": 1,
  "url": "https://yourapp.example.com/hooks/fluf",
  "events": ["new_sale"],
  "secret": "9f8c…",
  "note": "Store this secret now — it is not shown again."
}

Save the secret — you need it to verify deliveries, and it is never shown again. Omit events to receive everything.

Your URL must be https and publicly reachable. Private, loopback and local addresses are rejected. For local development use a tunnel such as ngrok or Cloudflare Tunnel.

Test it before you rely on it

curl -X POST "https://fluf.io/wp-json/fc/api/v1/webhooks/1/test" \
  -H "Authorization: Bearer fluf_pat_your_token"

This fires a real POST at your URL (with "test": true in the body) and returns the status code your server gave back — so you can build and debug your receiver without waiting for a sale.

The new_sale payload

{
  "event": "new_sale",
  "fired_at": "2026-07-29T14:03:11+00:00",
  "fluf_user_id": 1234,
  "order_id": "abc-123-def",
  "channel": "depop",
  "connection_id": 208,
  "sku": "T-001",
  "title": "Vintage Levi's 501 jeans",
  "price": 45.00,
  "currency": "GBP",
  "quantity": 1,
  "buyer_username": "a_buyer",
  "sold_at": "2026-07-28T14:03:11+00:00",
  "order_total": 48.99,
  "items": [
    {
      "sku": "T-001",
      "title": "Vintage Levi's 501 jeans",
      "price": 45.00,
      "quantity": 1,
      "external_id": "123456789",
      "fluf_id": 987654
    }
  ]
}

Worth knowing:

  • sku is your SKU — the reference code on the product in FLUF, not something the marketplace sent back. Most marketplaces don't return a SKU on orders at all, so we resolve it from your FLUF product. Put your own reference code in the SKU field when you create the listing and it comes back to you on the sale.
  • sold_at is the marketplace's timestamp, not when we noticed. It can be minutes or hours before fired_at, depending on how often that channel is synced.
  • items[] is the whole order. For a bundle (several items, one order) the flat sku, title and price fields describe the first line only — read items if you need them all. order_total covers the whole order including shipping.
  • price is what the buyer actually paid, accepted offers included — not the listing price.
  • channel is the marketplace it sold on; connection_id tells accounts apart if you have more than one on the same channel.

Verify every delivery

Each request carries these headers:

HeaderMeaning
X-FLUF-EventEvent key, e.g. new_sale
X-FLUF-Signaturesha256=
X-FLUF-TimestampUnix seconds, signed together with the body
X-FLUF-DeliveryUnique id for this attempt
X-FLUF-Attempt1 on the first try, higher on retries

The signature is HMAC-SHA256 of "{timestamp}.{raw request body}", keyed with your secret. Sign the raw body — re-serialising the JSON changes the bytes and the signatures won't match.

import hmac, hashlib, time

def verify(secret: str, body: bytes, signature: str, timestamp: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(f"sha256={expected}", signature):
        return False
    # Reject anything older than 5 minutes to stop replays.
    return abs(time.time() - int(timestamp)) < 300
const crypto = require('crypto');

function verify(secret, rawBody, signature, timestamp) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody)          // Buffer — do not JSON.stringify a parsed object
    .digest('hex');
  const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  return ok && Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
}

Reject anything that fails verification. Without this check, anyone who learns your URL could post fake sales into your system.

Retries and reliability

Respond 2xx quickly. Anything else — or a timeout beyond 10 seconds — counts as a failure. Do the minimum inline (write to a queue or a table) and process afterwards.

Failed deliveries are retried after 1 minute → 5 minutes → 30 minutes → 2 hours → 12 hours, then stop. X-FLUF-Attempt tells you which try you're on.

Deliveries are at-least-once, not exactly-once. If your server processes a request but fails to reply in time, the retry arrives and you'll see it twice. Deduplicate on order_id (or X-FLUF-Delivery) — never assume exactly one POST per sale.

After 20 consecutive failures a webhook is automatically disabled, so a dead endpoint doesn't retry forever. Check GET /webhooks for is_active and last_error, and inspect individual attempts:

curl "https://fluf.io/wp-json/fc/api/v1/webhooks/1/deliveries" \
  -H "Authorization: Bearer fluf_pat_your_token"

That returns the last 50 attempts with status codes and errors — so "did it actually fire?" always has an answer.

Events

GET /events returns the full catalogue with a sample payload for each.

EventFires when
new_saleAn item sold on any connected marketplace
new_listingA product was created in FLUF
listing_crosslistedA product went live on a marketplace
crosslisting_errorA crosslist attempt failed
listing_sold_outA product is no longer live on any channel
oos_alertStock hit zero
crosslisting_job_completedA bulk crosslist run finished

All endpoints

MethodPathPurpose
GET/webhooksList your webhooks, with health info
POST/webhooksRegister one; returns the signing secret once
DELETE/webhooks/{id}Remove one
POST/webhooks/{id}/testFire a synthetic event, synchronously
GET/webhooks/{id}/deliveriesRecent delivery attempts
GET/eventsEvent catalogue with sample payloads

Errors

CodeMeaning
400 https_requiredThe URL wasn't https
400 private_hostHost resolves to a private/local address, or doesn't resolve at all
400 unknown_eventEvent key not recognised — see GET /events
401Token invalid or revoked
403 no_active_subscriptionThe API needs an active FLUF plan
404 not_foundNo webhook with that id on your account
  • Drive FLUF from an AI assistant — the same token works with our MCP server, so Claude or Cursor can read your inventory, crosslist and check orders: npm install -g fluf-mcp
  • Read and write endpoints (/products, /orders, /crosslist) are coming to fc/api/v1. Email [email protected] if you need them and we'll prioritise.

Questions: [email protected]

Back to Help Center