Skip to content

Server hooks

Lifecycle hooks live in server/handlers.lua and are named Handlers.On…. They run after something has happened and cannot undo it; their return value is ignored. Every data table also has hook, timestamp, schemaVersion and resource (details).

Area Hooks
Sales and auctions OnSaleCompleted, OnAuctionEnded, OnReverseOfferAccepted
Listings OnListingCreated, OnListingExpired, OnListingCancelled, OnListingBumped
Bids OnBidPlaced, OnProxyBidSet
NPC delivery OnDeliveryCompleted, OnDeliveryFailed, OnDeliveryRobbed
In-person trades and barter OnInPersonTradeCompleted, OnInPersonTradeCancelled, OnMeetupStatusChanged, OnBarterProposalCreated, OnBarterProposalStatusChanged
Refunds OnRefundRequested, OnRefundApproved, OnRefundRejected
Social OnPlayerRated, OnMessageSent, OnFavoriteChanged, OnFollowChanged, OnWishlistChanged, OnReportCreated
Profile and mailbox OnProfileCreated, OnProfileUpdated, OnMailboxClaimed
Administration OnAdminAction
config.lua callbacks Config.Notify, Config.OnDeliveryRobbed

When: a buyer pays for a fixed-price listing, a bundle, a Dutch auction, or uses Buy Now on an auction. It fires as soon as the payment is committed.

What happens next depends on the delivery mode, told by data.deliveryMode and data.stage:

deliveryMode stage settlementPending What happens next
'mailbox' 'settled' false The item is already in the buyer’s inventory (or in their marketplace mailbox if it did not fit). Nothing else fires
'meetup' 'settled' false The item waits in the buyer’s mailbox, to be collected at the meetup point. Nothing else fires
'npc' 'committed' true A courier van is dispatched. OnDeliveryCompleted, OnDeliveryFailed or OnDeliveryRobbed fires later
'person' 'committed' true An in-person trade is created (the seller must be online, or the purchase is refused). OnInPersonTradeCompleted or OnInPersonTradeCancelled fires later
Field Meaning
buyerSource, buyerId, buyerName The buyer
sellerSource, sellerId, sellerName The seller (sellerSource is nil if offline)
item, itemLabel, itemMetadata, quantity What was sold
category, rarity Category and rarity
basePrice Listing price paid (after discounts), without extras
grossPaid Total paid by the buyer
platformFee, deliveryFee, modeSurcharge, npcDeliveryFee, insuranceFee Fees and extras (money fields)
sellerPayout What the seller receives
price Old alias of basePrice
deliveryMode 'mailbox', 'meetup', 'npc' or 'person'
listingType 'fixed', 'auction' (Buy Now), 'dutch' or 'bundle'
stage, settlementPending See the table above
listingId Listing ID

Example: XP for the seller when the sale is final

Handlers.OnSaleCompleted = function(data)
if data.settlementPending then return end -- courier / in person: wait for the final hook
TriggerEvent('yourxp:addXP', data.sellerId, math.floor(data.basePrice / 100))
end

How to test

  1. With the logger on, Config.Debug = true, run /markettest listing mailbox fixed bread 50.
  2. Give yourself money if needed: /markettest grant 1000.
  3. Open the marketplace and buy the Test item: bread listing.
  4. The console shows OnSaleCompleted with deliveryMode = mailbox, stage = settled.
  5. Repeat with /markettest listing npc fixed bread 50 to see stage = committed and settlementPending = true.

When: an ascending or blind auction reaches its end time with at least one bid. The script checks for finished auctions every 30 seconds, so it can fire up to 30 s after the end time. The item has already been sent to the winner’s mailbox (or to the meetup point) and the money to the seller’s mailbox.

An auction that ends with no bids fires OnListingExpired instead.

Field Meaning
winnerSource, winnerId, winnerName Highest bidder
sellerSource, sellerId, sellerName The seller
item, itemLabel, itemMetadata, category, rarity, quantity The item
winningBid / grossPrice Winning bid
platformFee, sellerPayout Fee and what the seller receives
finalPrice Old alias of sellerPayout
listingType 'auction' or 'blind'
deliveryMode, meetupPoint How the winner collects it
stage 'settled'
listingId Listing ID

Example: announce big auctions in chat

Handlers.OnAuctionEnded = function(data)
if data.winningBid >= 100000 then
TriggerClientEvent('chat:addMessage', -1, {
args = { 'Auction house', ('%s won %s for $%s'):format(data.winnerName, data.itemLabel, data.winningBid) }
})
end
end

