API
Recipes
Before you start
Every recipe below uses a secret key from resell.store/tools/api in a RESELL_KEY variable. Replace $LISTING and $SALE with ids from earlier answers. Amounts are dollars.
export RESELL_KEY=rs_live_...List something from a few words
The same agent that helps in the app does the work: it reads your words and picture, finds the item in the catalog, checks what similar ones are listed for, and sets a price and a lowest price. You only have to publish.
# 1. Make a draft and start the research agent on it
curl https://api.resell.store/v1/listings \
-H "Authorization: Bearer $RESELL_KEY" \
-d shop=maya \
-d prompt="yellow le creuset dutch oven, 5.5 qt, used twice" \
-d "photo_urls[]=https://example.com/dutch-oven.jpg" \
-d research=true
# 2. Check on it every few seconds until "status" is "done" (about a minute)
curl https://api.resell.store/v1/listings/$LISTING/research \
-H "Authorization: Bearer $RESELL_KEY"
# 3. Have the writing agent write the title and description
curl https://api.resell.store/v1/listings/$LISTING/words \
-H "Authorization: Bearer $RESELL_KEY" \
-d tone="Straight to the point"
# 4. Put it live
curl -X POST https://api.resell.store/v1/listings/$LISTING/publish \
-H "Authorization: Bearer $RESELL_KEY"Research sets price and lowest_price from what it found, unless you sent your own. Change anything with PATCH /listings/:id before you publish. See Listings and Research.
Already know the price?
title, price and publish=true in the first request and it goes live straight away.What's it worth?
POST /research is the shortcut for pricing something you haven't decided to sell. It makes a draft to hold the findings, so delete it afterwards if you don't need it.
# Look something up without listing it
curl https://api.resell.store/v1/research \
-H "Authorization: Bearer $RESELL_KEY" \
-d query="Patagonia Better Sweater, women's M, navy"
# Poll with the listing id from the answer, then read findings.price
curl https://api.resell.store/v1/listings/$LISTING/research \
-H "Authorization: Bearer $RESELL_KEY"
# Not selling it after all? Delete the draft
curl -X DELETE https://api.resell.store/v1/listings/$LISTING \
-H "Authorization: Bearer $RESELL_KEY"Answer offers with your own rules
Your shop agent already counters lowball offers in the app. If you'd like different rules, turn off "Haggle on offers" in your shop settings and run your own. This one accepts anything at or above your lowest price, declines anything far below it, and meets everyone else halfway.
// answer-offers.mjs: run it whenever you like, or from a webhook
const api = "https://api.resell.store/v1";
const headers = { Authorization: `Bearer ${process.env.RESELL_KEY}`, "Content-Type": "application/json" };
const get = (path) => fetch(api + path, { headers }).then((r) => r.json());
const post = (path, body = {}) => fetch(api + path, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => r.json());
const { data: offers } = await get("/offers?status=open&limit=100");
for (const offer of offers) {
const listing = await get(`/listings/${offer.listing.id}`);
const lowest = listing.lowest_price ?? listing.price;
if (offer.amount >= lowest) {
await post(`/offers/${offer.id}/accept`);
console.log(`Accepted $${offer.amount} from ${offer.buyer.name} on ${offer.listing.title}`);
} else if (offer.amount < lowest * 0.6) {
await post(`/offers/${offer.id}/decline`);
} else {
// Meet them halfway, never under your lowest
const amount = Math.max(lowest, Math.round((offer.amount + listing.price) / 2));
if (amount < listing.price) await post(`/offers/${offer.id}/counter`, { amount });
}
}A counter has to sit between their offer and your asking price, and the buyer then has 48 hours to take it. See Offers.
Ship what sold
Sales waiting for you have the status paid. Each one carries the buyer's ship_to address. Ship within 3 days of payment; after that the buyer may cancel.
# Everything waiting to be shipped
curl "https://api.resell.store/v1/sales?status=paid" \
-H "Authorization: Bearer $RESELL_KEY"
# Mark one shipped. The buyer gets an email with the tracking link
curl https://api.resell.store/v1/sales/$SALE/ship \
-H "Authorization: Bearer $RESELL_KEY" \
-d tracking_number=9400111899223197428490Export your sales
Every sale includes what the buyer paid and what you get once the money's released (payout.seller_net, after the resell.store and PayPal fees). This writes them all to a spreadsheet.
# Needs jq. Pages through every sale, 100 at a time
offset=0
echo "sold_at,item,shipping,total,you_get,status,title" > sales.csv
while :; do
page=$(curl -s "https://api.resell.store/v1/sales?limit=100&offset=$offset" -H "Authorization: Bearer $RESELL_KEY")
echo "$page" | jq -r '.data[] | [.paid_at, .item, .shipping, .total, .payout.seller_net, .status, .listing.title] | @csv' >> sales.csv
[ "$(echo "$page" | jq .has_more)" = "true" ] || break
offset=$((offset + 100))
doneReact when something happens
- Run a small server that checks the signature and handles the events you care about:
// server.mjs: node server.mjs, then set the address with PUT /webhooks
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const secret = process.env.RESELL_WEBHOOK_SECRET; // whsec_…, shown once when you set the webhook
const seen = new Set();
function genuine(rawBody, header = "") {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && expected.length === (parts.v1 ?? "").length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
createServer(async (req, res) => {
let raw = "";
for await (const chunk of req) raw += chunk;
if (!genuine(raw, req.headers["resell-signature"])) return res.writeHead(401).end();
const event = JSON.parse(raw);
res.writeHead(200).end(); // answer fast, then do the work
if (seen.has(event.id)) return; // retries reuse the same id
seen.add(event.id);
if (event.type === "listing.sold") {
const order = event.data.object;
console.log(`Sold: ${order.listing.title} for $${order.total}. Ship to ${order.ship_to.name}.`);
}
if (event.type === "offer.received") {
const offer = event.data.object;
console.log(`${offer.buyer.name} offered $${offer.amount} on ${offer.listing.title}`);
}
}).listen(3000);- Tell resell.store where it is. The first answer includes the signing
secret, once:
curl -X PUT https://api.resell.store/v1/webhooks \
-H "Authorization: Bearer $RESELL_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/resell", "events": ["listing.sold", "offer.received"] }'
# Send a test event to check it's wired up
curl -X POST https://api.resell.store/v1/webhooks/test \
-H "Authorization: Bearer $RESELL_KEY"Answer with a 2xx within 10 seconds. Anything else is retried, with the same event id, for about a day. See Webhooks for every event and what it carries.
Shop from a script
Marketplace search needs no key at all. Offers and checkout links need a key with the buying permission. Paying always happens in a browser: POST /checkout hands you the page, and nothing is charged until someone pays there.
# No key needed: search by meaning, not just words
curl "https://api.resell.store/v1/market/search?q=something%20to%20cook%20a%20stew%20in&price=25-100&offers=true"
# Then, as a buyer with a key: make an offer…
curl https://api.resell.store/v1/offers \
-H "Authorization: Bearer $RESELL_KEY" \
-d listing=$LISTING \
-d amount=60 \
-d note="Could you post it this week?"
# …and once it's accepted, get the page to pay on
curl https://api.resell.store/v1/checkout \
-H "Authorization: Bearer $RESELL_KEY" \
-d listing=$LISTINGSee Marketplace and Buying, or let an AI app do this for you with the shopping MCP server.