Developers

API, docs and MCP

One list of route definitions serves the public API, renders its reference docs, generates openapi.json and backs both MCP servers. Add a route once and all four pick it up.

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.

A route
// 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 apiRoutesWhere
The routercreateRouter in lib/server/api/router.ts, served by app/api/v1/[[...path]] (and api.resell.store/v1 via proxy.ts)
The reference pagesapp/docs/api/[group]/page.tsx, one page per group, with schemas, examples and curl samples
openapi.jsonopenapi() in index.ts: OpenAPI 3.1 from the same schemas (z.toJSONSchema), served at /v1/openapi.json
The MCP serversTheir 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

  1. The path after /v1 is matched against every route. A path that exists with another method gets a hint saying which.
  2. A bearer token (or x-api-key) is checked if sent, even on public routes. On access: "key" routes it's required, and its scopes must include the route's scope (GETs default to read).
  3. The request is counted against the month's limit, with x-ratelimit-limit and x-ratelimit-remaining headers.
  4. The query and body are parsed with the route's zod schemas. JSON, form fields and multipart all work.
  5. The handler runs. Writes revalidate the app so a change through the API shows there straight away.
  6. 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-id to quote.

Adding an endpoint

  1. Add a route() to the right file in lib/server/api/routes/ (or a new file, spread into apiRoutes). Put the logic in lib/server/* and keep the handler thin, so the app's server actions can share it.
  2. Pick access and scope: read, shops, listings, messages, offers, orders, buying or webhooks.
  3. Write the query and body schemas with a .describe() on every field. Those descriptions are the docs.
  4. Add an example with a realistic response. Use the docs' people: Maya's shop, Jess buying, the yellow dutch oven. Use a group that matches one in apiGroups (lib/docs/nav.ts), or it won't have a page.
  5. For a write, add a line to describers in lib/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.
  6. If an AI app should be able to do it, add a tool to packages/mcp/src/seller.ts or buyer.ts with the same scope. Mark it consequential if it moves money, answers an offer or puts something live.
  7. If it's something a seller would want to hear about, emit a webhook (emitOrderEvent, emitOfferEvent and friends in lib/server/api/webhooks.ts) from the shared function, so the app and the API both fire it.
  8. Add a test next to it. lib/server/api/routes.test.ts and router.test.ts show 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.

KindLooks likeMade onScopes
apirs_live_…/tools/apiEverything
agentmaya-…/tools/agentChosen 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 a whsec_ 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>, where v1 is 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

HostedLocal (stdio)
Whereapp/api/mcp/[[...path]]/route.tspackages/mcp/src/stdio.ts
Addresses/u/{token}, /seller (bearer), /buy, /buy/{token}, /buyerresell-mcp or resell-mcp buyer
Calls the APIIn-process: the same router, auth and limits, without a network hopOver HTTP to RESELL_API_URL
ServerA fresh one per request; who a token is gets cached for 15 secondsOne per process; a bad key fails at start
Terminal
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 server

Scopes 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

The docs site itself is app/docs, served on docs.resell.store by proxy.ts or at /docs on the marketplace. The sidebar is lib/docs/nav.ts.