How to test

  1. /markettest listing mailbox auction bread 100 creates a test auction that ends in 5 minutes.
  2. Bid on it from your account.
  3. Wait 5 minutes (plus up to 30 s). The console shows OnAuctionEnded and your client gets sl-marketplace:onAuctionWon.

When: in a reverse auction (a buyer posts “I’m looking for X, up to $Y”), the buyer accepts one of the sellers’ offers. The seller’s item goes to the buyer, the seller is paid and the rest of the buyer’s deposit is returned.

Field Meaning
buyerSource, buyerId, buyerName Author of the request (the one paying)
sellerSource, sellerId, sellerName The seller whose offer was accepted
item, itemLabel, itemMetadata, quantity The item
grossPrice Accepted offer
platformFee, sellerPayout Fee and what the seller receives
depositRefund Part of the deposit returned to the buyer
messageId, listingId The offer and the request
stage 'settled'

How to test: two players. A creates a reverse listing; B sends an offer from the listing; A accepts it. CanBuyListing also runs first with action = 'accept_reverse_offer'.

When: a listing of any type has been published (the item is already out of the seller’s inventory). The seller’s client also gets sl-marketplace:onListingCreated.

Field Meaning
sellerSource, sellerId, sellerName The seller
item, itemLabel, itemKind, itemMetadata, category, rarity, quantity The item
price / basePrice Price or starting price
buyNowPrice Buy Now price, or nil
listingType 'fixed', 'auction', 'blind', 'dutch', 'reverse', 'bundle' or 'barter'
deliveryMode, deliveryModes, meetupPoint Delivery options
featured, featuredCost Whether it was featured and the cost
expiresAt End time (Unix seconds), or nil for listings that do not expire
images, bundleItems, wantedItems, wantsAny Extra content
listingId New listing ID

Example: log featured listings

Handlers.OnListingCreated = function(data)
if data.featured then
print(('[market] %s paid $%d to feature %s'):format(data.sellerName, data.featuredCost, data.itemLabel))
end
end

How to test: open the marketplace, publish any item and check the console.

When: a listing with an end time reaches it without being sold. The check runs every 30 seconds.

Only these listing types expire: auction and blind auction without bids, Dutch auction, reverse auction and barter. Fixed-price listings and bundles never expire.

listingType What the script does returnMode
'auction', 'blind' (no bids) Item back to the seller’s mailbox 'mailbox'
'dutch' Item back to the seller’s mailbox 'mailbox'
'barter' Item back to the seller; every open proposal is rejected and its items and money returned 'mailbox'
'reverse' The buyer’s deposit is returned (if Config.Auctions.Reverse.DepositRequired) 'deposit_refund' or 'none'
Field Meaning
sellerSource, sellerId, sellerName Owner of the listing
item, itemLabel, itemMetadata, category, rarity, quantity The item
price / basePrice Listing price
listingType Type (see above)
deliveryMode, meetupPoint Delivery options
reason Currently always 'timeout'
returnMode See above
expiredAt When it expired (Unix seconds)
listingId Listing ID

How to test: /markettest listing mailbox auction bread 100 and don’t bid. After 5 minutes (plus up to 30 s) the console shows OnListingExpired with returnMode = mailbox.

When: a listing is cancelled, by its owner or by an admin from the panel. The item is returned to the seller.

Field Meaning
actorSource, actorId, actorName Who cancelled it
sellerSource, sellerId, sellerName Owner of the listing
item, itemLabel, itemMetadata, quantity, price The listing
listingType Type
cancellationType 'owner' or 'admin'
listingId Listing ID

An owner cancellation first passes CanCancelListing; an admin cancellation also fires OnAdminAction with action = 'force_cancel'.

How to test: publish something and cancel it (cancellationType = 'owner'). Then publish again and cancel it from /marketadmin ('admin').

When: a seller pays Config.BumpCost to push one of their listings back to the top of the results. Limited to once every 5 minutes (Config.RateLimits.bump).

Field Meaning
sellerSource, sellerId, sellerName The seller
item, itemLabel, quantity The listing
cost Amount paid
bumpedAt When (Unix seconds)
listingId Listing ID

How to test: bump one of your listings from the marketplace.

When: a bid is accepted on an auction. It fires for bids placed by hand (bidType = 'manual') and for automatic bids placed by proxy bidding on someone’s behalf (bidType = 'proxy_auto').

