NFT Marketplace Integration
Keep your marketplace listings automatically in sync with on-chain state by subscribing to NFT lifecycle events via XRNotify webhooks.
What you'll build
By the end of this guide, your marketplace backend will automatically respond to every stage of an NFT's on-chain lifecycle:
- →Mint — index new NFTs as soon as they appear on-chain
- →Offer created — mark NFTs as "listed for sale" or record incoming bids
- →Offer accepted — transfer ownership and record the sale
- →Offer cancelled — remove stale listings and bids
- →Burn — delist and archive burned tokens
Subscribe to NFT events
Create a single webhook that covers all NFT event types. Because this webhook has no account_filters, it will fire for any NFT activity on the network — suitable for a public marketplace. You can add filters later to narrow scope.
curl -X POST https://api.xrnotify.io/v1/webhooks \
-H "X-XRNotify-Key: xrn_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourmarket.com/webhooks/nft",
"event_types": [
"nft.minted",
"nft.burned",
"nft.offer_created",
"nft.offer_accepted",
"nft.offer_cancelled"
]
}'Central event dispatcher
A single route handles all NFT events and dispatches to the appropriate handler via a switch on event.event_type.
app.post('/webhooks/nft', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['x-xrnotify-signature'];
if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Idempotency guard
const deliveryId = req.headers['x-xrnotify-delivery-id'];
if (await cache.get(`delivery:${deliveryId}`)) return res.sendStatus(200);
switch (event.event_type) {
case 'nft.minted': await handleMinted(event); break;
case 'nft.offer_created': await handleOfferCreated(event); break;
case 'nft.offer_accepted': await handleOfferAccepted(event); break;
case 'nft.offer_cancelled':await handleOfferCancelled(event); break;
case 'nft.burned': await handleBurned(event); break;
}
await cache.set(`delivery:${deliveryId}`, 'done', 86400);
res.sendStatus(200);
});Handling nft.minted
When an NFT is minted, add it to your index. The issuer is also the initial owner. Decode the uri_decoded field (already hex-decoded by XRNotify) to get the metadata URL.
async function handleMinted(event) {
const { nft_id, issuer, uri_decoded, flags, taxon } = event.payload;
await db.nfts.create({
id: nft_id,
issuer,
owner: issuer, // Owner is issuer on mint
metadata_url: uri_decoded,
taxon,
is_transferable: !(flags & 8), // tfBurnable flag check
status: 'unlisted',
created_at: event.timestamp,
});
}Handling nft.offer_created
The payload distinguishes between sell offers (listings) and buy offers (bids) via the is_sell_offer boolean.
async function handleOfferCreated(event) {
const { nft_id, owner, amount, is_sell_offer, offer_id } = event.payload;
if (is_sell_offer) {
// Mark the NFT as listed with price and the offer ID
await db.nfts.update(nft_id, {
status: 'listed',
list_price: amount,
list_offer_id: offer_id,
});
} else {
// Record the incoming bid
await db.bids.create({
nft_id,
bidder: owner,
amount,
offer_id,
created_at: event.timestamp,
});
}
}Handling nft.offer_accepted
A sale is final. Transfer ownership in your database, clear the listing status, and record the sale for provenance.
async function handleOfferAccepted(event) {
const { nft_id, buyer, seller, price } = event.payload;
await db.nfts.update(nft_id, {
owner: buyer,
status: 'unlisted',
list_price: null,
list_offer_id: null,
last_sale: price,
});
await db.sales.create({
nft_id,
buyer,
seller,
price,
timestamp: event.timestamp,
});
// Remove any outstanding bids now that the NFT has sold
await db.bids.deleteWhere({ nft_id });
}Handling nft.offer_cancelled
async function handleOfferCancelled(event) {
const { nft_id, offer_id, is_sell_offer } = event.payload;
if (is_sell_offer) {
// Only unlist if this is the active listing offer
const nft = await db.nfts.findById(nft_id);
if (nft?.list_offer_id === offer_id) {
await db.nfts.update(nft_id, {
status: 'unlisted',
list_price: null,
list_offer_id: null,
});
}
} else {
await db.bids.deleteWhere({ offer_id });
}
}Handling nft.burned
A burned NFT is permanently destroyed. Remove it from active listings and archive any associated bids.
async function handleBurned(event) {
const { nft_id } = event.payload;
await db.nfts.update(nft_id, {
status: 'burned',
list_price: null,
list_offer_id: null,
burned_at: event.timestamp,
});
// Remove all bids — there is nothing left to bid on
await db.bids.deleteWhere({ nft_id });
}Real-time UI updates
Once your webhook handler updates the database, you can push the change to any connected browser clients so listings update without a page refresh. After calling the relevant db.* method, emit an event on a shared in-process emitter (or a Redis pub/sub channel if you run multiple instances):
// After updating the DB inside handleOfferAccepted:
nftEventEmitter.emit('nft-update', {
type: 'sold',
nft_id,
buyer,
price,
});
// SSE endpoint your frontend connects to:
app.get('/api/nft-stream/:nftId', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
const listener = (update) => {
if (update.nft_id === req.params.nftId) {
res.write(`data: ${JSON.stringify(update)}\n\n`);
}
};
nftEventEmitter.on('nft-update', listener);
req.on('close', () => nftEventEmitter.off('nft-update', listener));
});