DevelopersDocs › 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_idIdentifies the event. The same on every retry โ€” deduplicate on this
event_typee.g. listing.sold
channelThe marketplace, lowercase
accountWhich of your accounts on it. May be empty โ€” see below
item_idThe FLUF product id. Absent on account-scoped events
listing_idThe marketplace's own id, once a listing exists
occurred_atWhen it happened on the marketplace
observed_atWhen we saw it
occurred_at_exactWhether occurred_at is real or defaulted โ€” see below
seqMonotonic 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

EventFires when
listing.createdA listing went up, carrying the price it was actually listed at
listing.liveThe listing is visible to buyers, carrying its public URL
listing.updatedA change reached the marketplace. changed names the fields
listing.rejectedThe marketplace refused the listing โ€” the item needs editing
listing.errorThe attempt failed for a reason unrelated to the item
listing.delistedA listing came down. reason says why
listing.soldAn 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

EventFires when
listing.likedSomeone liked an item โ€” with whether they've also messaged or offered
message.receivedA 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.followerAn 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

EventFires when
offer.receivedA buyer made an offer
offer.acceptedAn offer was accepted โ€” the item sold at that price
offer.declinedAn 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.

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 listing attempt failed
listing_sold_outA product is no longer live anywhere
oos_alertStock hit zero
crosslisting_job_completedA 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.

Scroll to Top