Field Meaning
bidderSource, bidderId, bidderName Who bid
sellerId, sellerName The seller
item, itemLabel The item
amount New bid
previousBid Previous highest bid
bidType 'manual' or 'proxy_auto'
listingType 'auction' or 'blind'
listingId Listing ID

Example: warn staff about suspicious jumps

Handlers.OnBidPlaced = function(data)
if data.previousBid and data.previousBid > 0 and data.amount > data.previousBid * 10 then
print(('[market] big jump on #%d: %d -> %d by %s'):format(data.listingId, data.previousBid, data.amount, data.bidderName))
end
end

How to test: bid on a test auction (/markettest listing mailbox auction bread 100). To see proxy_auto, a second player sets a proxy maximum and you then outbid them.

When: a player sets or changes their maximum for proxy bidding (Config.Auctions.ProxyBidding.Enabled). The script will bid for them automatically up to that amount.

Field Meaning
bidderSource, bidderId, bidderName The player
sellerId, sellerName The seller
maxAmount Their maximum
minimumBid Minimum valid bid when it was set
listingId Listing ID

How to test: open a test auction and set a proxy maximum instead of a normal bid.

When: a courier van reaches the buyer and hands over the item; the seller is paid now. This is the final moment of an NPC sale. The buyer’s client also gets sl-marketplace:onDeliveryArrived and sl-marketplace:onDeliveryResult (result = 'delivered').

Field Meaning
deliveryId Delivery ID
buyerSource, buyerId, buyerName The buyer
sellerSource, sellerId, sellerName The seller
item, itemLabel, itemMetadata, quantity The item
basePrice, grossPaid, platformFee, deliveryFee, modeSurcharge, npcDeliveryFee, insuranceFee, sellerPayout Money fields
price Old alias of sellerPayout
deliveryMode, stage 'npc', final stage
listingId Listing ID

How to test: /markettest listing npc fixed bread 50, buy it and wait for the van (about 1–2 minutes with the default MinDistance / MaxDistance).

When: a courier delivery ends without the item being delivered (and without being robbed). The buyer is refunded automatically.

reason Cause
'timeout' The van did not arrive within Config.Gameplay.NPCDelivery.DeliveryTimeout (600 s by default)
'buyer_offline' The buyer left the server during the delivery
'destroyed' The van was destroyed
'unknown' Any other failure
Field Meaning
deliveryId Delivery ID
buyerSource, buyerId, buyerName, sellerSource, sellerId, sellerName The players
item, itemLabel, itemMetadata, quantity The item
price / basePrice, grossPaid What was charged
refundAmount Refunded to the buyer
deliveryFee, modeSurcharge, npcDeliveryFee, insuranceFee Extras
deliveryMode, stage, reason, listingId Details

How to test: buy an NPC test listing and, while the van is on its way, destroy it (destroyed) or disconnect (buyer_offline). For timeout, temporarily set DeliveryTimeout = 30 and block the van’s road.

When: another player robs a courier van: they force the cargo hold (the timing minigame) or, if Config.Gameplay.NPCDelivery.PoliceAlertOnKill is true, they kill the driver. It fires once per delivery, for whichever happens first. Config.OnDeliveryRobbed runs just before it. The robber’s client gets sl-marketplace:onDeliveryRobbed.

Field Meaning
deliveryId Delivery ID
thiefSource, thiefId, thiefName The robber
buyerId, sellerId Buyer and seller
item, itemLabel, itemMetadata, quantity Cargo
value Value of the cargo
trigger 'cargo_stolen' or 'driver_killed'
stage 'robbed'
listingId Listing ID

Example: police dispatch

Handlers.OnDeliveryRobbed = function(data)
local coords = GetEntityCoords(GetPlayerPed(data.thiefSource))
TriggerEvent('your_dispatch:alert', {
code = '10-90', message = 'Courier van robbery', coords = coords, job = 'police'
})
end

How to test: two players. Player B buys an NPC listing (/markettest listing npc fixed bread 50); player A intercepts the van, threatens the driver and clears the minigame on the cargo hold. The order must be worth at least MinValueForIntercept.

When: both players have confirmed the exchange face to face, for a sale with the in person delivery mode or a barter. This is when items and money actually change hands. Both clients get sl-marketplace:onInPersonTradeCompleted.

Field Meaning
tradeId Trade ID
buyerSource, buyerId, buyerName, sellerSource, sellerId, sellerName The players
item, itemLabel, quantity The item (nil for barter)
price / basePrice Price (0 for barter)
grossPaid, platformFee, sellerPayout Money
isBarter, tradeKind true / 'barter' or false / 'person'
barter What each side gave, for barter trades
stage 'settled'
listingId Listing ID

