Developers
API, docs and MCP
One definition, four uses
Every endpoint is one route() call in lib/server/api/routes/*.ts: method, path, docs group, who may call it, zod schemas for the query and body, an example, and the handler. lib/server/api/index.ts collects them into apiRoutes, and everything else reads that list.
// lib/server/api/routes/stats.ts
export const statsRoutes = [
route({
method: "GET",
path: "/stats",
group: "Stats", // the reference page it shows on
access: "key", // "public" or "key"
// scope: GETs default to "read"; writes name theirs, e.g. "listings"
summary: "How your shops are doing",
description: "Earnings, views, likes, shares, followers, offers and sales…",
query: z.object({
shop: z.string().optional().describe("A shop slug. Leave out for every shop."),
period: z.enum(["7d", "30d", "90d", "year"]).default("30d"),
}),
example: { query: "period=7d", response: { object: "stats", period: "7d", earned: 214, … } },
handler: async ({ auth, query }) => {
const all = await sellerStats(auth!.user.id, query.shop ?? null);
return { object: "stats", period: query.period, … };
},
}),
];| Reads apiRoutes | Where |
|---|---|
| The router | createRouter in lib/server/api/router.ts, served by app/api/v1/[[...path]] (and api.resell.store/v1 via proxy.ts) |
| The reference pages | app/docs/api/[group]/page.tsx, one page per group, with schemas, examples and curl samples |
| openapi.json | openapi() in index.ts: OpenAPI 3.1 from the same schemas (z.toJSONSchema), served at /v1/openapi.json |
| The MCP servers | Their tools call these routes through ResellClient; hosted, in-process |
Because the docs render from the definitions, they can't drift from what the API does.
What happens on a request
- The path after
/v1is matched against every route. A path that exists with another method gets a hint saying which. - A bearer token (or
x-api-key) is checked if sent, even on public routes. Onaccess: "key"routes it's required, and its scopes must include the route'sscope(GETs default toread). - The request is counted against the month's limit, with
x-ratelimit-limitandx-ratelimit-remainingheaders. - The query and body are parsed with the route's zod schemas. JSON, form fields and multipart all work.
- The handler runs. Writes revalidate the app so a change through the API shows there straight away.
- After the response, the change is written to the owner's activity log. Errors use one envelope with a type and a plain message, plus an
x-request-idto quote.
Adding an endpoint
- Add a
route()to the right file inlib/server/api/routes/(or a new file, spread intoapiRoutes). Put the logic inlib/server/*and keep the handler thin, so the app's server actions can share it. - Pick
accessandscope:read,shops,listings,messages,offers,orders,buyingorwebhooks. - Write the
queryandbodyschemas with a.describe()on every field. Those descriptions are the docs. - Add an
examplewith a realistic response. Use the docs' people: Maya's shop, Jess buying, the yellow dutch oven. Use agroupthat matches one inapiGroups(lib/docs/nav.ts), or it won't have a page. - For a write, add a line to
describersinlib/server/api/log.ts, keyed"POST /your/:path", so the activity log says what happened in words. Without one it falls back to the summary. - If an AI app should be able to do it, add a tool to
packages/mcp/src/seller.tsorbuyer.tswith the samescope. Mark itconsequentialif it moves money, answers an offer or puts something live. - If it's something a seller would want to hear about, emit a webhook (
emitOrderEvent,emitOfferEventand friends inlib/server/api/webhooks.ts) from the shared function, so the app and the API both fire it. - Add a test next to it.
lib/server/api/routes.test.tsandrouter.test.tsshow the pattern.
Keys and limits
Keys live in api_key (lib/server/api/keys.ts). A token is shown once, when it's made; we keep its SHA-256 hash, the readable start and the last four characters. Each person has at most one live key of each kind, and making a new one revokes the old one at once.
| Kind | Looks like | Made on | Scopes |
|---|---|---|---|
api | rs_live_… | /tools/api | Everything |
agent | maya-… | /tools/agent | Chosen by the owner; never shops or webhooks. Starts with read, listings, messages, offers, orders and buying, with offers and buying set to ask first, and a $200 ceiling per offer. |
Usage is counted per person per month in api_usage: 10,000 requests while we're in beta (MONTHLY_LIMIT), shared by scripts and MCP. Over that, requests get rate_limited until the 1st.
The activity log
Every change made with a key gets one line in api_activity, written by recordActivity in lib/server/api/log.ts; reads aren't kept. Refused calls are logged too ("Tried to …") so the owner sees what an app wasn't allowed to do. The app is named from the MCP server's resell-client header or the user agent (Claude, ChatGPT, Cursor, "A script"…). Lines are kept for 90 days; the webhook sweep clears older ones.
Webhooks
- One endpoint per person (
webhook_endpoint), with awhsec_secret. Events:listing.sold,offer.received,offer.updated,question.asked,order.completed,order.problem,order.refunded,payout.sent,review.created. - Each POST carries
Resell-Signature: t=<unix seconds>,v1=<hex>, wherev1is HMAC-SHA256 of{t}.{body}with the secret. 10 second timeout. Delivery never blocks or fails what caused it. - Anything but a 2xx is retried by the webhook sweep about 5 minutes, 30 minutes, 2 hours, 6 hours and a day later, six tries in all, with the same event id. An endpoint failing for three days straight is turned off until it's saved again.
The MCP package
packages/mcp (@repo/mcp) builds two servers on @modelcontextprotocol/server: a seller one (38 tools in src/seller.ts) and a buyer one (31 in src/buyer.ts). Tools never touch the database. They call the public API through ResellClient (src/client.ts), so an AI app can do exactly what a script with the same key can.
Hosted and local
| Hosted | Local (stdio) | |
|---|---|---|
| Where | app/api/mcp/[[...path]]/route.ts | packages/mcp/src/stdio.ts |
| Addresses | /u/{token}, /seller (bearer), /buy, /buy/{token}, /buyer | resell-mcp or resell-mcp buyer |
| Calls the API | In-process: the same router, auth and limits, without a network hop | Over HTTP to RESELL_API_URL |
| Server | A fresh one per request; who a token is gets cached for 15 seconds | One per process; a bad key fails at start |
pnpm --filter @repo/mcp build
RESELL_API_KEY=rs_live_… node packages/mcp/dist/stdio.js # your shops
RESELL_API_KEY=rs_live_… node packages/mcp/dist/stdio.js buyer # shopping (key optional)
RESELL_API_URL=http://localhost:5689/api/v1 … # point it at your dev serverScopes and asking first
createResellServer looks up the key with GET /v1/me and registers only the tools its scopes allow (allowedTools in src/tools.ts), so an app never sees a tool it can't use. For a consequential tool whose scope the owner set to "Ask me first", the server asks before calling the API: with an elicitation if the app can show a yes/no question, otherwise by returning a message telling the model to ask and call again with confirmed: true. A buyer key's offer ceiling is added to the server's instructions.
Public docs
app/docs, served on docs.resell.store by proxy.ts or at /docs on the marketplace. The sidebar is lib/docs/nav.ts.