# mongrl: the complete docs > mongrl is a headless marketplace API. You build the storefront (any stack); mongrl runs sellers, listings, search, checkout with Stripe, orders, payouts, shipping and messages behind it. Each marketplace is run by its team in the mongrl console. Rules for building on mongrl: - Call the API from your storefront's server, never from the browser: it holds the secret key (`sk_…`) and each user's session token (`X-User-Session`). Browsers can't call the API across origins. - Generate a typed client from the OpenAPI spec (https://dev.mongrl.com/openapi.json) instead of writing requests by hand. Never guess an endpoint or field. - Money is an integer in minor units with an ISO 4217 currency (`{"amount": 125000, "currency": "SEK"}` is 1 250,00 kr). - Errors are RFC 9457 problems (`application/problem+json`): show `detail` to people and map `errors[].location` onto form fields. - Send an `Idempotency-Key` header on writes you might retry, above all placing an order. - Orders move by named transitions: offer exactly the ones in `available_transitions`. - The thing on sale is a "listing". Emailed links land on pages the storefront must build (see "Pages your shop needs"). # What mongrl is > The pieces of a mongrl marketplace and who does what, before you write any code. mongrl is a headless marketplace API. You build the storefront people see, in any stack you like. mongrl runs everything behind it: accounts, listings, search, checkout with Stripe, orders, payouts, shipping and messages. The marketplace's team runs the day-to-day in the **console**, mongrl's back office. ## The pieces | Thing | What it is | |---|---| | **Marketplace** | One shop, with its own currencies, languages, commission, shipping table and rules. Every key, user and listing belongs to exactly one marketplace. | | **Listing** | The thing on sale: title, description, photos, price, catalogue terms (brand, designer…) and the marketplace's own fields. Always "listing", never "item" or "product". | | **Seller** | A user who lists. Any user can sell; they set up payouts with Stripe before they're paid. | | **Buyer** | A user who buys. Same accounts as sellers. | | **Order** | One purchase of one listing. It moves through states (`pending_payment`, `purchased`, `delivered`, `received`…) by named transitions. | | **Operator** | Someone on the marketplace's team, working in the console: they value and approve listings, ship orders, answer messages and set the rules. Roles are `admin` and `viewer`. | | **Console** | The marketplace's back office at `console.mongrl.com`. It's built on the same API, with its own sign-in. You don't build it. | ## Who builds what - **You** build the storefront: pages, design, domain, your server. Call the API from your server with your secret key. - **mongrl** runs the API, the database, payments, emails, timers (expired payments, automatic receipt, payouts) and the console. - **The marketplace's team** sets things up in the console: currencies, commission, what a listing needs, shipping and delivery, showrooms and warehouses, API keys. ## What a storefront usually has 1. **Browse:** the shop, search and filters, listing pages, brand and designer pages. See [Browsing and search](https://dev.mongrl.com/guides/browsing.md). 2. **Accounts:** sign-up, sign-in by password or emailed link, email confirmation, password reset. See [Accounts and sign-in](https://dev.mongrl.com/guides/accounts.md). 3. **Checkout:** delivery, vouchers, the Stripe payment form, then the order page. See [Checkout and orders](https://dev.mongrl.com/guides/checkout.md). 4. **Selling:** the sell form, photos, valuations, the seller's listings and sales. See [Selling](https://dev.mongrl.com/guides/selling.md) and [Payouts](https://dev.mongrl.com/guides/payouts.md). 5. **Messages:** questions about a listing, order messages and support. See [Messages](https://dev.mongrl.com/guides/messages.md). 6. **The pages emails link to.** See [Pages your shop needs](https://dev.mongrl.com/guides/storefront-pages.md). ## How the API behaves - REST over HTTPS with JSON, described by one [OpenAPI 3.1 spec](https://dev.mongrl.com/openapi.json) that's generated from the API's code, so it's always current. - Money is an integer in minor units with its currency. Errors are [RFC 9457 problems](https://dev.mongrl.com/reference/conventions.md#errors). Writes accept an [Idempotency-Key](https://dev.mongrl.com/reference/conventions.md#idempotency). - Everything is versioned under `/v1`. Changes within `v1` only add things: new endpoints, new optional fields. Next: the [Quickstart](https://dev.mongrl.com/start/quickstart.md). --- # Quickstart > Get a key, generate a typed client from the spec and run your first search from your server. From nothing to listings on a page in four steps. The examples use TypeScript, but any language that can send HTTPS requests works. ## 1. Get a key Keys come from the marketplace's team. In the console, **API keys → New key**, they create: - a **secret key** (`sk_test_…` or `sk_live_…`) for your server, and - optionally a **publishable key** (`pk_…`) for native apps. A key is shown once. Keep the secret key out of your code and out of browsers: put it in an environment variable such as `MONGRL_SECRET_KEY`. | Environment | API | Keys | |---|---|---| | Test | `https://api.staging.mongrl.com` | `sk_test_…`, `pk_test_…` | | Live | `https://api.mongrl.com` | `sk_live_…`, `pk_live_…` | Check a key: ```sh curl https://api.staging.mongrl.com/v1/marketplace \ -H "Authorization: Bearer $MONGRL_SECRET_KEY" ``` The answer is your marketplace: its currencies, languages, what a listing needs and its shipping table. ## 2. Generate a typed client The [OpenAPI spec](https://dev.mongrl.com/openapi.json) describes every endpoint and field. Generate a client from it instead of writing requests by hand: the compiler then catches wrong paths and fields. ```sh npm install -D @hey-api/openapi-ts curl -o openapi.json https://dev.mongrl.com/openapi.json ``` ```ts // openapi-ts.config.ts export default defineConfig({ input: './openapi.json', output: { path: 'src/api/generated' }, plugins: ['@hey-api/client-fetch'], }) ``` ```sh npx openapi-ts ``` Each operation becomes a typed function named after its operation id: `listListings`, `getListing`, `placeOrder`… Commit the spec with your code and update it on purpose, so a new API version never changes your build by surprise. ## 3. Call it from your server Make the client on the server, per request, with your secret key and (when someone is signed in) their session. Never import it into code that runs in the browser. ```ts // src/server/mongrl.ts (server only) export function mongrl(session?: string) { return createClient({ baseUrl: process.env.MONGRL_API_URL, // https://api.staging.mongrl.com auth: () => process.env.MONGRL_SECRET_KEY, // sent as Authorization: Bearer sk_… headers: session ? { 'X-User-Session': session } : undefined, }) } ``` ```ts const { data, error } = await listListings({ client: mongrl(), query: { q: 'lounge chair', currency: 'EUR', limit: 24 }, }) if (error) throw new Error(error.detail) for (const l of data.items ?? []) { console.log(l.title, l.display_price?.amount, l.images?.[0]?.urls?.medium) } ``` `display_price` is the price in the currency you asked for. Amounts are minor units: `125000` with `SEK` is 1 250,00 kr. ## 4. Sign a user in Requests for a user (their orders, their listings, buying) also send the user's session token. Your server gets it when the user signs in and keeps it in an HttpOnly cookie: ```ts const { data } = await signIn({ client: mongrl(), body: { email, password } }) // Store data.session.token in an HttpOnly, Secure, SameSite=Lax cookie. const orders = await listMyOrders({ client: mongrl(tokenFromCookie) }) ``` [Authentication](https://dev.mongrl.com/guides/authentication.md) explains keys and sessions in full. ## Next - Building with Claude, Cursor or another agent? Start with [Build with an AI agent](https://dev.mongrl.com/start/agents.md). - Otherwise, read the guide for the part you're building: [Browsing](https://dev.mongrl.com/guides/browsing.md), [Accounts](https://dev.mongrl.com/guides/accounts.md), [Checkout](https://dev.mongrl.com/guides/checkout.md) or [Selling](https://dev.mongrl.com/guides/selling.md). --- # Build with an AI agent > Point Claude, Cursor or any coding agent at mongrl and get a storefront that runs against real types, not guesses. mongrl is built to be built on by coding agents. Everything an agent needs is machine-readable, in one place, and kept in sync with the API by the build. ## What to give your agent | Resource | Use it for | |---|---| | [`/llms.txt`](https://dev.mongrl.com/llms.txt) | The entry point: what mongrl is, the rules, and links to every page as Markdown. | | [`/llms-full.txt`](https://dev.mongrl.com/llms-full.txt) | Every guide and the full API reference in one file, for agents with a big context. | | [`/llms-small.txt`](https://dev.mongrl.com/llms-small.txt) | The same, trimmed, for smaller contexts. | | [`/openapi.json`](https://dev.mongrl.com/openapi.json) | The public API spec. Generate the client from it. | | [`/AGENTS.md`](https://dev.mongrl.com/AGENTS.md) | Rules to keep in the storefront repo, so every session follows them. | | Any page + `.md` | One topic as Markdown, e.g. [`/guides/checkout.md`](https://dev.mongrl.com/guides/checkout.md.md). | ## Set up the repo ```sh curl -o AGENTS.md https://dev.mongrl.com/AGENTS.md curl -o openapi.json https://dev.mongrl.com/openapi.json ``` If your agent reads `CLAUDE.md` rather than `AGENTS.md`, add a `CLAUDE.md` that says `@AGENTS.md`. Then put the keys in your environment, never in the repo: ```sh MONGRL_API_URL=https://api.staging.mongrl.com MONGRL_SECRET_KEY=sk_test_… STRIPE_PUBLISHABLE_KEY=pk_test_… # the marketplace's Stripe account ``` ## A first prompt ```text Read https://dev.mongrl.com/llms.txt and AGENTS.md. We're building the storefront for our marketplace on mongrl, with . 1. Generate a typed client from openapi.json with @hey-api/openapi-ts. 2. Add a server-only mongrl client (secret key from MONGRL_SECRET_KEY, the user's session from an HttpOnly cookie as X-User-Session). 3. Build the shop page: search with q, category and brand filters, sort, and cursor paging, showing display_price in the visitor's currency. 4. Build the listing page from getListing. Use only endpoints and fields from openapi.json. Show problem `detail`s from errors to people. Stop and show me the shop page before going further. ``` Then go area by area, one guide per step, so the agent loads only what it needs: 1. Accounts: "Follow https://dev.mongrl.com/guides/accounts.md: sign-up, sign-in with password and with emailed links, confirm email, reset password." 2. Checkout: "Follow https://dev.mongrl.com/guides/checkout.md and https://dev.mongrl.com/guides/delivery.md." 3. Selling: "Follow https://dev.mongrl.com/guides/selling.md and https://dev.mongrl.com/guides/payouts.md." 4. Messages and the pages emails link to: "Follow https://dev.mongrl.com/guides/messages.md and https://dev.mongrl.com/guides/storefront-pages.md." ## Why agents get it right here - **One spec, generated from code.** The spec on this site is built from the API's own code on every release. An agent coding against it codes against what the API does. - **Types, not guesses.** With a generated client, a wrong field or path fails to compile instead of failing in production. - **Errors that say how to fix them.** Validation errors name the field (`body.price.amount`) and the problem, so an agent can correct its own request. - **Safe retries.** Idempotency keys make a repeated write run once, so an agent (or a flaky network) can't buy twice. - **Test keys.** `sk_test_…` keys and Stripe test cards let an agent run the whole flow without touching real orders or money. --- # Authentication > Secret and publishable keys, user sessions, and why everything goes through your server. Every request carries an API key. Requests for a signed-in user also carry that user's session. Both are sent by **your server**. ## Keys ```http Authorization: Bearer sk_test_51Hx… ``` | Key | Prefix | Where it lives | What it can do | |---|---|---|---| | Secret | `sk_live_…`, `sk_test_…` | Your server's environment | Everything a storefront does, including acting for a signed-in user. | | Publishable | `pk_live_…`, `pk_test_…` | Native apps | Public data only: the marketplace, listings, search, categories, catalogue terms, locations. | - **Live** keys work on `https://api.mongrl.com`, **test** keys on `https://api.staging.mongrl.com`. - A key belongs to one marketplace, so it also says which marketplace you mean. There's no marketplace id to send. - The marketplace's team creates and revokes keys in the console. A key is shown once. - A key that doesn't work (unknown, revoked, malformed) gets `401`, never anonymous access. - `GET /v1/marketplace` is the quickest way to check a key. ## Why your server makes the calls The API doesn't accept requests from web pages on other sites, and the secret key must never reach a browser. So your storefront's server calls mongrl, and your pages call your server. That's also where sessions stay safe: in an HttpOnly cookie that scripts can't read. ```text browser ──your cookie──▶ your server ──sk_… + X-User-Session──▶ mongrl API ``` Frameworks with server functions or route handlers (TanStack Start, Next.js, Remix, SvelteKit, Nuxt) make this short: one server-only module builds the client per request. ## User sessions Signing in returns a session: ```json { "user": { "id": "4c1e…", "email": "mia@example.com", "first_name": "Mia" }, "session": { "token": "q8Jx2mR…", "expires_at": "2026-11-18T10:00:00Z" } } ``` 1. Store `session.token` in a cookie: `HttpOnly; Secure; SameSite=Lax; Path=/`. Give it a long life (90 days); the API decides when the session ends. 2. On requests for that user, send it as `X-User-Session: ` together with your secret key. 3. A session ends 30 days after it was last used, and 90 days after sign-in at the latest. 4. Signing out is `DELETE /v1/sessions/current`. Then clear the cookie. 5. A `401` on a user request means the session has ended: clear the cookie and send them to sign in. The token is shown only when the session is created. mongrl stores just a hash of it. ## Which endpoints need what | Endpoints | Key | Session | |---|---|---| | `/v1/marketplace`, `/v1/listings`, `/v1/categories`, `/v1/catalog-terms`, `/v1/locations` | secret or publishable | no | | `/v1/users`, `/v1/sessions…`, `/v1/password-resets…`, `/v1/email-verifications` | secret | no (they create one) | | `/v1/me…` (profile, listings, orders, favourites, messages, payouts) | secret | yes | | `/v1/images/{id}/{file}` | none | no | Listing responses include `favorited` (has this user saved it?) only when you send a session. ## Common mistakes - **Calling the API from client-side code.** Move the call into a server function. - **Putting the token in localStorage.** Use an HttpOnly cookie. - **Sharing one client between requests with a session in it.** Build the client per request, so one user's session never leaks into another's request. --- # Accounts and sign-in > Sign-up, sign-in with a password or an emailed link, email confirmation, password reset and the profile. Buyers and sellers share one kind of account. People can use a password, emailed sign-in links, or both. All of these calls come from your server with your secret key ([Authentication](https://dev.mongrl.com/guides/authentication.md)). ## Sign up ```http POST /v1/users { "email": "mia@example.com", "password": "correct horse battery", "first_name": "Mia", "locale": "sv" } ``` - The answer is `201` with `user` and `session`: they're signed in. Store the session ([how](https://dev.mongrl.com/guides/authentication.md#user-sessions)). - `password` is optional. Without one, the user signs in with emailed links. Passwords are 8 to 128 characters. - `locale` is one of the marketplace's languages (`GET /v1/marketplace` → `locales`). Emails go out in it. - mongrl emails a link to confirm the address. Until then `user.email_verified` is `false`; buying and selling still work. - An address already in use gives `409`. ## Sign in with a password ```http POST /v1/sessions { "email": "mia@example.com", "password": "correct horse battery" } ``` After 10 wrong passwords in 15 minutes the account is locked for the rest of that window: `429` with a `Retry-After` header. Show the problem's `detail`. ## Sign in with an emailed link 1. `POST /v1/sessions/email-link` with `{ "email": "…" }`. It always answers `202`, so it never reveals who has an account. At most 5 emails an hour go to one address. 2. mongrl emails a link to **your** page: `https:////auth/sign-in?token=…`. 3. That page sends the token: `POST /v1/sessions/from-link` with `{ "token": "…" }`. The answer is `user` and `session`. Links work once and for 15 minutes. Following one also confirms the email address. A used or expired link gives `422`: offer to send a new one. A blocked user gets `403`. Accounts the marketplace's team creates in the console get a welcome email with the same kind of link, valid for 7 days. ## Confirm the email address The confirmation link goes to `https:////auth/verify-email?token=…`. That page calls `POST /v1/email-verifications` with `{ "token": "…" }`. Links last 24 hours; `POST /v1/me/verification-email` sends a new one to the signed-in user. ## Reset a password 1. `POST /v1/password-resets` with `{ "email": "…" }`. Always `202`. 2. The link goes to `https:////auth/reset-password?token=…` and lasts an hour. 3. That page asks for the new password and calls `POST /v1/password-resets/confirm` with `{ "token": "…", "password": "…" }`. A reset signs the user out everywhere. Sign them in again with the new password. ## The profile - `GET /v1/me` returns the signed-in user: name, email, `email_verified`, `has_password`, `locale`, `bio`, `phone`. - `PATCH /v1/me` changes only the fields you send. - `display_name` and `bio` are public (shown with a seller's listings); `phone` is private. ## Sign out `DELETE /v1/sessions/current` with the user's session, then clear your cookie. ## Checklist - [ ] Sign-up, sign-in and sign-out pages, plus "email me a link". - [ ] `//auth/sign-in`, `//auth/verify-email` and `//auth/reset-password` read `?token=` and call the API. See [Pages your shop needs](https://dev.mongrl.com/guides/storefront-pages.md). - [ ] The marketplace's team has set the shop's address in the console (**Settings → Shop and emails**), so emailed links point at your site. --- # Browsing and search > The shop page, search, filters, sorting, prices in the visitor's currency, listing pages, photos, categories, brands and favourites. Everything a visitor sees before they sign in works with your secret key alone (a publishable key works too, from a native app). ## Start from the marketplace `GET /v1/marketplace` tells you how to build the shop: | Field | Use it for | |---|---| | `currencies` | The currency picker. The first is the default. | | `locales` | Languages. The first is the default. | | `term_kinds` | Which catalogue filters exist, e.g. `brand`, `designer`. | | `listings.attributes_schema` | A JSON Schema of the marketplace's own listing fields (condition, dimensions…). Filter on them with `attr`. | | `shipping` | Delivery prices by size and zone, for "delivery from…" hints. See [Shipping and delivery](https://dev.mongrl.com/guides/delivery.md). | Cache it for a few minutes; it changes when the team edits Settings. ## Search and filter ```http GET /v1/listings?q=lounge+chair&term=&attr=condition:excellent&min_price=500000¤cy=EUR&sort=newest&limit=24 ``` | Parameter | What it does | |---|---| | `q` | Words in the title, catalogue terms or description. Accents don't matter and word beginnings match: "henn" finds Henningsen. | | `category` | This category and everything under it. | | `term` | Catalogue term ids; repeat it. Terms of one kind match any (brand A **or** B); different kinds must all match (brand A **and** designer C). | | `attr` | `key:value` on the marketplace's own fields; repeat it. Values of one key match any. | | `min_price`, `max_price` | Minor units of `currency`, which is then required. Listings in other currencies are compared at the day's rate. | | `seller` | One seller's listings, for a seller page. | | `currency` | Adds `display_price` in this currency (one of the marketplace's). | | `sort` | `relevance` (the default with `q`), `newest` (the default without), `price_asc`, `price_desc`, `popular` (most saved). | | `limit`, `cursor` | Paging. Pass `next_cursor` back as `cursor`; there's no more when it's absent. | The answer has `items`, `total` (how many match) and `next_cursor`. Only listings on sale are returned. ## Prices Every listing has `price`: what the seller set, in the listing's currency. With `currency`, it also has `display_price`, converted at the day's rate. - Show `display_price` when you asked for one, otherwise `price`. - Converted prices are approximate. The buyer pays what the [quote](https://dev.mongrl.com/guides/checkout.md#1-quote) says. - Format with `Intl.NumberFormat(locale, { style: 'currency', currency })` after dividing by 100 for currencies with two decimals. ```ts const money = ({ amount, currency }: { amount: number; currency: string }, locale = 'sv-SE') => new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount / 100) ``` A listing on sale may have a `price_schedule`: the price falls in steps while it doesn't sell. The current `price` is already the dropped one, so you don't need to work it out. ## The listing page `GET /v1/listings/{id}?currency=EUR` returns one listing on sale (`404` otherwise). Useful fields: - `title`, `description`, `terms` (by kind, each with `name` and `slug`), `attributes`, `category_id`. - `images` in display order; the first is the main photo. - `delivery`: how it can reach the buyer. See [Shipping and delivery](https://dev.mongrl.com/guides/delivery.md). - `site`: the showroom or warehouse it's stored at, when it is (`buyer_pickup` says whether buyers may collect it there). - `favorite_count`, and `favorited` when you send the user's session. - `seller_id`, for a link to the seller's other listings (`?seller=`). ## Photos Each image has `status` and `urls`: | Size | Use | |---|---| | `thumb` | 400 px square, for grids | | `medium` | 800 px wide, for listing pages on phones | | `large` | 1600 px wide, for zoom | The URLs are public and can be cached forever; a changed photo gets a new URL. Only `ready` images have sizes. ## Categories and catalogue terms - `GET /v1/categories?locale=sv` returns the whole tree at once, with `parent_id` and `position`. Build the menu from it. - `GET /v1/catalog-terms?kind=brand&in_use=true` returns every brand with listings on sale, each with `listing_count`, for a filter list. - `GET /v1/catalog-terms?kind=designer&q=hen` filters by the start of the name, a page at a time, for a brand or designer index. - `GET /v1/catalog-terms/{kind}/{slug}` returns one term, for a brand page at `/brands/`. Then list its listings with `term=`. ## Showrooms `GET /v1/locations` returns the marketplace's active showrooms and warehouses: address, opening hours and whether buyers may collect there. Use it for a "visit us" page. ## Favourites With the user's session: - `PUT /v1/me/favorites/{listing id}` saves, `DELETE` removes. Both are safe to repeat. - `GET /v1/me/favorites?currency=EUR` lists them, newest first, with cursor paging. Show the heart from `favorited` on listings you fetched with the session. --- # Selling > The sell form, drafts, photos, publishing, valuations, storage and the seller's listings. Any signed-in user can sell. A listing starts as a draft, gets photos, and is published: straight on sale, to the marketplace's team for approval, or to be valued first. All calls send the user's session. ## What the marketplace asks for Read `GET /v1/marketplace` → `listings` before building the form: | Field | Meaning | |---|---| | `min_images` | Photos needed to go on sale (at least 1 to be valued). | | `require_category` | A category is needed. | | `required_terms` | Term kinds needed, e.g. `["brand"]`. | | `attributes_schema` | A JSON Schema for the marketplace's own fields (condition, dimensions…). Render a field per property and send the values in `attributes`. | | `valuations` | Sellers may ask the team to value the listing instead of setting a price. | | `require_approval` | The team reviews listings before they go on sale. | ## Statuses ```text draft ──publish──▶ published (no approval needed) draft ──publish──▶ pending_approval ──▶ published (the team approves) └─▶ denied (with review_note) draft ──publish {ask_valuation}──▶ pending_valuation ──▶ valuation_offered valuation_offered ──accept──▶ pending_approval valuation_offered ──counter──▶ pending_approval (the team approves, denies or offers again) published ──close──▶ closed ──publish──▶ … ``` `GET /v1/me/listings` returns the seller's listings in every status, latest change first, with cursor paging. ## 1. Start a draft ```http POST /v1/me/listings { "title": "Lounge chair", "locale": "sv" } ``` A draft can be incomplete. Save as the seller goes: `PUT /v1/me/listings/{id}` **replaces** the listing's content, so always send the whole form, not just the changed field. | Field | Notes | |---|---| | `title`, `description` | Plain text. `locale` is the language the seller writes in. | | `price` | `{ "amount": 650000, "currency": "SEK" }`, minor units, one of the marketplace's currencies. Leave it out when asking for a valuation. | | `category_id`, `terms` | `terms` maps a kind to a term id: `{ "brand": "…", "designer": "…" }`. | | `attributes` | The marketplace's own fields, matching `attributes_schema`. | | `location` | `{ "country": "SE", "postal_code": "…", "city": "…" }`. A country is needed to publish. | | `delivery` | How it can reach the buyer: `shipping` (with a `price`, or none for "ask for a quote"), `pickup`, `buyer_arranged`. At least one. See [Shipping and delivery](https://dev.mongrl.com/guides/delivery.md). | | `stock` | 1 unless the seller has several of the same. | ## 2. Add photos Send each photo as the request body, with its content type: ```http POST /v1/me/listings/{id}/images Content-Type: image/jpeg ``` - JPEG, PNG or WebP, up to 20 MB and 50 megapixels, at most 20 per listing. - The answer is `202` with the image in `processing`. Its sizes are ready within seconds; poll `GET /v1/me/listings/{id}` until `status` is `ready`. - `PUT /v1/me/listings/{id}/images/order` with every image id sets the order; the first is the main photo. - `DELETE /v1/me/listings/{id}/images/{image_id}` removes one. Upload from your server: the browser sends the file to your server, which streams it on with the user's session. ## 3. Publish ```http POST /v1/me/listings/{id}/publish { "ask_valuation": false, "wants_storage": false } ``` - With approval on, only the basics are checked (title, photos, country, price), because the team completes the listing. - `ask_valuation: true` (where `valuations` is on): no price needed; the listing goes to `pending_valuation`. - `wants_storage: true`: the seller asks the marketplace to collect the listing and store it until it sells. The team books the collection; until then it stays with the seller. - If anything is missing, the `422` lists **every** problem in `errors`, so you can mark all the fields at once. A closed listing can be published again the same way. ## Valuations When the team has valued it, the listing is `valuation_offered` with: - `offer`: the price offered, and `offer_note`, why. - `offer_schedule`: price drops offered with it, if any. The seller answers: - `POST /v1/me/listings/{id}/offer/accept`: the offer becomes the price. - `POST /v1/me/listings/{id}/offer/counter` with `{ "amount": 720000 }`: their own price, in the offer's currency. Either way the listing goes to `pending_approval`. A denied listing has `review_note` with the reason; show it, and don't offer to send it again. ## On sale - `price_schedule` lists the drops ahead: each step has `months`, `amount`, `on` (when it applies) and `done`. Show the seller what's coming. - `custody` says where it is (`seller`, `collecting`, `stored`), and `site` which showroom or warehouse. `fulfilment` says who delivers it (`seller` or `marketplace`). - Editing a listing on sale keeps it on sale, so it must stay complete. Sellers can't edit a listing while the team reviews it, and closed or denied listings can't be edited. - `POST /v1/me/listings/{id}/close` takes it off sale. `DELETE` removes a draft; a listing that was ever on sale is closed instead. ## When it sells The seller sees the order under `GET /v1/me/orders?as=seller`. If the seller delivers it themselves, they mark it sent with tracking details. See [Checkout and orders](https://dev.mongrl.com/guides/checkout.md#moving-an-order-on). They're paid once the buyer has it, after setting up [payouts](https://dev.mongrl.com/guides/payouts.md). ## Emails mongrl sends sellers When the team offers a price, denies a listing or puts it on sale, the seller gets an email linking to `https:////account/listings/`. Build that page. See [Pages your shop needs](https://dev.mongrl.com/guides/storefront-pages.md). --- # Checkout and orders > Quote, place the order, take the payment with Stripe, and follow the order until the seller is paid. One order buys one listing. Checkout is three calls and Stripe's payment form; after that, the order moves on by transitions until the buyer has it and the seller is paid. Every call sends the buyer's session. ## 1. Quote Show what the buyer will pay before they pay it: ```http POST /v1/me/orders/quote { "listing_id": "8f3c…", "currency": "EUR", "delivery": { "method": "shipping", "option": "standard", "address": { "name": "Mia Holm", "line1": "Hallvägen 29", "postal_code": "121 62", "city": "Johanneshov", "country": "SE", "phone": "+46…" } }, "voucher_code": "SPRING10" } ``` ```json { "currency": "EUR", "total": 61900, "line_items": [ { "code": "item", "label": "Eames lounge chair", "unit_price": 59000, "quantity": 1, "amount": 59000 }, { "code": "shipping", "label": "Shipping", "unit_price": 8800, "quantity": 1, "amount": 8800 }, { "code": "voucher", "label": "Voucher SPRING10", "unit_price": -5900, "quantity": 1, "amount": -5900 } ] } ``` - Show `line_items` as they come, by `label`, and `total` as the amount to pay. Don't add things up yourself. - Quote again whenever the delivery, address or voucher changes. Placing the order prices it the same way. - `currency` is what the buyer pays in: one of the marketplace's, the listing's when left out. Line item codes: | Code | Meaning | |---|---| | `item` | The listing. | | `shipping` | Delivery, priced. | | `collect` | Collected at the showroom or warehouse where it's stored. Free. | | `shipping_address_needed` | Ask for the address; the price comes with it. Placing without one fails. | | `shipping_quote` | Delivery can't be priced here: the marketplace sends a quote after the order. | | `voucher` | A discount (negative). | A voucher that doesn't apply gives `422` with the reason on `body.voucher_code`. Show it next to the voucher field. ## 2. Place the order ```http POST /v1/me/orders Idempotency-Key: 6f1d3c5e-… { the same body as the quote } ``` ```json { "order": { "id": "1a2b…", "state": "pending_payment", "total": 61900, "currency": "EUR", "available_transitions": [] }, "payment": { "client_secret": "pi_3Q…_secret_…" } } ``` - This reserves the listing and starts the payment. The buyer has **15 minutes** to pay; then the order becomes `payment_expired` and the listing is released. - Send an `Idempotency-Key` (a new UUID for each press of "Pay"), so a retried request can't place two orders. - `409` means it can't be bought right now: sold, reserved by someone else, or the delivery isn't possible. Show the `detail` and go back to the listing. ## 3. Take the payment with Stripe Payments run on the marketplace's Stripe account. Use Stripe's [Payment Element](https://docs.stripe.com/payments/payment-element) with the marketplace's Stripe **publishable** key (the team gives it to you; it's public) and the order's `client_secret`. Cards, Klarna and the other methods the marketplace has turned on in Stripe appear by themselves. ```ts const stripe = await loadStripe(STRIPE_PUBLISHABLE_KEY) const elements = stripe.elements({ clientSecret: payment.client_secret }) elements.create('payment').mount('#payment') // On "Pay": const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: `${location.origin}/orders/${order.id}` }, }) if (error) showError(error.message) // otherwise Stripe sends the buyer to return_url ``` When Stripe confirms the payment, mongrl moves the order to `purchased`. That happens in the background, a moment after the buyer lands on your order page: read the order (`GET /v1/me/orders/{id}`) and, while it's still `pending_payment`, check again every couple of seconds. In the test environment, pay with Stripe's test card `4242 4242 4242 4242`, any future date and any CVC. ## Order states ```text (new) ─request-payment─▶ pending_payment ─confirm-payment─▶ purchased └─expire-payment (15 min)─▶ payment_expired purchased ─mark-delivered─▶ delivered ─mark-received / auto (14 days)─▶ received purchased ─mark-received-early─▶ received purchased ─cancel / auto-cancel (14 days)─▶ canceled delivered ─dispute─▶ disputed ─resolve-received─▶ received └─resolve-canceled / auto (60 days)─▶ canceled ``` `payment_expired`, `received` and `canceled` are final. Each order has `history`: every transition with when it happened, for a timeline on the order page. ## Moving an order on Each order has `available_transitions`: what **this user** may do now. Show a button for each and nothing else. ```http POST /v1/me/orders/{id}/transitions/mark-delivered { "data": { "tracking_number": "JJSE00012345", "carrier": "PostNord" } } ``` | Transition | Who | When | |---|---|---| | `mark-delivered` | the seller | It's sent or handed over. `data` can carry `tracking_number` and `carrier`; they appear in `order.delivery`. | | `mark-received` | the buyer | They have it. The seller is paid. | | `mark-received-early` | the buyer | They have it before it was marked sent (e.g. picked up). The seller is paid. | | `dispute` | the buyer | Something's wrong after delivery. The marketplace's team resolves it. | When the marketplace delivers a listing itself (`fulfilment: marketplace`, e.g. stored listings), its team marks the order sent in the console. The timers keep orders moving if nobody acts: an order that's never sent is canceled and refunded after 14 days; a delivered order counts as received after 14 days. ## Listing orders - `GET /v1/me/orders` lists the buyer's purchases; `?as=seller` lists their sales. Filter with `state`; page with `limit` and `offset`. - Each order has `line_items`, `total`, `delivery` and, for the seller, `payout`: `subtotal`, `commission`, `storage_fee`, `amount` and its `status` (`pending` until the buyer has it, then `paid`; `waiting_for_payout_details` until the seller sets up [payouts](https://dev.mongrl.com/guides/payouts.md)). ## Emails mongrl sends Buyers and sellers get emails when an order is paid, sent and received, each linking to `https:////orders/`. Build that page. See [Pages your shop needs](https://dev.mongrl.com/guides/storefront-pages.md). ## Checklist - [ ] Checkout: delivery choice, address, voucher, live quote, "Pay" with the Payment Element. - [ ] `Idempotency-Key` on `placeOrder`. - [ ] The order page waits for `purchased` after Stripe returns, then shows the timeline and the buttons from `available_transitions`. - [ ] A delivery-time picker when the order offers one. See [Shipping and delivery](https://dev.mongrl.com/guides/delivery.md#booking-a-delivery-time). --- # Shipping and delivery > How a listing reaches the buyer, how delivery is priced from the marketplace's shipping table, and letting buyers book a delivery time. A listing reaches the buyer in one of a few ways, and who moves it depends on the listing. mongrl prices it; your checkout offers the choices and shows the quote. ## Who moves it Every listing has `fulfilment`: - **`seller`**: the seller ships it or hands it over. The ways they offer are in the listing's `delivery`. - **`marketplace`**: the marketplace's team delivers it, typically because it's stored at their showroom or warehouse (`custody: stored`, `site`). ## The delivery methods | `delivery.method` | Offered when | Price | |---|---|---| | `shipping` | Seller listings with `delivery.shipping`, and every marketplace listing. | The seller's `shipping.price`, or the marketplace's shipping table (below). No price means "we'll send a quote". | | `pickup` | Seller listings with `delivery.pickup`: the buyer picks it up from the seller. | Free | | `buyer_arranged` | Seller listings with `delivery.buyer_arranged`: the buyer sends their own courier. | Free | | `collect` | Marketplace listings stored at a site where `site.buyer_pickup` is true: the buyer collects it there. | Free | When a listing offers only one method you can leave `method` out. Offer the choices from the listing, then quote with the buyer's pick. ## The shipping table Listings the marketplace moves are priced from its table, in `GET /v1/marketplace` → `shipping`: - **`sizes`**: size classes, e.g. small parcel, armchair, sofa. Each listing has a `shipping_size`. - **`zones`**: where buyers are, by `countries` and optional `postal_prefixes`. Each zone has a `delivery_time` to show buyers, e.g. "2–5 working days". - **`options`**: delivery services, e.g. standard and white glove, with an `about` text. Send the buyer's pick as `delivery.option` (the first when left out). - **`delivery`**: prices by option, size and zone. A missing price means a quote. - **`collection`**: what it costs to collect a listing that's still at the seller's, by size and the seller's zone. mongrl adds it to the shipping line by itself. You don't need to work prices out: send the address and option in the [quote](https://dev.mongrl.com/guides/checkout.md#1-quote) and show what comes back. Use the table only for hints such as "delivery from €49" or the delivery time. The address needs at least `country` (and `postal_code` where zones use prefixes) to be priced. Without it the quote says `shipping_address_needed`. ## Booking a delivery time Some marketplaces let buyers choose when the delivery comes (`shipping.booking`: weekdays, time windows, how far ahead). It applies to paid orders that the marketplace ships to a zone it books. ```http GET /v1/me/orders/{id}/delivery-booking ``` ```json { "booked": null, "changeable": true, "slots": [ { "date": "2026-10-23", "window": "am", "label": "08–12", "full": false }, { "date": "2026-10-23", "window": "pm", "label": "13–17", "full": true } ] } ``` - `409` means this order has no time to book (not paid yet, collected, or the carrier arranges it). Show nothing. - Show `slots` grouped by `date`; `full` ones can't be chosen. - Book with `PUT /v1/me/orders/{id}/delivery-booking` and `{ "date": "2026-10-23", "window": "am" }`. - `changeable` turns false once it's too close to change. The booked time is in `booked`. ## Storage fees (sellers) When a seller asks the marketplace to store their listing (`wants_storage` when publishing), storage can cost a monthly fee by size after `free_storage_weeks`. It's taken from the seller's payout when it sells and shown on the order as `payout.storage_fee`. Show sellers `shipping.storage_fees` and the free weeks before they choose storage. --- # Payouts > Let sellers set up payouts with Stripe's embedded onboarding, and show them where their money is. Sellers are paid through Stripe Connect on the marketplace's Stripe account. A seller sets up payouts once, inside your shop, with Stripe's embedded onboarding form. Until they do, sales still happen; the payout waits. ## Check where they are ```http GET /v1/me/payouts ``` The answer has `status`: | `status` | Show | |---|---| | `none` | "Set up payouts". | | `action_required` | "Finish setting up payouts". `due` lists what Stripe still needs. | | `pending` | "Stripe is checking your details". | | `ready` | Nothing to do: they'll be paid. | ## Run the onboarding form 1. Ask whether they sell as a private person or a company, and where they live. 2. Start the session from your server: ```http POST /v1/me/payouts/session { "country": "SE", "business_type": "individual" } ``` The answer has a `client_secret`. `country` and `business_type` are used the first time only. 3. Show Stripe's [account onboarding component](https://docs.stripe.com/connect/supported-embedded-components/account-onboarding) with it, asking only for what's needed now: ```ts import { loadConnectAndInitialize } from '@stripe/connect-js' const connect = loadConnectAndInitialize({ publishableKey: STRIPE_PUBLISHABLE_KEY, fetchClientSecret: async () => (await startPayoutSetup()).client_secret, // your server calls POST /v1/me/payouts/session }) const onboarding = connect.create('account-onboarding') onboarding.setCollectionOptions({ fields: 'currently_due', futureRequirements: 'omit' }) onboarding.setOnExit(() => refreshPayoutStatus()) document.querySelector('#payouts')?.append(onboarding) ``` 4. When they leave the form, read `GET /v1/me/payouts` again. ## When sellers are paid - An order's `payout` shows the seller's side: `subtotal`, `commission`, `storage_fee` and `amount`. - It's `pending` until the buyer has the listing (`received`), then `paid`. - `waiting_for_payout_details` means the sale is done but the seller hasn't finished setting up payouts. It's paid as soon as they do. - `held_for_chargeback` means the buyer's bank disputed the payment. The marketplace's team handles it. --- # Messages > Questions about a listing, messages about an order, and support from the marketplace's team. Users message each other and the marketplace in conversations. There are three kinds, and there's one conversation per pair and subject, so starting one again continues it. | `kind` | Between | Start it with | |---|---|---| | `listing` | A buyer and the seller, before buying | `listing_id` | | `order` | The buyer and the seller of an order | `order_id` | | `support` | A user and the marketplace's team | `support: true` | ## Start or continue a conversation ```http POST /v1/me/conversations { "listing_id": "8f3c…", "body": "Is the leather original?" } ``` The answer has the `conversation` and the new `message`. If one already exists, the message joins it. ## Show the inbox - `GET /v1/me/conversations` lists the user's conversations, latest first, each with `last_message`, `last_message_at` and `unread`. Page with `limit` and `offset`. - `GET /v1/me/conversations/{id}/messages` reads one, oldest first, and marks it read. Pass `after=` to get only newer messages. - `POST /v1/me/conversations/{id}/messages` with `{ "body": "…" }` replies. In a message, `from_marketplace: true` means the marketplace's team wrote it (support); otherwise `sender_id` is the user who did. In `support` conversations, `user_a` is the user asking. Show a badge from the sum of `unread`. There's no push: refresh the inbox when the user opens it, and while a conversation is open, ask for messages `after` the last one every few seconds. ## Emails When someone gets a message they're emailed a link to `https:////messages/`. Build that page. See [Pages your shop needs](https://dev.mongrl.com/guides/storefront-pages.md). --- # Content pages > Render the pages the marketplace's team builds in the console (the home page, About, How it works) from blocks filled in with live data. The marketplace's team builds pages in the console from blocks: a hero, listing grids, banners, category tiles and so on. Your storefront renders them, in its own design. You never hard-code the home page's content: the team changes it without a deploy. ## Fetch a page ```http GET /v1/pages/home?locale=en¤cy=EUR ``` `home` is the front page; other pages have their own address (`about`, `how-it-works`). Serve each at `/`. The answer has `title` and `description` (for `` and the meta description), `share_image` (for `og:image`) and `blocks`, in order. - `404` means the page isn't published. For `home`, fall back to a simple page of your own. - Pages come in the marketplace's languages; without one in `locale`, you get the default language's page. - `GET /v1/pages` lists every published page, for your sitemap. ## Render the blocks Render each block by its `type`, and **skip types you don't know**, so new block types never break your shop. Blocks arrive filled in: listings with their photos and prices, images with their sizes, categories with their names, the showroom with its address. A block with nothing left to show (its listings all sold, a missing image) is left out, so you don't need to check. | `type` | Show | Filled in | |---|---|---| | `hero` | `eyebrow`, `heading`, `text`, a search box when `search`, up to two `buttons`; beside it the `image` or the `listing` | `image`, `listing` | | `links` | A row of `links` | | | `listing_grid` | `heading`, a "view all" `link`, and the listing cards | `listings` | | `banner` | A full-width `image` (`phone_image` on phones), with `heading` and `text` on it, linking to `link` | `image`, `phone_image` | | `category_tiles` | `heading` and `tiles`: each tile's `image` and `label` (or its category's name), linking to the category | `tiles[].category`, `tiles[].image` | | `image_text` | The `image` on `image_side`, beside `eyebrow`, `heading`, `text` and `link` | `image` | | `featured_listing` | One `listing`, big, with `heading` and `text`. It may be sold (`stock` 0) when the team chose to keep it | `listing` | | `cta` | A band in `tone` (brand, light or dark) with `heading`, `text` and up to two `buttons` | | | `newsletter` | `heading`, `text`, an email field and `button_label` | | | `promises` | `items`, each a `title` and `text` | | | `showroom` | `heading`, `text`, and the `location`'s address and `opening_hours` | `location`, `image` | | `text` | `body`, Markdown | | | `faq` | `heading` and `questions`, each a `question` and a Markdown `answer` | | Links (`href`) are a path in your shop (`/shop?sort=newest`) or an `https://` address. Listings in blocks are the same as in search, so reuse your listing card. Buttons have a `style`: `primary` or `secondary`. Image URLs (`urls.thumb`, `medium`, `large`, `xlarge`) never change: cache them forever and use `srcset`. ## The newsletter block Send the address from your server: ```http POST /v1/newsletter/subscribers { "email": "mia@example.com", "first_name": "Mia", "locale": "en", "source": "home" } ``` It always answers `202` for a valid address (never revealing who's signed up), and a bad address gets `422`. mongrl keeps the list and adds the address to the marketplace's mailing tool. ## Preview The console's **Preview** opens `https://<your shop>/<slug>?preview=<token>`. When that query is there, fetch the draft instead: ```http GET /v1/pages/home/preview?token=<token>¤cy=EUR ``` Don't cache previews, and add `<meta name="robots" content="noindex">` to them. --- # Pages your shop needs > The pages mongrl's emails link to, and the settings that make the links work. mongrl sends the emails (sign-in links, confirmations, order and message notices), and their links point at **your** storefront. Build these pages, or the links lead nowhere. ## Set the shop's address first In the console, **Settings → Shop and emails**: the marketplace's team sets the shop's address (e.g. `https://www.example.com`) and who emails come from. Until the address is set, mongrl can't send emailed links: requesting one gives `409`. ## The pages `<lang>` is the user's language, lower-case (`sv`, `en`): their `locale`, or the marketplace's default. | Page | Linked from | What it does | |---|---|---| | `/<lang>/auth/sign-in?token=…` | Sign-in links, welcome emails | `POST /v1/sessions/from-link` with the token, store the session, go on to the account. | | `/<lang>/auth/verify-email?token=…` | Sign-up, "send it again" | `POST /v1/email-verifications` with the token, then say it's confirmed. | | `/<lang>/auth/reset-password?token=…` | Password reset emails | Ask for a new password, `POST /v1/password-resets/confirm`, then sign in. | | `/<lang>/orders/<order id>` | Order emails to buyers and sellers | Sign in if needed, then `GET /v1/me/orders/{id}`: state, timeline, buttons from `available_transitions`, delivery booking. | | `/<lang>/messages/<conversation id>` | New message emails | Sign in if needed, then the conversation. | | `/<lang>/account/listings/<listing id>` | Valuation offers, denials, listings put on sale | Sign in if needed, then `GET /v1/me/listings/{id}`: the offer to accept or answer, the reason it was denied, or that it's on sale. | Tokens in links work once. If the page loads twice (a mail scanner, a double click), the second try gets `422`: say the link has been used and offer a new one. ## If your shop doesn't use language prefixes Redirect them. A shop in one language can send `/en/…` to `/…` and keep the query string, so every emailed link still lands. ## Everything else is yours Shop, search, listing, brand and designer pages, checkout, the account, selling: their addresses are up to you. Keep them stable, because listing URLs get shared and indexed. --- # Conventions > Errors, money, ids and dates, paging, idempotency, rate limits, languages and versioning: how every endpoint behaves. ## Base URLs | Environment | URL | Keys | |---|---|---| | Test | `https://api.staging.mongrl.com` | `sk_test_…`, `pk_test_…` | | Live | `https://api.mongrl.com` | `sk_live_…`, `pk_live_…` | All requests and responses are JSON (`application/json`), except photo uploads (the image's bytes). See [Authentication](https://dev.mongrl.com/guides/authentication.md) for keys and sessions. ## Errors Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problems, sent as `application/problem+json`: ```json { "title": "Unprocessable Entity", "status": 422, "detail": "the listing isn't ready to go on sale", "errors": [ { "location": "body.price", "message": "a price is needed", "value": null }, { "location": "body.images", "message": "add at least 3 photos" } ] } ``` - Show `detail` to people. It's written to be read. - `errors[].location` points at the field: `body.<field>`, `query.<name>` or `path.<name>`. Map it onto your form. - Validation errors list **every** problem at once, not just the first. | Status | Meaning | |---|---| | `400` | The request is malformed (bad JSON, a bad `Idempotency-Key`). | | `401` | No key, a key that doesn't work, or a user session that has ended. | | `403` | The key or user may not do this (e.g. a blocked user). | | `404` | Not found, or not yours. Other marketplaces' and users' things are always `404`. | | `409` | Not possible in the current state: sold, already in use, a transition that isn't available. | | `422` | The input is wrong. See `errors`. | | `429` | Too many attempts. Wait for `Retry-After` seconds. | | `503` | A dependency (such as payments) isn't available. Try again later. | ## Money Money is always an object with an integer amount in **minor units** and an ISO 4217 currency: ```json { "amount": 125000, "currency": "SEK" } ``` That's 1 250,00 kr. Never use floats for money. Totals and line items on orders are plain integers in the order's `currency`. ## Ids and dates - Ids are UUIDs (`"8f3c2a1e-…"`). Treat them as opaque strings. - Times are RFC 3339 in UTC (`"2026-10-19T14:05:00Z"`). Dates without a time are `YYYY-MM-DD`. ## Paging Two styles, depending on the list: | Lists | Style | |---|---| | Listings, favourites, a seller's listings, catalogue terms | **Cursor:** pass `next_cursor` back as `cursor`. No `next_cursor` means the last page. | | Orders, conversations | **Offset:** `limit` and `offset`. A page shorter than `limit` is the last. | | Messages in a conversation | **After:** `after=<message id>` returns only newer messages, oldest first. | `limit` is at most 100 (200 for catalogue terms and messages). Search also returns `total`, the number of matches. ## Idempotency Send `Idempotency-Key` on any `POST`, `PUT`, `PATCH` or `DELETE` you might retry, above all placing an order. Use a fresh UUID per user action. - A retry with the same key and the same request doesn't run again: it replays the first response, with `Idempotent-Replayed: true`. - The same key with a different request is `422`; while the first request still runs it's `409`. - Server errors (`5xx`) aren't stored, so retrying them runs the request again. - Keys last 24 hours and belong to your key (and user), so they never collide with anyone else's. ## Rate limits Limits protect people, not the API's capacity: - Sign-in: 10 wrong passwords in 15 minutes locks the account for the rest of that window. - Emailed links (sign-in, password reset, confirmation): 5 an hour per address. Over a limit you get `429` with `Retry-After`. ## Languages The marketplace's languages are in `GET /v1/marketplace` → `locales` (BCP 47, the first is the default). Pass `locale` where an endpoint takes one (categories, catalogue terms); a user's `locale` decides the language of their emails. ## Versioning Everything is under `/v1`. Within `v1`, changes only add things: new endpoints, new optional request fields, new response fields, new enum values. Write clients that ignore fields they don't know. Anything that would break a client gets a new version. ## The spec The [OpenAPI 3.1 spec](https://dev.mongrl.com/openapi.json) ([YAML](https://dev.mongrl.com/openapi.yaml)) is generated from the API's code and published with it, so it always matches the API. It covers everything a storefront can call; the console's own endpoints aren't in it. --- # Marketplace > Who the key belongs to and its rules: currencies, languages, what a listing needs, the shipping table, showrooms and warehouses. Who the key belongs to and its rules: currencies, languages, what a listing needs, the shipping table, showrooms and warehouses. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/locations`](#the-marketplaces-showrooms-and-warehouses) | The marketplace's showrooms and warehouses | | GET | [`/v1/marketplace`](#get-the-callers-marketplace) | Get the caller's marketplace | ## The marketplace's showrooms and warehouses `GET /v1/locations` · operation `listLocations` Active ones, e.g. for a showroom page. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Responses:** `200` [ListOutputBody](#listoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Get the caller's marketplace `GET /v1/marketplace` · operation `getMarketplace` The marketplace the API key (or console session) belongs to. Handy for checking a key. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Responses:** `200` [PublicBody](#publicbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### ListOutputBody | Field | Type | Notes | |---|---|---| | `items` * | [SiteBody](#sitebody)[] | | ### PublicBody | Field | Type | Notes | |---|---|---| | `currencies` * | string[] | ISO 4217 codes; the first is the default. Empty until chosen. | | `id` * | string | | | `listings` * | [ListingRules](#listingrules) | What sellers need to know to list | | `locales` * | string[] | BCP 47 language tags; the first is the default. | | `name` * | string | | | `shipping` * | [ShippingBody](#shippingbody) | Delivery prices by size and zone (empty: listings have their own) | | `slug` * | string | | | `term_kinds` * | [TermKindBody](#termkindbody)[] | Kinds of catalogue terms listings refer to, e.g. brand | ### SiteBody | Field | Type | Notes | |---|---|---| | `active` * | boolean | | | `buyer_pickup` * | boolean | Buyers may collect purchases here | | `city` * | string | | | `country` * | string | | | `created_at` * | string (date-time) | | | `id` * | string | | | `kind` * | string | one of `showroom`, `warehouse` | | `line1` * | string | | | `name` * | string | | | `opening_hours` | string | Free text, e.g. Sundays 11–16 | | `postal_code` * | string | | ### ListingRules | Field | Type | Notes | |---|---|---| | `attributes_schema` | any | JSON Schema for the marketplace's own listing fields | | `min_images` * | integer | Photos needed to go on sale (to be valued, at least 1) | | `require_approval` * | boolean | Listings are reviewed before they go on sale | | `require_category` * | boolean | | | `required_terms` * | string[] | Term kinds a listing needs, e.g. brand | | `valuations` * | boolean | Sellers may ask the marketplace to value a listing instead of setting a price | ### ShippingBody | Field | Type | Notes | |---|---|---| | `booking` | [BookingBody](#bookingbody) | When buyers can choose their delivery time | | `collection` | map of map of integer | Collecting from the seller, by size and the seller's zone; missing means quoted | | `currency` | string | The rates' currency | | `delivery` | map of map of map of integer | By option, size and the buyer's zone, in minor units; missing means quoted | | `free_storage_weeks` | integer | Weeks of storage before the fee starts. ≥ 0; ≤ 104 | | `options` | [ShippingOptionBody](#shippingoptionbody)[] | Delivery services, e.g. standard, white glove | | `sizes` * | [ShippingSizeBody](#shippingsizebody)[] | | | `storage_fees` | map of integer | Per started month, by size, taken from the seller's payout | | `zones` * | [ShippingZoneBody](#shippingzonebody)[] | | ### TermKindBody | Field | Type | Notes | |---|---|---| | `key` * | string | | | `names` * | map of string | Per language | ### BookingBody | Field | Type | Notes | |---|---|---| | `horizon_days` * | integer | How far ahead buyers can book. ≥ 1; ≤ 120 | | `lead_days` * | integer | The earliest slot is this many days after today. ≥ 0; ≤ 30 | | `per_window` | integer | Deliveries per window; 0 is no limit. ≥ 0 | | `time_zone` | string | e.g. Europe/Stockholm (the default) | | `weekdays` * | integer[] | 1 is Monday, 7 Sunday | | `windows` * | [BookingWindowBody](#bookingwindowbody)[] | | | `zones` * | string[] | Zone keys whose buyers book | ### ShippingOptionBody | Field | Type | Notes | |---|---|---| | `about` | string | | | `key` * | string | | | `name` * | string | | ### ShippingSizeBody | Field | Type | Notes | |---|---|---| | `about` | string | | | `key` * | string | | | `name` * | string | | ### ShippingZoneBody | Field | Type | Notes | |---|---|---| | `countries` * | string[] | ISO codes, e.g. ["SE"] | | `delivery_time` | string | Shown to buyers, e.g. 2–5 working days | | `key` * | string | | | `name` * | string | | | `postal_prefixes` | string[] | None: the whole countries | ### BookingWindowBody | Field | Type | Notes | |---|---|---| | `key` * | string | | | `label` * | string | e.g. 08–12 | --- # Listings and search > Listings on sale, searched and filtered, and their photos. Listings on sale, searched and filtered, and their photos. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/images/{id}/{file}`](#get-a-photo) | Get a photo | | GET | [`/v1/listings`](#search-listings-on-sale) | Search listings on sale | | GET | [`/v1/listings/{id}`](#get-a-listing-on-sale) | Get a listing on sale | ## Get a photo `GET /v1/images/{id}/{file}` · operation `getImage` Public, for <img> tags: no key needed. `file` is thumb.jpg, medium.jpg or large.jpg. Never changes, so it's cached for a year. **Auth:** None. **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | | `file` * | path | string | one of `thumb.jpg`, `medium.jpg`, `large.jpg` | **Responses:** `200` OK · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Search listings on sale `GET /v1/listings` · operation `listListings` Every parameter is optional. `q` matches the title, catalogue terms (brand, designer…) and description, ignoring accents and matching word beginnings ("henn" finds Henningsen). Filters combine: `term` matches any of the terms given per kind (brand A or B, and designer C), `attr` matches any of the values given per attribute. Prices are compared across currencies at the day's rate. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `limit` | query | integer | ≥ 1; ≤ 100; default `24` | | `cursor` | query | string | next_cursor from the previous page | | `q` | query | string | Words to look for. max 200 chars | | `sort` | query | string | relevance (default with q), newest (default without), popular (most saved). one of `relevance`, `newest`, `price_asc`, `price_desc`, `popular` | | `category` | query | string | This category and its subcategories | | `term` | query | string[] | Catalogue term ids, e.g. brands; repeat for several | | `seller` | query | string | | | `attr` | query | string[] | key:value, e.g. condition:excellent; repeat for several | | `min_price` | query | integer | In minor units of currency. ≥ 0 | | `max_price` | query | integer | In minor units of currency. ≥ 0 | | `currency` | query | string | Show prices in this currency too (display_price); one of the marketplace's | **Responses:** `200` [SearchOutputBody](#searchoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Get a listing on sale `GET /v1/listings/{id}` · operation `getListing` Listings not on sale are 404 here; sellers see theirs under /v1/me/listings. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | | `currency` | query | string | Show prices in this currency too (display_price); one of the marketplace's | **Responses:** `200` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### SearchOutputBody | Field | Type | Notes | |---|---|---| | `items` * | [ListingBody](#listingbody)[] | | | `next_cursor` | string | | | `total` * | integer | How many listings match | ### ListingBody | Field | Type | Notes | |---|---|---| | `attributes` * | map of any | The marketplace's own fields | | `category_id` | string | | | `closed_at` | string (date-time) | | | `commission_percent` | number (double) | Console only: overrides the marketplace's commission; absent means the default | | `created_at` * | string (date-time) | | | `custody` * | string | Where it is: at the seller's, being collected, or stored. one of `seller`, `collecting`, `stored` | | `delivery` * | [Delivery](#delivery) | How the item can reach the buyer | | `description` * | string | | | `display_price` | [MoneyBody](#moneybody) | The price in the requested currency, converted at the day's rate (approximate; the final amount is set at checkout) | | `favorite_count` * | integer | How many users have saved it | | `favorited` | boolean | Whether the signed-in user has saved it (only with X-User-Session) | | `fulfilment` * | string | Who moves it to the buyer. one of `seller`, `marketplace` | | `id` * | string | | | `images` * | [ImageBody](#imagebody)[] | In display order; the first is the main photo | | `locale` * | string | The language the seller wrote in | | `location` * | [LocationBody](#locationbody) | | | `offer` | [MoneyBody](#moneybody) | The price the marketplace offers (valuation_offered) | | `offer_note` | string | A note with the offer | | `offer_schedule` | [PriceStepBody](#pricestepbody)[] | The price drops offered with the price (valuation_offered) | | `price` | [MoneyBody](#moneybody) | Base price, in the seller's currency | | `price_schedule` | [PriceStepBody](#pricestepbody)[] | The price falls in these steps while on sale | | `published_at` | string (date-time) | | | `review_note` | string | Why the marketplace denied it (denied) | | `seller_blocked` | boolean | The seller is blocked, so the listing is out of the shop | | `seller_id` * | string | | | `shipping_size` | string | Prices delivery from the marketplace's shipping table | | `site` | [SiteRefBody](#siterefbody) | The marketplace location it's stored at or being collected to | | `status` * | string | one of `draft`, `pending_valuation`, `valuation_offered`, `pending_approval`, `published`, `closed`, `denied` | | `stock` * | integer | | | `stored_at` | string (date-time) | Stored since | | `terms` * | map of [TermRefBody](#termrefbody) | By kind, e.g. brand | | `title` * | string | | | `updated_at` * | string (date-time) | | | `wants_storage` | boolean | The seller asked to have it collected and stored | ### Delivery | Field | Type | Notes | |---|---|---| | `buyer_arranged` | boolean | | | `pickup` | boolean | | | `shipping` | [Shipping](#shipping) | | ### MoneyBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | In minor units: 125000 SEK is 1 250,00 kr. ≥ 0 | | `currency` * | string | ISO 4217, one of the marketplace's | ### ImageBody | Field | Type | Notes | |---|---|---| | `height` | integer | | | `id` * | string | | | `status` * | string | Sizes exist once ready. one of `processing`, `ready`, `failed` | | `urls` | map of string | thumb (400 px square), medium (800 px), large (1600 px); cacheable forever | | `width` | integer | | ### LocationBody | Field | Type | Notes | |---|---|---| | `city` | string | | | `country` | string | ISO 3166-1 alpha-2, e.g. SE | | `postal_code` | string | | ### PriceStepBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | The new price, in minor units. ≥ 1 | | `done` | boolean | It has applied | | `months` * | integer | Months after the schedule starts (the listing goes on sale, or the schedule changes while on sale). ≥ 1; ≤ 60 | | `on` | string (date-time) | When it applies (once on sale) | ### SiteRefBody | Field | Type | Notes | |---|---|---| | `buyer_pickup` | boolean | Buyers may collect purchases here | | `city` | string | | | `id` * | string | | | `kind` * | string | one of `showroom`, `warehouse` | | `name` * | string | | ### TermRefBody | Field | Type | Notes | |---|---|---| | `descriptions` | map of string | About the brand, designer…, by locale (e.g. en, sv); on single listings only | | `id` * | string | | | `name` * | string | | | `slug` * | string | | ### Shipping | Field | Type | Notes | |---|---|---| | `additional_item_price` | integer | ≥ 0 | | `price` | integer | For the first item; omit for "ask for a quote". ≥ 0 | --- # Categories and catalogue terms > The category tree and catalogue terms such as brands and designers. The category tree and catalogue terms such as brands and designers. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/catalog-terms`](#list-catalogue-terms-of-a-kind) | List catalogue terms of a kind | | GET | [`/v1/catalog-terms/{kind}/{slug}`](#get-a-catalogue-term-by-its-slug) | Get a catalogue term by its slug | | GET | [`/v1/categories`](#list-the-category-tree) | List the category tree | ## List catalogue terms of a kind `GET /v1/catalog-terms` · operation `listCatalogTerms` E.g. all brands, ordered by name, a page at a time; `q` filters by the start of the name. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `kind` * | query | string | One of the marketplace's term kinds | | `q` | query | string | Only names starting with this. max 200 chars | | `limit` | query | integer | ≥ 1; ≤ 200; default `50` | | `cursor` | query | string | next_cursor from the previous page | | `locale` | query | string | Language for descriptions | | `in_use` | query | boolean | Only terms that listings on sale use, all at once (no paging, q ignored), each with listing_count | **Responses:** `200` [PublicTermPageBody](#publictermpagebody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Get a catalogue term by its slug `GET /v1/catalog-terms/{kind}/{slug}` · operation `getCatalogTerm` E.g. a designer's page: /v1/catalog-terms/designer/poul-henningsen. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `kind` * | path | string | | | `slug` * | path | string | | | `locale` | query | string | Language for the description | **Responses:** `200` [TermBody](#termbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## List the category tree `GET /v1/categories` · operation `listCategories` All categories with parent ids, in one language (falling back to the default). **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `locale` | query | string | Language for names; the marketplace's default when omitted | **Responses:** `200` [ListCategoriesResponse](#listcategoriesresponse) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### PublicTermPageBody | Field | Type | Notes | |---|---|---| | `items` * | [TermBody](#termbody)[] | | | `next_cursor` | string | | ### TermBody | Field | Type | Notes | |---|---|---| | `description` | string | | | `id` * | string | | | `kind` * | string | | | `listing_count` | integer | Listings on sale with this term (with in_use) | | `name` * | string | | | `slug` * | string | | ### ListCategoriesResponse | Field | Type | Notes | |---|---|---| | `items` * | [CategoryBody](#categorybody)[] | | ### CategoryBody | Field | Type | Notes | |---|---|---| | `id` * | string | | | `name` * | string | | | `parent_id` | string | | | `position` * | integer | | | `slug` * | string | | --- # Accounts and sessions > Sign-up, sign-in (password or emailed link), email confirmation, password reset and the profile. Sign-up, sign-in (password or emailed link), email confirmation, password reset and the profile. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | POST | [`/v1/email-verifications`](#confirm-an-email-address-with-the-emailed-link) | Confirm an email address with the emailed link | | GET | [`/v1/me`](#get-the-signed-in-user) | Get the signed-in user | | PATCH | [`/v1/me`](#update-the-signed-in-users-profile) | Update the signed-in user's profile | | POST | [`/v1/me/verification-email`](#send-the-email-confirmation-link-again) | Send the email confirmation link again | | POST | [`/v1/password-resets`](#email-a-password-reset-link) | Email a password reset link | | POST | [`/v1/password-resets/confirm`](#set-a-new-password-with-the-emailed-link) | Set a new password with the emailed link | | POST | [`/v1/sessions`](#sign-in-with-email-and-password) | Sign in with email and password | | DELETE | [`/v1/sessions/current`](#sign-out) | Sign out | | POST | [`/v1/sessions/email-link`](#email-a-sign-in-link) | Email a sign-in link | | POST | [`/v1/sessions/from-link`](#sign-in-with-an-emailed-link) | Sign in with an emailed link | | POST | [`/v1/users`](#create-an-account) | Create an account | ## Confirm an email address with the emailed link `POST /v1/email-verifications` · operation `confirmEmail` **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Body** (`application/json`): [TokenInputBody](#tokeninputbody) **Responses:** `200` [UserBody](#userbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Get the signed-in user `GET /v1/me` · operation `getMe` **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Responses:** `200` [UserBody](#userbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Update the signed-in user's profile `PATCH /v1/me` · operation `updateMe` Only the fields sent change. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Body** (`application/json`): [UpdateMeInputBody](#updatemeinputbody) **Responses:** `200` [UserBody](#userbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Send the email confirmation link again `POST /v1/me/verification-email` · operation `resendVerificationEmail` Links to `<storefront>/<language>/auth/verify-email?token=…`. Does nothing if the address is already confirmed. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Responses:** `202` Accepted · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Email a password reset link `POST /v1/password-resets` · operation `requestPasswordReset` Sends a link to `<storefront>/<language>/auth/reset-password?token=…` if the address has an account. Always answers 202. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Body** (`application/json`): [EmailInputBody](#emailinputbody) **Responses:** `202` Accepted · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Set a new password with the emailed link `POST /v1/password-resets/confirm` · operation `resetPassword` Signs the user out everywhere; sign them in again with the new password. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Body** (`application/json`): [ResetInputBody](#resetinputbody) **Responses:** `204` No Content · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Sign in with email and password `POST /v1/sessions` · operation `signIn` After 10 wrong passwords in 15 minutes the account is locked for the rest of that time (429 with Retry-After). **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Body** (`application/json`): [SignInInputBody](#signininputbody) **Responses:** `201` [SignedInOutputBody](#signedinoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Sign out `DELETE /v1/sessions/current` · operation `signOut` Ends the session sent as X-User-Session. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Responses:** `204` No Content · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Email a sign-in link `POST /v1/sessions/email-link` · operation `requestSignInLink` Sends a link to `<storefront>/<language>/auth/sign-in?token=…` if the address has an account. Always answers 202, so it doesn't reveal who has one. At most 5 emails an hour per address. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Body** (`application/json`): [EmailInputBody](#emailinputbody) **Responses:** `202` Accepted · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Sign in with an emailed link `POST /v1/sessions/from-link` · operation `signInWithLink` The token from the link works once. Following the link also confirms the email address. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Body** (`application/json`): [TokenInputBody](#tokeninputbody) **Responses:** `201` [SignedInOutputBody](#signedinoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Create an account `POST /v1/users` · operation `signUp` Creates the user and signs them in. Call from the shop's server. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Body** (`application/json`): [SignUpInputBody](#signupinputbody) **Responses:** `201` [SignedInOutputBody](#signedinoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### TokenInputBody | Field | Type | Notes | |---|---|---| | `token` * | string | From the emailed link's ?token=. max 200 chars | ### UserBody | Field | Type | Notes | |---|---|---| | `bio` * | string | Public | | `created_at` * | string (date-time) | | | `display_name` * | string | | | `email` * | string (email) | | | `email_verified` * | boolean | | | `first_name` * | string | | | `has_password` * | boolean | | | `id` * | string | | | `last_name` * | string | | | `locale` * | string | The user's language; empty means the marketplace's default | | `phone` * | string | Private | ### UpdateMeInputBody | Field | Type | Notes | |---|---|---| | `bio` | string | max 2000 chars | | `display_name` | string | max 100 chars | | `first_name` | string | max 100 chars | | `last_name` | string | max 100 chars | | `locale` | string | One of the marketplace's languages, or empty for its default | | `phone` | string | max 40 chars | ### EmailInputBody | Field | Type | Notes | |---|---|---| | `email` * | string | max 254 chars | ### ResetInputBody | Field | Type | Notes | |---|---|---| | `password` * | string | max 128 chars | | `token` * | string | From the emailed link's ?token=. max 200 chars | ### SignInInputBody | Field | Type | Notes | |---|---|---| | `email` * | string | max 254 chars | | `password` * | string | max 128 chars | ### SignedInOutputBody | Field | Type | Notes | |---|---|---| | `session` * | [SessionBody](#sessionbody) | | | `user` * | [UserBody](#userbody) | | ### SignUpInputBody | Field | Type | Notes | |---|---|---| | `bio` | string | max 2000 chars | | `display_name` | string | max 100 chars | | `email` * | string (email) | max 254 chars | | `first_name` | string | max 100 chars | | `last_name` | string | max 100 chars | | `locale` | string | One of the marketplace's languages | | `password` | string | Optional: without one, the user signs in with emailed links. max 128 chars | | `phone` | string | max 40 chars | ### SessionBody | Field | Type | Notes | |---|---|---| | `expires_at` * | string (date-time) | Extended while the session is used | | `token` * | string | Send as the X-User-Session header. Shown only here. | --- # Favourites > Listings a user has saved. Listings a user has saved. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/me/favorites`](#list-saved-listings) | List saved listings | | PUT | [`/v1/me/favorites/{id}`](#save-a-listing) | Save a listing | | DELETE | [`/v1/me/favorites/{id}`](#remove-a-saved-listing) | Remove a saved listing | ## List saved listings `GET /v1/me/favorites` · operation `listFavorites` Latest saved first, including listings no longer on sale (check status). **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `limit` | query | integer | ≥ 1; ≤ 100; default `24` | | `cursor` | query | string | next_cursor from the previous page | | `currency` | query | string | Show prices in this currency too (display_price); one of the marketplace's | **Responses:** `200` [SearchOutputBody](#searchoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Save a listing `PUT /v1/me/favorites/{id}` · operation `saveFavorite` Only listings on sale. Saving twice is fine. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Responses:** `204` No Content · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Remove a saved listing `DELETE /v1/me/favorites/{id}` · operation `removeFavorite` **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Responses:** `204` No Content · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### SearchOutputBody | Field | Type | Notes | |---|---|---| | `items` * | [ListingBody](#listingbody)[] | | | `next_cursor` | string | | | `total` * | integer | How many listings match | ### ListingBody | Field | Type | Notes | |---|---|---| | `attributes` * | map of any | The marketplace's own fields | | `category_id` | string | | | `closed_at` | string (date-time) | | | `commission_percent` | number (double) | Console only: overrides the marketplace's commission; absent means the default | | `created_at` * | string (date-time) | | | `custody` * | string | Where it is: at the seller's, being collected, or stored. one of `seller`, `collecting`, `stored` | | `delivery` * | [Delivery](#delivery) | How the item can reach the buyer | | `description` * | string | | | `display_price` | [MoneyBody](#moneybody) | The price in the requested currency, converted at the day's rate (approximate; the final amount is set at checkout) | | `favorite_count` * | integer | How many users have saved it | | `favorited` | boolean | Whether the signed-in user has saved it (only with X-User-Session) | | `fulfilment` * | string | Who moves it to the buyer. one of `seller`, `marketplace` | | `id` * | string | | | `images` * | [ImageBody](#imagebody)[] | In display order; the first is the main photo | | `locale` * | string | The language the seller wrote in | | `location` * | [LocationBody](#locationbody) | | | `offer` | [MoneyBody](#moneybody) | The price the marketplace offers (valuation_offered) | | `offer_note` | string | A note with the offer | | `offer_schedule` | [PriceStepBody](#pricestepbody)[] | The price drops offered with the price (valuation_offered) | | `price` | [MoneyBody](#moneybody) | Base price, in the seller's currency | | `price_schedule` | [PriceStepBody](#pricestepbody)[] | The price falls in these steps while on sale | | `published_at` | string (date-time) | | | `review_note` | string | Why the marketplace denied it (denied) | | `seller_blocked` | boolean | The seller is blocked, so the listing is out of the shop | | `seller_id` * | string | | | `shipping_size` | string | Prices delivery from the marketplace's shipping table | | `site` | [SiteRefBody](#siterefbody) | The marketplace location it's stored at or being collected to | | `status` * | string | one of `draft`, `pending_valuation`, `valuation_offered`, `pending_approval`, `published`, `closed`, `denied` | | `stock` * | integer | | | `stored_at` | string (date-time) | Stored since | | `terms` * | map of [TermRefBody](#termrefbody) | By kind, e.g. brand | | `title` * | string | | | `updated_at` * | string (date-time) | | | `wants_storage` | boolean | The seller asked to have it collected and stored | ### Delivery | Field | Type | Notes | |---|---|---| | `buyer_arranged` | boolean | | | `pickup` | boolean | | | `shipping` | [Shipping](#shipping) | | ### MoneyBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | In minor units: 125000 SEK is 1 250,00 kr. ≥ 0 | | `currency` * | string | ISO 4217, one of the marketplace's | ### ImageBody | Field | Type | Notes | |---|---|---| | `height` | integer | | | `id` * | string | | | `status` * | string | Sizes exist once ready. one of `processing`, `ready`, `failed` | | `urls` | map of string | thumb (400 px square), medium (800 px), large (1600 px); cacheable forever | | `width` | integer | | ### LocationBody | Field | Type | Notes | |---|---|---| | `city` | string | | | `country` | string | ISO 3166-1 alpha-2, e.g. SE | | `postal_code` | string | | ### PriceStepBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | The new price, in minor units. ≥ 1 | | `done` | boolean | It has applied | | `months` * | integer | Months after the schedule starts (the listing goes on sale, or the schedule changes while on sale). ≥ 1; ≤ 60 | | `on` | string (date-time) | When it applies (once on sale) | ### SiteRefBody | Field | Type | Notes | |---|---|---| | `buyer_pickup` | boolean | Buyers may collect purchases here | | `city` | string | | | `id` * | string | | | `kind` * | string | one of `showroom`, `warehouse` | | `name` * | string | | ### TermRefBody | Field | Type | Notes | |---|---|---| | `descriptions` | map of string | About the brand, designer…, by locale (e.g. en, sv); on single listings only | | `id` * | string | | | `name` * | string | | | `slug` * | string | | ### Shipping | Field | Type | Notes | |---|---|---| | `additional_item_price` | integer | ≥ 0 | | `price` | integer | For the first item; omit for "ask for a quote". ≥ 0 | --- # Selling > A seller's own listings: drafts, photos, publishing, valuations and storage. A seller's own listings: drafts, photos, publishing, valuations and storage. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/me/listings`](#list-the-signed-in-sellers-listings) | List the signed-in seller's listings | | POST | [`/v1/me/listings`](#start-a-listing) | Start a listing | | GET | [`/v1/me/listings/{id}`](#get-one-of-the-sellers-listings) | Get one of the seller's listings | | PUT | [`/v1/me/listings/{id}`](#replace-a-listings-content) | Replace a listing's content | | DELETE | [`/v1/me/listings/{id}`](#delete-a-draft) | Delete a draft | | POST | [`/v1/me/listings/{id}/close`](#take-a-listing-off-sale) | Take a listing off sale | | POST | [`/v1/me/listings/{id}/images`](#add-a-photo) | Add a photo | | PUT | [`/v1/me/listings/{id}/images/order`](#set-the-order-of-the-photos) | Set the order of the photos | | DELETE | [`/v1/me/listings/{id}/images/{image_id}`](#remove-a-photo) | Remove a photo | | POST | [`/v1/me/listings/{id}/offer/accept`](#accept-the-marketplaces-price) | Accept the marketplace's price | | POST | [`/v1/me/listings/{id}/offer/counter`](#answer-the-marketplaces-price-with-your-own) | Answer the marketplace's price with your own | | POST | [`/v1/me/listings/{id}/publish`](#put-a-listing-on-sale) | Put a listing on sale | ## List the signed-in seller's listings `GET /v1/me/listings` · operation `listMyListings` Every status, latest change first. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `limit` | query | integer | ≥ 1; ≤ 100; default `24` | | `cursor` | query | string | next_cursor from the previous page | **Responses:** `200` [PageOutputBody](#pageoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Start a listing `POST /v1/me/listings` · operation `createMyListing` Saved as a draft; it may be incomplete until it's published. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Body** (`application/json`): [ListingInputBody](#listinginputbody) **Responses:** `201` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Get one of the seller's listings `GET /v1/me/listings/{id}` · operation `getMyListing` **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Responses:** `200` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Replace a listing's content `PUT /v1/me/listings/{id}` · operation `updateMyListing` A listing on sale must stay complete. Closed listings can't be edited. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Body** (`application/json`): [ListingInputBody](#listinginputbody) **Responses:** `200` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Delete a draft `DELETE /v1/me/listings/{id}` · operation `deleteMyListing` Listings that were ever on sale are closed instead. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Responses:** `204` No Content · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Take a listing off sale `POST /v1/me/listings/{id}/close` · operation `closeMyListing` **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Responses:** `200` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Add a photo `POST /v1/me/listings/{id}/images` · operation `uploadListingImage` Send the photo as the body (JPEG, PNG or WebP, up to 20 MB and 50 megapixels). It's "processing" until its sizes are made, usually within seconds. At most 20 per listing. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Body** (`image/jpeg`): the file's bytes. **Responses:** `202` [ImageBody](#imagebody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Set the order of the photos `PUT /v1/me/listings/{id}/images/order` · operation `reorderListingImages` Send every image id once; the first becomes the main photo. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Body** (`application/json`): [ReorderListingImagesRequest](#reorderlistingimagesrequest) **Responses:** `200` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Remove a photo `DELETE /v1/me/listings/{id}/images/{image_id}` · operation `deleteListingImage` **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | | `image_id` * | path | string | | **Responses:** `204` No Content · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Accept the marketplace's price `POST /v1/me/listings/{id}/offer/accept` · operation `acceptMyListingOffer` The offer becomes the price and the listing waits for approval. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Responses:** `200` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Answer the marketplace's price with your own `POST /v1/me/listings/{id}/offer/counter` · operation `counterMyListingOffer` The seller's price is set and the listing waits for approval; the team approves it, denies it or makes a new offer. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Body** (`application/json`): [CounterMyListingOfferRequest](#countermylistingofferrequest) **Responses:** `200` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Put a listing on sale `POST /v1/me/listings/{id}/publish` · operation `publishMyListing` From draft or closed. Goes to approval first if the marketplace requires it, with only the basics checked (title, photos, country, price), since the team completes the listing. With ask_valuation (where the marketplace offers valuations) no price is needed: the team offers one, which the seller accepts or answers with their own. If anything is missing, the 422 lists every problem. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Body** (`application/json`): [PublishMyListingRequest](#publishmylistingrequest) **Responses:** `200` [ListingBody](#listingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### PageOutputBody | Field | Type | Notes | |---|---|---| | `items` * | [ListingBody](#listingbody)[] | | | `next_cursor` | string | | ### ListingInputBody | Field | Type | Notes | |---|---|---| | `attributes` | map of any | | | `category_id` | string | | | `delivery` | [Delivery](#delivery) | At least one method is needed to publish | | `description` | string | max 10000 chars | | `locale` | string | The marketplace's default when omitted | | `location` | [LocationBody](#locationbody) | | | `price` | [MoneyBody](#moneybody) | | | `shipping_size` | string | Operators: one of the marketplace's shipping sizes, which prices delivery; "" for none, omitted to leave it | | `stock` | integer | 1 when omitted | | `terms` | map of string | Catalogue term ids by kind, e.g. {"brand": "…"} | | `title` | string | max 140 chars | ### ListingBody | Field | Type | Notes | |---|---|---| | `attributes` * | map of any | The marketplace's own fields | | `category_id` | string | | | `closed_at` | string (date-time) | | | `commission_percent` | number (double) | Console only: overrides the marketplace's commission; absent means the default | | `created_at` * | string (date-time) | | | `custody` * | string | Where it is: at the seller's, being collected, or stored. one of `seller`, `collecting`, `stored` | | `delivery` * | [Delivery](#delivery) | How the item can reach the buyer | | `description` * | string | | | `display_price` | [MoneyBody](#moneybody) | The price in the requested currency, converted at the day's rate (approximate; the final amount is set at checkout) | | `favorite_count` * | integer | How many users have saved it | | `favorited` | boolean | Whether the signed-in user has saved it (only with X-User-Session) | | `fulfilment` * | string | Who moves it to the buyer. one of `seller`, `marketplace` | | `id` * | string | | | `images` * | [ImageBody](#imagebody)[] | In display order; the first is the main photo | | `locale` * | string | The language the seller wrote in | | `location` * | [LocationBody](#locationbody) | | | `offer` | [MoneyBody](#moneybody) | The price the marketplace offers (valuation_offered) | | `offer_note` | string | A note with the offer | | `offer_schedule` | [PriceStepBody](#pricestepbody)[] | The price drops offered with the price (valuation_offered) | | `price` | [MoneyBody](#moneybody) | Base price, in the seller's currency | | `price_schedule` | [PriceStepBody](#pricestepbody)[] | The price falls in these steps while on sale | | `published_at` | string (date-time) | | | `review_note` | string | Why the marketplace denied it (denied) | | `seller_blocked` | boolean | The seller is blocked, so the listing is out of the shop | | `seller_id` * | string | | | `shipping_size` | string | Prices delivery from the marketplace's shipping table | | `site` | [SiteRefBody](#siterefbody) | The marketplace location it's stored at or being collected to | | `status` * | string | one of `draft`, `pending_valuation`, `valuation_offered`, `pending_approval`, `published`, `closed`, `denied` | | `stock` * | integer | | | `stored_at` | string (date-time) | Stored since | | `terms` * | map of [TermRefBody](#termrefbody) | By kind, e.g. brand | | `title` * | string | | | `updated_at` * | string (date-time) | | | `wants_storage` | boolean | The seller asked to have it collected and stored | ### ImageBody | Field | Type | Notes | |---|---|---| | `height` | integer | | | `id` * | string | | | `status` * | string | Sizes exist once ready. one of `processing`, `ready`, `failed` | | `urls` | map of string | thumb (400 px square), medium (800 px), large (1600 px); cacheable forever | | `width` | integer | | ### ReorderListingImagesRequest | Field | Type | Notes | |---|---|---| | `image_ids` * | string[] | | ### CounterMyListingOfferRequest | Field | Type | Notes | |---|---|---| | `amount` * | integer | In minor units, in the offer's currency. ≥ 1 | ### PublishMyListingRequest | Field | Type | Notes | |---|---|---| | `ask_valuation` | boolean | Ask the marketplace to value it instead of setting a price | | `wants_storage` | boolean | Ask the marketplace to collect and store it until it sells (otherwise it stays with the seller) | ### Delivery | Field | Type | Notes | |---|---|---| | `buyer_arranged` | boolean | | | `pickup` | boolean | | | `shipping` | [Shipping](#shipping) | | ### LocationBody | Field | Type | Notes | |---|---|---| | `city` | string | | | `country` | string | ISO 3166-1 alpha-2, e.g. SE | | `postal_code` | string | | ### MoneyBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | In minor units: 125000 SEK is 1 250,00 kr. ≥ 0 | | `currency` * | string | ISO 4217, one of the marketplace's | ### PriceStepBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | The new price, in minor units. ≥ 1 | | `done` | boolean | It has applied | | `months` * | integer | Months after the schedule starts (the listing goes on sale, or the schedule changes while on sale). ≥ 1; ≤ 60 | | `on` | string (date-time) | When it applies (once on sale) | ### SiteRefBody | Field | Type | Notes | |---|---|---| | `buyer_pickup` | boolean | Buyers may collect purchases here | | `city` | string | | | `id` * | string | | | `kind` * | string | one of `showroom`, `warehouse` | | `name` * | string | | ### TermRefBody | Field | Type | Notes | |---|---|---| | `descriptions` | map of string | About the brand, designer…, by locale (e.g. en, sv); on single listings only | | `id` * | string | | | `name` * | string | | | `slug` * | string | | ### Shipping | Field | Type | Notes | |---|---|---| | `additional_item_price` | integer | ≥ 0 | | `price` | integer | For the first item; omit for "ask for a quote". ≥ 0 | --- # Orders > Quotes, placing orders, the payment, delivery times and moving orders on. Quotes, placing orders, the payment, delivery times and moving orders on. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/me/orders`](#list-the-signed-in-users-orders) | List the signed-in user's orders | | POST | [`/v1/me/orders`](#buy-a-listing) | Buy a listing | | POST | [`/v1/me/orders/quote`](#price-an-order-without-placing-it) | Price an order without placing it | | GET | [`/v1/me/orders/{id}`](#get-one-of-the-users-orders) | Get one of the user's orders | | GET | [`/v1/me/orders/{id}/delivery-booking`](#when-the-delivery-comes) | When the delivery comes | | PUT | [`/v1/me/orders/{id}/delivery-booking`](#choose-when-the-delivery-comes) | Choose when the delivery comes | | POST | [`/v1/me/orders/{id}/transitions/{transition}`](#move-an-order-on) | Move an order on | ## List the signed-in user's orders `GET /v1/me/orders` · operation `listMyOrders` As buyer (purchases) or seller (sales), newest first. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `state` | query | string | | | `limit` | query | integer | ≥ 1; ≤ 100; default `25` | | `offset` | query | integer | ≥ 0; ≤ 10000 | | `as` | query | string | one of `buyer`, `seller`; default `"buyer"` | **Responses:** `200` [OrderListOutputBody](#orderlistoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Buy a listing `POST /v1/me/orders` · operation `placeOrder` Reserves the stock and starts the payment. Pay within 15 minutes with payment.client_secret, or the order expires and the item is released. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Body** (`application/json`): [PlaceOrderRequest](#placeorderrequest) **Responses:** `201` [PlaceOrderResponse](#placeorderresponse) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Price an order without placing it `POST /v1/me/orders/quote` · operation `quoteOrder` What the buyer would pay, line by line, e.g. to show a voucher's effect at checkout. Placing the order prices it the same way. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Body** (`application/json`): [QuoteRequest](#quoterequest) **Responses:** `200` [QuoteBody](#quotebody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Get one of the user's orders `GET /v1/me/orders/{id}` · operation `getMyOrder` Its buyer or its seller. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Responses:** `200` [OrderBody](#orderbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## When the delivery comes `GET /v1/me/orders/{id}/delivery-booking` · operation `getMyDeliveryBooking` The buyer's booked delivery time and the open slots. 409 when the delivery isn't booked by the buyer (not paid yet, collected, or arranged by the carrier). **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Responses:** `200` [DeliveryBookingBody](#deliverybookingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Choose when the delivery comes `PUT /v1/me/orders/{id}/delivery-booking` · operation `bookMyDelivery` One of the open slots; it can be changed until the lead time. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Body** (`application/json`): [SlotInputBody](#slotinputbody) **Responses:** `200` [DeliveryBookingBody](#deliverybookingbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Move an order on `POST /v1/me/orders/{id}/transitions/{transition}` · operation `takeMyOrderTransition` One of the order's available_transitions for the user, e.g. mark-delivered (seller, with data.tracking_number) or mark-received (buyer). **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | | `transition` * | path | string | | **Body** (`application/json`): [TransitionInputBody](#transitioninputbody) **Responses:** `200` [OrderBody](#orderbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### OrderListOutputBody | Field | Type | Notes | |---|---|---| | `items` * | [OrderBody](#orderbody)[] | | ### PlaceOrderRequest | Field | Type | Notes | |---|---|---| | `currency` | string | What the buyer pays in, one of the marketplace's; the listing's when omitted | | `delivery` | [DeliveryBody](#deliverybody) | | | `listing_id` * | string | | | `quantity` | integer | 1 when omitted. ≥ 1; ≤ 100 | | `voucher_code` | string | max 40 chars | ### PlaceOrderResponse | Field | Type | Notes | |---|---|---| | `order` * | [OrderBody](#orderbody) | | | `payment` * | [PaymentStruct](#paymentstruct) | | ### QuoteRequest | Field | Type | Notes | |---|---|---| | `currency` | string | What the buyer pays in; the listing's when omitted | | `delivery` | [DeliveryBody](#deliverybody) | | | `listing_id` * | string | | | `quantity` | integer | 1 when omitted. ≥ 1; ≤ 100 | | `voucher_code` | string | max 40 chars | ### QuoteBody | Field | Type | Notes | |---|---|---| | `currency` * | string | | | `line_items` * | [LineItemBody](#lineitembody)[] | | | `total` * | integer | | ### OrderBody | Field | Type | Notes | |---|---|---| | `available_transitions` * | string[] | What the caller can do now, e.g. mark-delivered | | `buyer_id` * | string | | | `chargeback` | [ChargebackBody](#chargebackbody) | | | `created_at` * | string (date-time) | | | `currency` * | string | | | `delivery` * | map of string | Tracking number, carrier… as given when marked delivered | | `history` * | [EventBody](#eventbody)[] | | | `id` * | string | | | `line_items` * | [LineItemBody](#lineitembody)[] | | | `listing_id` * | string | | | `payout` | [PayoutBody](#payoutbody) | | | `quantity` * | integer | | | `refunded_outside` | integer | Refunded in Stripe outside the order, in the order's currency | | `seller_id` * | string | | | `state` * | string | pending_payment, purchased, delivered, received, disputed, canceled, payment_expired | | `state_entered_at` * | string (date-time) | | | `total` * | integer | In minor units of currency | ### DeliveryBookingBody | Field | Type | Notes | |---|---|---| | `booked` | [SlotBody](#slotbody) | | | `changeable` * | boolean | The booked time can still be changed | | `slots` * | [SlotBody](#slotbody)[] | | ### SlotInputBody | Field | Type | Notes | |---|---|---| | `date` * | string | YYYY-MM-DD | | `window` * | string | max 40 chars | ### TransitionInputBody | Field | Type | Notes | |---|---|---| | `data` | map of string | E.g. tracking_number and carrier when marking delivered | ### DeliveryBody | Field | Type | Notes | |---|---|---| | `address` | [Address](#address) | Where to ship to (shipping only) | | `method` | string | One of the listing's; may be omitted when it offers only one. collect: from the marketplace location it's stored at. one of `shipping`, `pickup`, `buyer_arranged`, `collect` | | `option` | string | Shipping with the marketplace: one of its delivery options (the first when omitted) | ### PaymentStruct | Field | Type | Notes | |---|---|---| | `client_secret` * | string | For the payment form | ### LineItemBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | | | `code` * | string | | | `label` * | string | | | `quantity` * | integer | | | `unit_price` * | integer | | ### ChargebackBody | Field | Type | Notes | |---|---|---| | `id` * | string | | | `reason` * | string | | | `status` * | string | Stripe's dispute status, e.g. needs_response, under_review, won, lost | ### EventBody | Field | Type | Notes | |---|---|---| | `actor` * | string | one of `buyer`, `seller`, `operator`, `system` | | `at` * | string (date-time) | | | `data` | map of string | | | `from` * | string | | | `to` * | string | | | `transition` * | string | | ### PayoutBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | What the seller is paid | | `commission` * | integer | | | `currency` * | string | | | `status` * | string | pending until the buyer has the item. one of `pending`, `paid`, `waiting_for_payout_details`, `held_for_chargeback` | | `storage_fee` | integer | Taken for storage, per started month | | `storage_months` | integer | | | `subtotal` * | integer | | ### SlotBody | Field | Type | Notes | |---|---|---| | `date` * | string | YYYY-MM-DD | | `full` | boolean | | | `label` * | string | e.g. 08–12 | | `window` * | string | | ### Address | Field | Type | Notes | |---|---|---| | `city` | string | | | `country` * | string | ISO 3166, e.g. SE | | `line1` | string | | | `line2` | string | | | `name` | string | | | `phone` | string | | | `postal_code` * | string | | --- # Payouts > The seller's payout setup with Stripe. The seller's payout setup with Stripe. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/me/payouts`](#how-far-the-sellers-payout-setup-has-come) | How far the seller's payout setup has come | | POST | [`/v1/me/payouts/session`](#start-or-continue-the-sellers-payout-setup) | Start or continue the seller's payout setup | ## How far the seller's payout setup has come `GET /v1/me/payouts` · operation `getMyPayouts` **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Responses:** `200` [PayoutSetupBody](#payoutsetupbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Start or continue the seller's payout setup `POST /v1/me/payouts/session` · operation `startMyPayoutSetup` Returns a client secret for Stripe's embedded onboarding component (account-onboarding). Configure it with collectionOptions {fields: 'currently_due', futureRequirements: 'omit'} to ask only for what's required now. Ask the seller first whether they sell as a private person or a company; the form then asks only that kind's questions. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Body** (`application/json`): [StartMyPayoutSetupRequest](#startmypayoutsetuprequest) **Responses:** `200` [StartMyPayoutSetupResponse](#startmypayoutsetupresponse) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### PayoutSetupBody | Field | Type | Notes | |---|---|---| | `due` | string[] | What Stripe still needs, when action is required | | `status` * | string | one of `none`, `action_required`, `pending`, `ready` | ### StartMyPayoutSetupRequest | Field | Type | Notes | |---|---|---| | `business_type` | string | Private person (default) or company; used the first time. one of `individual`, `company` | | `country` * | string | Where the seller lives (ISO 3166, e.g. SE); used the first time | ### StartMyPayoutSetupResponse | Field | Type | Notes | |---|---|---| | `client_secret` * | string | | --- # Messages > Conversations between buyers, sellers and the marketplace. Conversations between buyers, sellers and the marketplace. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/me/conversations`](#the-signed-in-users-conversations) | The signed-in user's conversations | | POST | [`/v1/me/conversations`](#message-the-seller-the-buyer-or-the-marketplace) | Message the seller, the buyer or the marketplace | | GET | [`/v1/me/conversations/{id}/messages`](#read-a-conversation) | Read a conversation | | POST | [`/v1/me/conversations/{id}/messages`](#reply-in-a-conversation) | Reply in a conversation | ## The signed-in user's conversations `GET /v1/me/conversations` · operation `listMyConversations` Latest first, with the last message and how many are unread. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `limit` | query | integer | ≥ 1; ≤ 100; default `50` | | `offset` | query | integer | ≥ 0 | **Responses:** `200` [ConversationListOutputBody](#conversationlistoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Message the seller, the buyer or the marketplace `POST /v1/me/conversations` · operation `startConversation` About a listing (a question before buying), an order (its buyer or seller), or support (the marketplace). Continues the existing conversation if there is one. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Body** (`application/json`): [StartConversationRequest](#startconversationrequest) **Responses:** `201` [StartConversationResponse](#startconversationresponse) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Read a conversation `GET /v1/me/conversations/{id}/messages` · operation `readMyConversation` Oldest first; marks them read. **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | | `after` | query | integer | Only messages after this id. ≥ 0 | | `limit` | query | integer | ≥ 1; ≤ 200; default `100` | **Responses:** `200` [MessagesOutputBody](#messagesoutputbody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Reply in a conversation `POST /v1/me/conversations/{id}/messages` · operation `sendMessage` **Auth:** your secret key (`Authorization: Bearer sk_…`) and the user's session (`X-User-Session: …`), from your server **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | **Body** (`application/json`): [MessageInput](#messageinput) **Responses:** `201` [MessageBody](#messagebody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### ConversationListOutputBody | Field | Type | Notes | |---|---|---| | `items` * | [ConversationBody](#conversationbody)[] | | ### StartConversationRequest | Field | Type | Notes | |---|---|---| | `body` * | string | max 5000 chars | | `listing_id` | string | | | `order_id` | string | | | `support` | boolean | To the marketplace | ### StartConversationResponse | Field | Type | Notes | |---|---|---| | `conversation` * | [ConversationBody](#conversationbody) | | | `message` * | [MessageBody](#messagebody) | | ### MessagesOutputBody | Field | Type | Notes | |---|---|---| | `items` * | [MessageBody](#messagebody)[] | | ### MessageInput | Field | Type | Notes | |---|---|---| | `body` * | string | max 5000 chars | ### MessageBody | Field | Type | Notes | |---|---|---| | `at` * | string (date-time) | | | `body` * | string | | | `from_marketplace` * | boolean | Written by the marketplace (support) | | `id` * | integer | | | `sender_id` | string | The user who wrote it | ### ConversationBody | Field | Type | Notes | |---|---|---| | `id` * | string | | | `kind` * | string | one of `listing`, `order`, `support` | | `last_message` | string | | | `last_message_at` * | string (date-time) | | | `listing_id` | string | | | `order_id` | string | | | `unread` * | integer | | | `user_a` * | string | The buyer, or the user asking for support | | `user_b` | string | The seller (not in support conversations) | --- # Pages > Content pages the marketplace edits in the console (the home page, About…), built from blocks and filled in with live data. Content pages the marketplace edits in the console (the home page, About…), built from blocks and filled in with live data. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | GET | [`/v1/media/{id}/{file}`](#get-a-page-image) | Get a page image | | GET | [`/v1/pages`](#list-the-published-pages) | List the published pages | | GET | [`/v1/pages/{slug}`](#get-a-published-page) | Get a published page | | GET | [`/v1/pages/{slug}/preview`](#preview-a-pages-draft) | Preview a page's draft | ## Get a page image `GET /v1/media/{id}/{file}` · operation `getMedia` Public, for <img> tags: no key needed. `file` is thumb.jpg, medium.jpg, large.jpg or xlarge.jpg. Never changes, so it's cached for a year. **Auth:** None. **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `id` * | path | string | | | `file` * | path | string | one of `thumb.jpg`, `medium.jpg`, `large.jpg`, `xlarge.jpg` | **Responses:** `200` OK · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## List the published pages `GET /v1/pages` · operation `listPages` Their addresses and languages, e.g. for a sitemap. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Responses:** `200` [ListPagesResponse](#listpagesresponse) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Get a published page `GET /v1/pages/{slug}` · operation `getPage` The page at `slug` (`home` is the front page) in `locale`, falling back to the marketplace's default language, with its blocks filled in: listings (on sale, prices in `currency`), images in sizes, categories, the showroom. Blocks with nothing to show are left out. 404 until the page is published. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `slug` * | path | string | max 80 chars | | `locale` | query | string | max 35 chars | | `currency` | query | string | Show listing prices in this currency too (display_price); one of the marketplace's | **Responses:** `200` [PageBody](#pagebody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Preview a page's draft `GET /v1/pages/{slug}/preview` · operation `previewPage` The draft, filled in like a published page, for the console's Preview link: the shop opens `/<slug>?preview=<token>` and passes the token here. Don't cache it, and don't let search engines index it. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Parameters** | Name | In | Type | Notes | |---|---|---|---| | `slug` * | path | string | max 80 chars | | `token` * | query | string | max 100 chars | | `currency` | query | string | | **Responses:** `200` [PageBody](#pagebody) · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### ListPagesResponse | Field | Type | Notes | |---|---|---| | `items` * | [PublishedPageBody](#publishedpagebody)[] | | ### PageBody | Field | Type | Notes | |---|---|---| | `blocks` * | [Block](#block)[] | In order, filled in with live data; render each by its type and skip types you don't know | | `description` * | string | For search results and link previews | | `locale` * | string | | | `published_at` | string (date-time) | | | `share_image` | [ImageRef](#imageref) | For link previews (og:image) | | `slug` * | string | home is the front page | | `title` * | string | For the browser tab and search results | ### PublishedPageBody | Field | Type | Notes | |---|---|---| | `locale` * | string | | | `published_at` * | string (date-time) | | | `slug` * | string | | ### Block | Field | Type | Notes | |---|---|---| | `body` | string | Markdown (text) | | `button_label` | string | newsletter | | `buttons` | [Link](#link)[] | Up to 2 (hero, cta) | | `eyebrow` | string | A short line above the heading (hero, image_text) | | `heading` | string | hero, listing_grid, banner, category_tiles, image_text, featured_listing, cta, newsletter, showroom, faq | | `id` * | string | Stable id within the page (1-40 letters, digits, - or _) | | `image` | [ImageRef](#imageref) | The image, in sizes | | `image_id` | string | An uploaded image (hero with visual image, banner, image_text, showroom; cta: optional, behind the text) | | `image_side` | string | image_text. one of `left`, `right` | | `items` | [Item](#item)[] | promises: 1 to 6 | | `limit` | integer | listing_grid: how many to show (1-24, default 8) | | `link` | [Link](#link) | Where the block leads (listing_grid "view all", banner, image_text) | | `links` | [Link](#link)[] | 1 to 8 (links) | | `listing` | [ListingBody](#listingbody) | hero (visual listing or newest), featured_listing | | `listing_id` | string | hero (visual listing), featured_listing | | `listing_ids` | string[] | listing_grid with source picked, in order | | `listings` | [ListingBody](#listingbody)[] | listing_grid: the listings on sale to show, in order | | `location` | [LocationRef](#locationref) | showroom | | `location_id` | string | showroom: one of the marketplace's locations | | `phone_image` | [ImageRef](#imageref) | | | `phone_image_id` | string | banner: the image for phones (the main one otherwise) | | `questions` | [Question](#question)[] | faq: 1 to 40 | | `rule` | [Rule](#rule) | listing_grid with source rule | | `search` | boolean | hero: show a search box | | `source` | string | listing_grid: hand-picked listings or a rule. one of `picked`, `rule` | | `text` | string | Plain text (hero, banner, image_text, featured_listing note, cta, newsletter, showroom) | | `tiles` | [Tile](#tile)[] | category_tiles: 1 to 12 | | `tone` | string | cta: the band's colour. one of `brand`, `light`, `dark` | | `type` * | string | one of `hero`, `links`, `listing_grid`, `banner`, `category_tiles`, `image_text`, `featured_listing`, `cta`, `newsletter`, `promises`, `showroom`, `text`, `faq` | | `visual` | string | hero: what's beside the text; newest is the newest listing on sale. one of `image`, `listing`, `newest`, `none` | | `when_sold` | string | featured_listing: hide the block once it sells, or keep showing it (as sold). one of `hide`, `show` | ### ImageRef | Field | Type | Notes | |---|---|---| | `alt` * | string | | | `height` * | integer | | | `id` * | string | | | `urls` * | map of string | thumb (400 px square), medium (800 px), large (1600 px), xlarge (2400 px); cacheable forever | | `width` * | integer | | ### Link | Field | Type | Notes | |---|---|---| | `href` * | string | A path in the shop (/shop?sort=newest) or an https:// address. max 500 chars | | `label` * | string | max 60 chars | | `style` | string | Buttons: the first is usually primary. one of `primary`, `secondary` | ### Item | Field | Type | Notes | |---|---|---| | `text` | string | | | `title` * | string | | ### ListingBody | Field | Type | Notes | |---|---|---| | `attributes` * | map of any | The marketplace's own fields | | `category_id` | string | | | `closed_at` | string (date-time) | | | `commission_percent` | number (double) | Console only: overrides the marketplace's commission; absent means the default | | `created_at` * | string (date-time) | | | `custody` * | string | Where it is: at the seller's, being collected, or stored. one of `seller`, `collecting`, `stored` | | `delivery` * | [Delivery](#delivery) | How the item can reach the buyer | | `description` * | string | | | `display_price` | [MoneyBody](#moneybody) | The price in the requested currency, converted at the day's rate (approximate; the final amount is set at checkout) | | `favorite_count` * | integer | How many users have saved it | | `favorited` | boolean | Whether the signed-in user has saved it (only with X-User-Session) | | `fulfilment` * | string | Who moves it to the buyer. one of `seller`, `marketplace` | | `id` * | string | | | `images` * | [ImageBody](#imagebody)[] | In display order; the first is the main photo | | `locale` * | string | The language the seller wrote in | | `location` * | [LocationBody](#locationbody) | | | `offer` | [MoneyBody](#moneybody) | The price the marketplace offers (valuation_offered) | | `offer_note` | string | A note with the offer | | `offer_schedule` | [PriceStepBody](#pricestepbody)[] | The price drops offered with the price (valuation_offered) | | `price` | [MoneyBody](#moneybody) | Base price, in the seller's currency | | `price_schedule` | [PriceStepBody](#pricestepbody)[] | The price falls in these steps while on sale | | `published_at` | string (date-time) | | | `review_note` | string | Why the marketplace denied it (denied) | | `seller_blocked` | boolean | The seller is blocked, so the listing is out of the shop | | `seller_id` * | string | | | `shipping_size` | string | Prices delivery from the marketplace's shipping table | | `site` | [SiteRefBody](#siterefbody) | The marketplace location it's stored at or being collected to | | `status` * | string | one of `draft`, `pending_valuation`, `valuation_offered`, `pending_approval`, `published`, `closed`, `denied` | | `stock` * | integer | | | `stored_at` | string (date-time) | Stored since | | `terms` * | map of [TermRefBody](#termrefbody) | By kind, e.g. brand | | `title` * | string | | | `updated_at` * | string (date-time) | | | `wants_storage` | boolean | The seller asked to have it collected and stored | ### LocationRef | Field | Type | Notes | |---|---|---| | `buyer_pickup` * | boolean | | | `city` * | string | | | `country` * | string | | | `kind` * | string | | | `line1` * | string | | | `name` * | string | | | `opening_hours` * | string | | | `postal_code` * | string | | ### Question | Field | Type | Notes | |---|---|---| | `answer` * | string | | | `question` * | string | | ### Rule | Field | Type | Notes | |---|---|---| | `category_id` | string | | | `sort` | string | one of `newest`, `price_asc`, `price_desc`, `popular` | | `term_ids` | string[] | | ### Tile | Field | Type | Notes | |---|---|---| | `category` | [CategoryRef](#categoryref) | | | `category_id` * | string | | | `image` | [ImageRef](#imageref) | | | `image_id` | string | | | `label` | string | Shown instead of the category's name | ### Delivery | Field | Type | Notes | |---|---|---| | `buyer_arranged` | boolean | | | `pickup` | boolean | | | `shipping` | [Shipping](#shipping) | | ### MoneyBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | In minor units: 125000 SEK is 1 250,00 kr. ≥ 0 | | `currency` * | string | ISO 4217, one of the marketplace's | ### ImageBody | Field | Type | Notes | |---|---|---| | `height` | integer | | | `id` * | string | | | `status` * | string | Sizes exist once ready. one of `processing`, `ready`, `failed` | | `urls` | map of string | thumb (400 px square), medium (800 px), large (1600 px); cacheable forever | | `width` | integer | | ### LocationBody | Field | Type | Notes | |---|---|---| | `city` | string | | | `country` | string | ISO 3166-1 alpha-2, e.g. SE | | `postal_code` | string | | ### PriceStepBody | Field | Type | Notes | |---|---|---| | `amount` * | integer | The new price, in minor units. ≥ 1 | | `done` | boolean | It has applied | | `months` * | integer | Months after the schedule starts (the listing goes on sale, or the schedule changes while on sale). ≥ 1; ≤ 60 | | `on` | string (date-time) | When it applies (once on sale) | ### SiteRefBody | Field | Type | Notes | |---|---|---| | `buyer_pickup` | boolean | Buyers may collect purchases here | | `city` | string | | | `id` * | string | | | `kind` * | string | one of `showroom`, `warehouse` | | `name` * | string | | ### TermRefBody | Field | Type | Notes | |---|---|---| | `descriptions` | map of string | About the brand, designer…, by locale (e.g. en, sv); on single listings only | | `id` * | string | | | `name` * | string | | | `slug` * | string | | ### CategoryRef | Field | Type | Notes | |---|---|---| | `id` * | string | | | `name` * | string | | | `slug` * | string | | ### Shipping | Field | Type | Notes | |---|---|---| | `additional_item_price` | integer | ≥ 0 | | `price` | integer | For the first item; omit for "ask for a quote". ≥ 0 | --- # Newsletter > Newsletter sign-ups from the shop. Newsletter sign-ups from the shop. Generated from the [OpenAPI spec](https://dev.mongrl.com/openapi.json). Each endpoint says which key it needs; see [Authentication](https://dev.mongrl.com/guides/authentication.md). | Method | Path | What it does | |---|---|---| | POST | [`/v1/newsletter/subscribers`](#sign-up-for-the-newsletter) | Sign up for the newsletter | ## Sign up for the newsletter `POST /v1/newsletter/subscribers` · operation `subscribeNewsletter` Always answers 202 for a valid address, so it never reveals who's signed up; signing up again changes nothing. mongrl keeps the list and adds new addresses to the marketplace's Resend contacts. Call from the shop's server. **Auth:** a secret or publishable key (`Authorization: Bearer sk_…` or `pk_…`) **Body** (`application/json`): [SubscribeNewsletterRequest](#subscribenewsletterrequest) **Responses:** `202` Accepted · `default` [a problem](https://dev.mongrl.com/reference/conventions.md#errors) ## Schemas Fields marked * are always present in responses and required in requests. ### SubscribeNewsletterRequest | Field | Type | Notes | |---|---|---| | `email` * | string (email) | max 254 chars | | `first_name` | string | max 100 chars | | `locale` | string | max 35 chars | | `source` | string | Where they signed up, e.g. home or footer. max 60 chars |