How to test: two players, both online. B buys A’s listing choosing in person, they agree a meeting point in the chat, meet there and both confirm (CanConfirmInPersonTrade runs for each one).

When: an in-person or barter trade is cancelled: by either player, because one of them left the meeting area for more than AbandonTimeoutSeconds, or because TimeoutMinutes passed. Both are refunded automatically.

Field Meaning
tradeId Trade ID
buyerSource, buyerId, buyerName, sellerSource, sellerId, sellerName The players
item, itemLabel, quantity, price / basePrice The deal
isBarter, tradeKind, stage Details
reason Why, as text (for example a timeout or who cancelled it)
listingId Listing ID

How to test: start an in-person trade as above and cancel it from the chat.

When: the meeting point of an in-person trade changes state in the chat.

status Meaning
'proposed' A player proposed a location
'accepted' The other player accepted it (the blip and prompts appear)
'declined' The other player declined it
'cancelled' The author withdrew it
'expired' Nobody answered within MeetRequestTimeoutSeconds (300 s)
Field Meaning
actorSource, actorId, actorName Who changed it
partnerSource, partnerId The other player
status See above
tradeIds Trades covered by this meeting
coords Proposed location
windowEndsAt When the meeting window closes

How to test: in an in-person trade, propose a location from the chat, then accept it or decline it with the other player.

When: a barter proposal or a seller’s counteroffer has been sent (after CanCreateBarterProposal). The offered items and money are now held in escrow.

Field Meaning
actorSource, actorId Who sent it
recipientSource, recipientId Who receives it
proposalType 'proposal' or 'counter'
offeredItems, wantedItems, offerAmount Contents
deliveryMode Chosen delivery mode
messageId, listingId IDs

How to test: two players; B sends a proposal on A’s barter listing, then A counters it.

When: a barter proposal changes state.

status Meaning
'accepted' Accepted; the trade is created
'countered' Answered with a counteroffer
'rejected' Rejected; the offered items and money go back
'cancelled' Withdrawn by its author
'completed' The trade finished
Field Meaning
actorSource, actorId, actorName Who changed it
proposalFromId, proposalToId Author and recipient
status See above
messageId, tradeId, listingId IDs

How to test: follow a barter from proposal to acceptance, and another one to rejection.

Refunds need Config.Refunds.Enabled = true. A buyer can request one within WindowDays (7) of the purchase.

When: a buyer opens a refund request (after CanRequestRefund).

Field Meaning
refundId, historyId Request and purchase
buyerSource, buyerId, buyerName, sellerSource, sellerId, sellerName The players
item, itemLabel, quantity What was bought
amount Amount requested
reason Buyer’s explanation
listingId Listing ID

When: a refund is approved and the money returned to the buyer. Three ways: the seller approves it, an admin forces it (forced = true, and OnAdminAction also fires), or nobody answers within AutoApproveAfterHours (72 h; checked every hour).

Field Meaning
refundId, historyId Request and purchase
buyerSource, buyerId, buyerName, sellerSource, sellerId, sellerName The players
item, itemLabel, quantity What was bought
originalPrice Price paid
amount / refundAmount Amount refunded
forced true if an admin forced it
resolvedBy, resolutionType, status, stage How it was resolved
listingId Listing ID

When: the seller rejects a refund request.

Field Meaning
refundId, historyId Request and purchase
buyerSource, buyerId, buyerName, sellerSource, sellerId, sellerName The players
amount Amount requested
note The seller’s note
resolvedBy Who rejected it
listingId Listing ID

How to test the three: two players. B buys from A and requests a refund (OnRefundRequested). A approves it (OnRefundApproved) or rejects it (OnRefundRejected). Force another one from /marketadmin to see forced = true.

When: a buyer rates a seller after a purchase.

Field Meaning
raterSource, raterId, raterName Who rated
sellerSource, sellerId, sellerName Who was rated
stars 1 to 5
comment Comment, may be an empty string
historyId, listingId Purchase and listing
stage 'submitted'

Example: reward reviews

Handlers.OnPlayerRated = function(data)
if #data.comment >= 20 and data.raterSource then
exports.ox_inventory:AddItem(data.raterSource, 'money', 50)
end
end

How to test: buy something from another player and rate them.

When: a message is sent in a listing’s chat: normal text, price offers, barter proposals… The recipient’s client gets sl-marketplace:onMessageReceived.

