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
- Go to Settings → Developer (open it)
- Under API tokens, give the token a name (e.g. "Inventory sync") and click Create token
- 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:
skuis 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_atis the marketplace's timestamp, not when we noticed. It can be minutes or hours beforefired_at, depending on how often that channel is synced.items[]is the whole order. For a bundle (several items, one order) the flatsku,titleandpricefields describe the first line only — readitemsif you need them all.order_totalcovers the whole order including shipping.priceis what the buyer actually paid, accepted offers included — not the listing price.channelis the marketplace it sold on;connection_idtells accounts apart if you have more than one on the same channel.
Verify every delivery
Each request carries these headers:
| Header | Meaning |
|---|---|
X-FLUF-Event | Event key, e.g. new_sale |
X-FLUF-Signature | sha256= |
X-FLUF-Timestamp | Unix seconds, signed together with the body |
X-FLUF-Delivery | Unique id for this attempt |
X-FLUF-Attempt | 1 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.
| Event | Fires when |
|---|---|
new_sale | An item sold on any connected marketplace |
new_listing | A product was created in FLUF |
listing_crosslisted | A product went live on a marketplace |
crosslisting_error | A crosslist attempt failed |
listing_sold_out | A product is no longer live on any channel |
oos_alert | Stock hit zero |
crosslisting_job_completed | A bulk crosslist run finished |
All endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /webhooks | List your webhooks, with health info |
POST | /webhooks | Register one; returns the signing secret once |
DELETE | /webhooks/{id} | Remove one |
POST | /webhooks/{id}/test | Fire a synthetic event, synchronously |
GET | /webhooks/{id}/deliveries | Recent delivery attempts |
GET | /events | Event catalogue with sample payloads |
Errors
| Code | Meaning |
|---|---|
400 https_required | The URL wasn't https |
400 private_host | Host resolves to a private/local address, or doesn't resolve at all |
400 unknown_event | Event key not recognised — see GET /events |
| 401 | Token invalid or revoked |
403 no_active_subscription | The API needs an active FLUF plan |
404 not_found | No webhook with that id on your account |
Related
- 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 tofc/api/v1. Email [email protected] if you need them and we'll prioritise.
Questions: [email protected]