Section 4: Bytes Go Around, Not Through - API Design (with pushback)
Executive Summary: The contract has four jobs: get a listing and its photos in, find listings near a buyer, let the seller change or close a listing, and let two strangers talk without swapping phone numbers. We walk Maya's posting calls and Daniel's search URL, then defend four choices under pushback: photos bypass the API, exact coordinates go in but only fuzzed ones come out, search uses cursors instead of page numbers, and a new listing returns pending_review, not active.
"Show me the API." Write the whole surface first, one line per endpoint, before arguing any of it.
| Job | Endpoint |
|---|---|
| Get an upload slot | POST /v1/uploads |
| Post a listing | POST /v1/listings |
| Read a listing | GET /v1/listings/{id} |
| Edit, drop price, mark sold | PATCH /v1/listings/{id} |
| Renew, remove | POST /v1/listings/{id}/renew, DELETE /v1/listings/{id} |
| Search near me | GET /v1/search |
| Message a seller | POST /v1/conversations, POST /v1/conversations/{id}/messages, GET /v1/conversations |
| Report | POST /v1/reports |
"Everything is JSON under /v1, behind a bearer token. No request body anywhere carries a sellerId or buyerId. Who you are comes from the verified token and nowhere else, so 'edit someone else's listing' is not a request a client can even write."
Posting: Two Steps, and the Bytes Skip the API
Maya is in Mueller at 7:40 pm on a Sunday, inside the weekly posting peak, with six photos of a Trek road bike. The app first asks for upload slots, one per photo:
POST /v1/uploads
{ "contentType": "image/jpeg", "bytes": 1520000 }
201 Created
{ "uploadId": "up_8c1f…", "uploadUrl": "https://uploads…/raw/…?sig=…",
"expiresInSec": 900, "maxBytes": 5000000 }
The uploadUrl is presigned: it permits PUTs to exactly one object key in a private quarantine bucket for 15 minutes (reusable until expiry, so retries work), with the content type and the exact declared Content-Length signed in (the maxBytes here is an illustrative cap). A presigned POST policy with a content-length-range is the alternative when size isn't known. The app has already resized each photo to 2,048 px on the long edge, so each PUT is about 1.5 MB, and it goes straight to object storage. Then the listing itself, which is a few kilobytes:
POST /v1/listings
Idempotency-Key: 3b9d0e44-71a2-4c5e-9f08-2d6a1c7e90b3
{ "title": "Trek road bike, 56 cm", "description": "…",
"priceCents": 45000, "categoryId": 412, "condition": "good",
"location": { "lat": 30.2985, "lng": -97.7044 },
"photoUploadIds": ["up_8c1f…", "…5 more"] }
202 Accepted
{ "listingId": "1849302217733120", "status": "pending_review", "version": 1 }
The Idempotency-Key matters on a phone. If the connection drops after the server commits but before the response arrives, the retry with the same key returns the same listingId. Without it, a flaky network posts the bike twice, and identical listings are exactly what Section 9's trust-and-safety pipeline flags as spam.
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson