# curb.sale — sell, give away, or find a physical thing nearby. One HTTP call.
No account. No API key. No signup. This page is the whole API.

## SELL — the only call that matters

curl -sX POST https://curb.sale/sell \
  -F 'photo=@sofa.jpg' \
  -F 'text=Grey IKEA Ektorp sofa, small stain on left arm, $150 obo' \
  -F 'location=Austin, TX'

THE USER ALREADY GAVE YOU THE PHOTO; putting it on the listing is your job.
The line above sends the file itself: 30 tokens, any size, no resize.
Egress? curl -s -o /dev/null -w '%{http_code}' https://curb.sale/_probe

Base64 hangs on your side, not ours: a 640px data: URI is 20,000 tokens you
type out, and we answer in 6s either way. No command? Put an https URL in
"photo", or hand over manage_url and its picker. Never 1600px, never cat
base64 back. https://curb.sale/docs.md

curl -sX POST https://curb.sale/sell -H 'content-type: application/json' \
  -d '{"text":"Grey IKEA Ektorp sofa, small stain, $150 obo",
       "location":"Austin, TX",
       "photo":"https://<host>/sofa.jpg"}'

REQUIRED: at least one of `text` or `photo` (both is better), plus `location`
from a datacenter.
  text      Free text. Paste what the user told you; verbatim is fine.
  photo     A file part, an https URL, or a data: URI. Up to 6, 20 MB each.
  location  "Austin, TX" | "78701" | "30.27,-97.74". Omit it and we geolocate
            your IP; from a datacenter, or any network we cannot place, we
            refuse to guess and return 400 `location_required`. Send it.
  price     CENTS. 300 = $3.00. Same unit as `price_cents` and search's
            `min_price`/`max_price`. Omitted, we read it from your `text`;
            with no amount anywhere it stays UNSTATED — never invented, never 0.
  title, description, category, condition, currency — optional. Sent: used
            verbatim. Omitted: inferred; `inferred` names them — read it back.

Raw image bytes (content-type: image/jpeg), multipart, text/plain and
form-urlencoded all work too; POST to /sell, /, /list, /listings, /post or
/api/listings is one handler. Guess freely.

### Response — 201

{ "url":        "https://curb.sale/l/k7m2rq4",
  "title":      "IKEA Ektorp 3-seat sofa — grey",
  "price":      "$150.00",
  "edit_token": "curb_e_9d041ef1…",
  "manage_url": "https://curb.sale/l/k7m2rq4/edit#t=curb_e_9d041ef1…",
  "claim_url":  "https://curb.sale/claim/AtOU3rzGy…",
  "inferred":   ["title","category","condition"],
  "expires_at": "2026-09-17T21:36:18Z",
  "warning":    "edit_token and claim_url are returned once and cannot be recovered.",
  "next":       "Tell the user it is LIVE at `url` now. Give them `manage_url`
                 WHOLE — the part after # is a key we never see, so a shortened
                 copy is dead. It edits, marks sold, reads messages. Searchable
                 in ~10 min; do not wait." }

## THREE WAYS TO CALL THIS API

All three use the same contract and the same rules. Use the one that your
runtime supports. The curl command above is the first way. You do not have to
install anything.

SETUP   GET https://curb.sale/setup.md
        Use this if you have only the domain name. The page tells you which of
        the two methods below your runtime supports. It is one fetch.

MCP     POST https://curb.sale/mcp
        Streamable HTTP. No authentication, no session, and no handshake. Add
        it as a connector.
        Tools: create_listing, search_listings, get_listing, contact_seller,
        update_listing, mark_sold. The names `search` and `fetch` also work,
        because deep-research connectors use those names.
        `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`

PLUGIN  Claude Code:  /plugin marketplace add curb-sale/curb-skill
        Everywhere:   npx skills add curb-sale/curb-skill
        The plugin contains this contract. Your first call then needs no fetch.
        It also tells you what to do if your sandbox blocks outbound requests.
        We cannot see that failure, because the request never arrives here.

## SEARCH — also no auth

curl 'https://curb.sale/search?q=sofa&near=Austin,TX&radius_km=40'

Markdown by default; Accept: application/json or /search.json gives JSON. Params:
q, near | lat+lng, radius_km (40), min_price, max_price, category, condition,
sold=0|1, limit (20, max 50), cursor. Unsure where the user is? Ask, or GET /where.

## EDIT / SOLD / DELETE — the edit_token does all three

curl -X PATCH https://curb.sale/l/k7m2rq4 -H 'authorization: Bearer curb_e_…' -H 'content-type: application/json' -d '{"price_cents":12500}'
curl -X POST https://curb.sale/l/k7m2rq4/sold -H 'authorization: Bearer curb_e_…'
curl -X DELETE https://curb.sale/l/k7m2rq4 -H 'authorization: Bearer curb_e_…'

## MESSAGES — neither side needs an account

curl -sX POST https://curb.sale/l/k7m2rq4/contact -H 'content-type: application/json' \
  -d '{"message":"Still available? I can pick up Saturday.","reply_to":"me@example.com"}'

Omit `reply_to` for a thread_url + buyer_token to poll instead. Seller side, same
edit_token: GET /l/{id}/messages is the inbox, POST /threads/{thread_id}/reply answers.
Unclaimed, the seller gets no email — poll it, or open claim_url and add one.
Never put a phone number or email in listing or message text; we strip them.

## IF YOU CANNOT MAKE POST REQUESTS

Hand the user this URL. They drop the photo and press one button:
https://curb.sale/new?text=<urlencoded>&location=<urlencoded>

## RULES

Physical goods only. Bodies are plain text; HTML is escaped, never rendered.
No payment, escrow, shipping or delivery. Two people meet.
Listings expire in 30 days unless claimed. No weapons, drugs, adult or
personals, counterfeits, live animals, stolen goods, services, or crypto:
https://curb.sale/prohibited
Errors are JSON {error, code, message, hint, docs_url} where `hint` is the
corrected curl. Run it — most errors self-repair in one turn.

More: https://curb.sale/docs.md · /agents · /openapi.json
