API reference
The ten operations of Marka's public v1 HTTP API, generated from the OpenAPI contract.
Base URL: https://joinmarka.com/api/v1
Every page in this section is generated from apps/docs/openapi/openapi.json, which is itself
generated from the route contract in @marka/shared. Nothing here is hand-maintained, so the
reference cannot drift from what the handlers implement.
The surface
| Method | Endpoint | Operation |
|---|---|---|
GET | /v1/categories | List active listing categories |
GET | /v1/listings | List your listings |
POST | /v1/listings | Create a draft listing |
GET | /v1/listings/{id} | Get a listing by id |
PATCH | /v1/listings/{id} | Update listing fields |
POST | /v1/listings/{id}/publish | Publish a draft listing |
GET | /v1/me | Get the authenticated account and its plan |
GET | /v1/orders | List orders on your listings |
GET | /v1/orders/{id} | Get an order by id |
GET | /v1/search | Search the 200 most recently listed active listings |
Conventions
Money is integer minor units
Every amount — price, shippingFlatAmount, autoAcceptOfferAmount, the price breakdown on an
order — is an integer in the minor unit of its accompanying currency. 4500 with "currency": "CAD" is CA$45.00. The API converts nothing, and neither does the SDK.
Writes are gated, reads are not
createListing, updateListing and publishListing require the api_write entitlement when
called with an API key, and answer 402 with code: "entitlement_required" without it. Reads are
available to any valid key. See Plans & limits.
Lifecycle is not a field
PATCH /v1/listings/{id} updates fields. It cannot change a listing's status: the body is strict,
so {"status": "active"} is a 400 that points you at the publish endpoint rather than a 200
that quietly did nothing. Publishing runs the domain's activation guards — a listing needs at least
one fulfilment option (pickup or shipping) and at least one order rail (in-person handoff or card
checkout), and a shipping-enabled listing needs a shippingFlatAmount above zero.
Search has a horizon
GET /v1/search ranks the 200 most recently listed active listings. That ceiling bounds the
candidate set, not the page size — an offset at or past it returns an empty page rather than
reaching older items, and no query reaches an item outside it. The same read backs search on the
Marka website, so this is not an API-only restriction.
A city that matches nothing in the catalogue returns no results, rather than dropping the location
filter and answering with the whole country.
Orders are read-only, and contain personal data
There is no write path for orders on this API. Order fulfilment happens in the Marka app.
checkoutShippingDetails is PII
On the card-checkout rail, an order's checkoutShippingDetails carries the name, email, phone
and postal address the buyer supplied at checkout. It is exposed deliberately — a seller cannot
buy a shipping label without it — but it is personal data. Handle it accordingly, and do not log
it. It is null on the in-person pickup rail, where no such details are collected.
The buyer is otherwise identified only by an opaque buyerId; there are no top-level buyer name or
email fields.
Pagination
List endpoints take limit and offset, and return the records alongside the limit and offset
that were applied plus a hasMore boolean. Ordering is newest-first for listings and orders, and
relevance-ranked for search.
A limit outside the allowed range is a 400, not a silent clamp: a published contract that
quietly returns a different page size than the one asked for is a contract that lies to a generated
client.
GET /v1/categories is the exception — it has no pages at all. Categories are the distinct
categoryPath values across active listings, grouped and counted, ordered by count descending. Its
limit truncates the tail of that single list; a lower limit never surfaces different
categories.
Errors
One envelope, { error, code? }, for every non-2xx response. See
Authentication → Errors for the status and code tables.