Developers › Docs › Webhook events
Webhook events
Every webhook event we send, what it carries, and when it fires.
Every event FLUF delivers to a registered webhook. This is the payload reference for Webhooks: use that page for registration, signatures, retries and delivery behaviour.
The machine-readable version is events.yaml. GET /events returns the same catalogue with a sample payload for each, generated from the running code โ so if the two ever disagree, the endpoint is right.
The envelope
Every detailed event carries these fields, merged flat into the body alongside the event's own fields โ not nested.
| Field | |
|---|---|
event_id | Identifies the event. The same on every retry โ deduplicate on this |
event_type | e.g. listing.sold |
channel | The marketplace, lowercase |
account | Which of your accounts on it. May be empty โ see below |
item_id | The FLUF product id. Absent on account-scoped events |
listing_id | The marketplace's own id, once a listing exists |
occurred_at | When it happened on the marketplace |
observed_at | When we saw it |
occurred_at_exact | Whether occurred_at is real or defaulted โ see below |
seq | Monotonic per item. Use it to discard stale events |
Three things that will bite you if you skip them
Deduplicate on event_id. Delivery is at-least-once. If your server processes a request but fails to reply in time, the retry arrives and you'll see the event twice. event_id identifies the event itself and does not change across retries, so storing the ones you've handled is enough.
Order isn't guaranteed โ use seq. A delivery that failed and retried lands after whatever came next. We don't claim ordering we can't provide over HTTP; instead every event carries seq, monotonic within one item. Keep the highest you've applied per item and ignore anything lower. Only compare seq between events about the same item.
An empty account means "unknown", not "the default one". Where a business has several accounts on one marketplace and we can't determine unambiguously which one an event belongs to, we send an empty string rather than guess. Guessing would attribute a sale to the wrong shop, which is how an oversell happens. Treat empty as unknown and fall back to GET /items/{vid}.
Timestamps
occurred_at is when it happened on the marketplace; observed_at is when we saw it. Most marketplaces expose no timestamp of their own, in which case the two are equal and occurred_at_exact is false. Check that flag before treating the gap between them as latency โ otherwise you're measuring a default.
Listing events
| Event | Fires when |
|---|---|
listing.created | A listing went up, carrying the price it was actually listed at |
listing.live | The listing is visible to buyers, carrying its public URL |
listing.updated | A change reached the marketplace. changed names the fields |
listing.rejected | The marketplace refused the listing โ the item needs editing |
listing.error | The attempt failed for a reason unrelated to the item |
listing.delisted | A listing came down. reason says why |
listing.sold | An item sold, with the price and the marketplace's order id |
rejected vs error
Both carry the same body โ an errors array โ but they mean opposite things:
listing.rejectedโ something about the item is wrong. A category, a size, a brand, a banned word, an image. Someone has to change it; retrying the identical payload fails identically.listing.errorโ nothing is wrong with the item. A connection needs reauthorising, a rate limit was hit, the marketplace had an outage. The same payload succeeds once the condition clears.
Branch on errors[].retryable rather than on the event name if you only want one code path. An unrecognised failure is reported retryable, deliberately: telling you to give up on a listing that would have succeeded is the more expensive mistake.
listing.delisted reasons
sold_elsewhere ยท ended_by_us ยท ended_by_channel ยท expired ยท out_of_stock ยท unknown
sold_elsewhere is the one to act on โ it means we pulled this listing because the item sold on another marketplace.
listing.sold
The most urgent event: until the item is down everywhere, it can sell twice. One event per line item, so the thing that sold is never ambiguous.
shopify_order_id is optional. A sale can be observed before a mirror order exists, and a delist keys on item_id, so a missing join key never blocks the urgent half of your reaction.
Engagement events
| Event | Fires when |
|---|---|
listing.liked | Someone liked an item โ with whether they've also messaged or offered |
message.received | A buyer sent you a message on Depop or Vinted, the marketplaces the FLUF Inbox covers. Inbound only; your own replies don't fire it |
account.follower | An account gained followers |
account.follower is account-scoped: no item_id or listing_id. It carries total_followers and gained. It does not name the individual who followed โ that isn't something we can see.
Offer events
| Event | Fires when |
|---|---|
offer.received | A buyer made an offer |
offer.accepted | An offer was accepted โ the item sold at that price |
offer.declined | An offer was declined or expired |
offer.accepted is a sale. Treat it like listing.sold: the item needs to come down everywhere else.
sent_by distinguishes buyer from fluf โ the latter meaning we sent the offer on your behalf to someone who liked the item.
Original events
The first-generation events, still delivered and unchanged. They pre-date the envelope above and carry a flatter, simpler payload.
| 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 listing attempt failed |
listing_sold_out | A product is no longer live anywhere |
oos_alert | Stock hit zero |
crosslisting_job_completed | A bulk listing run finished |
New integrations should prefer the dotted events above โ they carry the account, the ordering key and a stable id.
Coverage
Not every event reaches every marketplace; it depends what each one exposes. An event that never fires for a channel isn't an error โ it means that marketplace doesn't surface the underlying signal.
Something missing? Email [email protected] โ we prioritise by what people actually ask for.