Field Meaning
senderSource, senderId, senderName Sender
recipientSource, recipientId, recipientName Recipient
messageId, listingId IDs
messageKind Kind of message (text, offer, barter…)
body Text
offerAmount, offeredItems, deliveryMode For offers and proposals

How to test: open another player’s listing and send them a message.

When: a player adds or removes a listing from their favourites.

Field Meaning
playerSource, playerId, playerName The player
listingId Listing
favorited true added, false removed

When: a player follows or unfollows a seller.

Field Meaning
followerSource, followerId, followerName The player
sellerSource, sellerId The seller
following true follows, false stopped

When: a player adds or removes an entry from their wishlist (Config.Wishlist.Enabled).

Field Meaning
playerSource, playerId, playerName The player
action 'added' or 'removed'
wishlistId Entry ID
item, maxPrice, notifyOnce What they are looking for

When: a player reports a listing (Config.Reports.Enabled). After AutoHideThreshold (5) open reports the listing is hidden until an admin reviews it.

Field Meaning
reportId Report ID
reporterSource, reporterId, reporterName Who reported
sellerId, sellerName Reported seller
item, itemLabel, listingId The listing
reason One of Config.Reports.Reasons
description Player’s text

How to test the social hooks: from the marketplace, favourite a listing, follow a seller, add an item to your wishlist and report a listing; each action prints its hook.

When: a player creates their marketplace shop (the first time they open it).

Field Meaning
playerSource, playerId, playerName The player
shopName, bio, avatarColor Their profile
lang Chosen language

How to test: with a character that has never opened the marketplace, press F6 and create the shop. To repeat it, delete that player’s row from marketplace_users (only on a test server).

When: a player changes their profile or storefront.

Field Meaning
playerSource, playerId, playerName The player
shopName Shop name
changedFields Which fields changed
profile The profile after the change

How to test: edit your shop name or bio from your profile.

When: a player collects something from their marketplace mailbox: an item (from an auction, a return, a pickup…) or money (from a sale, a refund…).

Field Meaning
playerSource, playerId, playerName The player
mailboxId Mailbox entry
claimType 'item' or 'money'
amount Money collected
item, itemLabel, itemMetadata, quantity Item collected
meetupPoint Meetup point, if it had to be picked up there
reason Why it was in the mailbox

How to test: win a test auction (/markettest listing mailbox auction bread 100, bid and wait 5 minutes): the item always goes to your mailbox. Open the mailbox and collect it (claimType = 'item'). Selling something to another player leaves the money in your mailbox, which tests claimType = 'money'.

When: an admin does something from /marketadmin.

action Meaning Extra fields
'force_cancel' Cancelled a listing listingId
'ban_user' Banned a player from the marketplace targetId
'unban_user' Lifted a ban targetId
'resolve_report' Resolved a report reportId
'force_refund' Forced a refund refundId, listingId
Field Meaning
adminSource, adminId, adminName The admin
action See above
targetId Affected player
details Extra data (for a forced refund: amount and sellerId)

Example: audit log to Discord

Handlers.OnAdminAction = function(data)
PerformHttpRequest(GetConvar('my_admin_webhook', ''), function() end, 'POST',
json.encode({ content = ('**%s** did `%s` on %s'):format(data.adminName, data.action, tostring(data.targetId or data.listingId)) }),
{ ['Content-Type'] = 'application/json' })
end

How to test: give yourself admin (add_ace group.admin marketplace.admin allow), open /marketadmin and cancel a listing, ban and unban a test player, resolve a report and force a refund.

Two older callbacks live in config.lua instead of handlers.lua. They still work and are called together with the hooks above.

When: every time the marketplace shows a notification to a player from the server. Replace its body to use your own notification system.

Config.Notify = function(src, msg, type)
TriggerClientEvent('ox_lib:notify', src, { description = msg, type = type or 'inform' })
end

How to test: do anything that shows a notification (buy something, fail a purchase for lack of money) and check it appears with your system.

When: exactly when OnDeliveryRobbed fires, just before it, once per delivery.

Config.OnDeliveryRobbed = function(thiefSrc, deliveryId, value, trigger)
-- trigger: 'driver_killed' | 'cargo_stolen'
TriggerEvent('dispatch:robbery', thiefSrc, GetEntityCoords(GetPlayerPed(thiefSrc)))
end

Use one of the two, not both, or you will send two alerts per robbery. Handlers.OnDeliveryRobbed is recommended: it receives more data and shows up in /markethandlers.