Skip to content
MintGuides

Go live

API

Endpoints for your own website, app or server — generated from your models or made one by one, each with who may call it, rules on its records, its fields, filters and pages; server keys for your own server.

Open API
On this page

What it is

Your own website, app or server reading and writing your models — at addresses you choose, with rules you set.

A product catalogue on your shop’s site, a booking form that saves straight into Bookings, an app where customers see their own orders, your till sending in the day’s sales. Each of those is an endpoint: an address that does one thing with one model’s records — list them, read one, create, update or delete — for whoever you allow. Nothing is public until you make an endpoint for it.

The project key

Every call names your project with its key: ?apiKey=<project key>. Without it the API answers 401.

Each project has one project key, made with the project. Find it on API → Keys (also on the API Overview and in Project settings), with a copy button. Add it to every address — or send it as the X-Api-Key header:

GET https://mint-c5e1255519fc.herokuapp.com/public/api/products?apiKey=mint_pk_…

curl 'https://mint-c5e1255519fc.herokuapp.com/public/api/products' -H 'X-Api-Key: mint_pk_…'

The project key is meant for your website’s code, where anyone can read it: it only says which project a call is for. What a call may do is still each endpoint’s choice — anyone, signed-in customers, or a server key. Endpoints for server keys need a secret server key as well; never put one of those in a page.

AnswerMeans
401 api_key_missingThe call has no project key — add ?apiKey= to the address.
401 api_key_invalidNo project has that key — it’s mistyped, or it was regenerated. Copy it again from API → Keys.

Regenerate (API → Keys) makes a new key; the old one stops at once, so every site, widget and app that calls the API needs the new one. Sites MINT hosts for the project switch by themselves. The widget and analytics snippets carry it as data-key.

Addresses used to name the project — /public/api/acme-store/products. Those no longer answer: use /public/api/products?apiKey=….

The API pages

Sidebar → API (needs the Build permission). Five tabs run along the top of each.

PageWhat it’s for
OverviewThe API’s address, how many endpoints are on, calls, keys and customers; each model and what it has on the API; what was called lately; code to start from.
EndpointsEvery endpoint, by model — method, address, who can call it, how many rules, calls, an on/off switch. Generate and make endpoints here; open one to change it.
KeysThe project key every call carries, and secret keys for your own server, scripts or partners.
LogsEvery call to the API in the last 7 days — who called, which endpoint answered, what they got, and why it failed.
ReferenceEvery endpoint written up for whoever builds your site or app — what to send, what comes back — with a tester.

Customers (who signed up on your site) and Webhooks (your server told when records change) sit in the same sidebar section.

Endpoints

An endpoint is a method and an address under your project’s API — GET /products/featured — plus its rules: who can call it, which records it reaches, which fields go in and out, and for a list, how it’s filtered and paged.

KindAddress
StandardA model’s own address: /products (list, create) and /products/:id (read one, update, delete). Made by Generate from a model. Their address can’t change.
CustomAny address you choose — /products/featured, /my/orders, /contact. Several endpoints can work on the same model, each with its own rules: a public list of published products, and a key-only one with everything.

Switch an endpoint off and it answers 404 until it’s on again; its settings are kept. Changes on its page are a draft until you press Save changes.

Generate from a model

Endpoints → Generate from a model (or Generate beside a model).

  1. Pick the model.
  2. Tick its endpoints — List and Read one start ticked; add Create, Update, Delete if your site or app changes records.
  3. Pick who can call them. For Signed-in customers, leave Only their own records ticked so each customer reaches just theirs — see Customer sign-in. Press Generate.

They answer at once. Endpoints the model already has are left as they are (one that was off is turned back on). Then open each to set its rules and fields. A website project’s Pages, SEO and Contents have their list and read-one endpoints from the start — that’s how your site reads them.

Customer sign-in

Built into every project — nothing to generate. Endpoints → Customer sign-in, below your models.

Every project’s API can sign customers up and in from the start. These endpoints are always on and can’t be switched off; they’re listed on Endpoints as Built in, and in the API reference with an example of each.

EndpointDoes
POST /auth/registerSigns a customer up — name, email, password (8 or more), phone if you like → { token, customer }.
POST /auth/loginSigns them in — email, password → { token, customer }.
GET /auth/meThe signed-in customer (needs their token).
PUT /auth/meChanges their name or phone (needs their token).
POST /auth/forgotEmails a link to choose a new password — see Forgot password.
POST /auth/resetThe new password, with the token from that link → { token, customer }.
POST /auth/logout-everywhereSigns them out on every device (needs their token).

Set up sign-in (on that section) opens Widgets → Login & account: where customers are kept (MINT’s Customers, or a model of yours), their customer roles, and the ready-made sign-in box for your site. Your own sign-in form calling these endpoints works just as well.

Their own data, with their token

  1. Sign in (or up). The answer has a token — keep it on their device.
  2. Send it on calls: Authorization: Bearer <token>.
  3. Call endpoints for Signed-in customers with Only their own records — on Endpoints, Generate endpoints for customers makes them. Each customer then gets just theirs: the bookings or orders they created, and their own record in the model they sign up to.
// 1. Sign in
const res = await fetch('https://mint-c5e1255519fc.herokuapp.com/public/api/auth/login?apiKey=mint_pk_…', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password }),
});
const { token } = await res.json();
localStorage.setItem('customerToken', token);

// 2. Their bookings — only theirs
const mine = await fetch('https://mint-c5e1255519fc.herokuapp.com/public/api/bookings?apiKey=mint_pk_…', {
  headers: { Authorization: 'Bearer ' + localStorage.getItem('customerToken') },
}).then(r => r.json());
A token lasts 30 days. A changed password or /auth/logout-everywhere ends every token they had. With the Login widget on the page, Mint.auth.fetch('bookings') sends the token for you.

Making an endpoint

Endpoints → New endpoint: four short steps, then its page opens.

  1. Model — whose records it works on.
  2. What it does — List, Read one, Create, Update, Delete, or Upload files.
  3. Address — lowercase words and dashes, up to four parts: /products/featured. Read one, Update and Delete end with /:id, where the record’s id goes: /my/orders/:id. A few first words are kept for MINT’s own addresses (auth, site, pages, shop, cart, checkout…), and two endpoints can’t share a method and address.
  4. Who can call it, and a name for you and your team.

When a custom address and a model’s /:id could both match — /products/featured and /products/:id — the one with more fixed words wins, so featured is never taken for an id.

Upload files

For a form that takes attachments: a model with a file or image field, like a contact form’s “Attachments”.

An Upload files endpoint takes the files and answers their links; your site then sends those links in the record’s file field through its Create (or Update) endpoint. Make one with Generate (tick Upload files — it answers at /<model>/files) or New endpoint. Only models with a file or image field can have one.

SendA POST with multipart form data, the files in the field files: up to 5, 10 MB each.
KindsPhotos (png, jpg, webp, gif, heic), PDF, Word, Excel, PowerPoint, text, CSV and zip — never svg or html.
Answer201 with { files: [{ url, name, type, size }] }.
Where they goYour media library, folder website-uploads.

Who can call it is its Access tab, as for any endpoint — and a valid server key may always upload, whoever the endpoint is for. With Anyone, one address can upload 20 times in 15 minutes.

A site that sends its form from its own server (the safest way) keeps the server key there: the browser posts the form to your server, and your server calls Upload files and Create with the key — set both endpoints to A server key.

Who can call it

An endpoint’s Access tab.

ChoiceMeans
AnyoneNo sign-in, no server key — just the project key every call carries. A product list, opening hours, a contact form.
Signed-in customersYour customers, signed in with the sign-in widget or your own form. Rules can use who they are: Only their own records makes each customer reach only the records they created through the API — orders, bookings, support requests. Which customer roles lets in only some kinds of customers (sellers, pickers, VIPs) — see Customer roles; anyone else gets 403 with code: "customer_role_required". These are your customers’ roles, not your team’s.
A server keyYour own server, scripts or partners, with a server key. For what must never be called from a browser — importing the till’s sales, a partner’s stock feed.
With Anyone, a Create, Update or Delete endpoint is open to the whole internet. That’s right for a contact form’s Create — never for Update or Delete. Records added in the panel belong to no customer, so “only their own records” never shows them to anyone.

Rules: which records

Access tab → Which records it reaches. Add a rule: a field, a test, a value.

Status is Published, Stock is more than 0, Email is their email. The tests fit the field: is, is not, is one of, contains, more / less than (before / after for dates), is empty, is not empty. Records must meet all the rules — or switch to any one of these.

EndpointWhat the rules do
ListOnly records that meet them are listed and counted.
Read one, Update, DeleteA record that doesn’t meet them doesn’t exist for this endpoint — the call answers 404.
CreateThe new record (with its defaults) must meet them, or the call is refused with 403 and code: "rule_failed". “Only their own records” makes the signed-in customer its owner.

On an endpoint for signed-in customers a rule can compare with the customer: Owner is the signed-in customer (what “only their own records” is), or a text field with their email, their name or their phone — orders placed with a customer’s email before they had an account, say — or an options field with their customer role. A customer without one (no customer role, no phone) matches nothing. A value is checked against the field when you save (“Price” takes a number), so a rule can’t fail later for being mistyped. Archived records and, on models with record access, private ones never reach the API whatever the rules say.

Fields in and out

An endpoint’s Fields tab.

ColumnMeans
Sent backThe fields in each record it answers with — leave out what the site doesn’t need or mustn’t see (a cost price, an internal note). _id always comes back. A caller can ask for fewer with fields=, never more.
WrittenCreate and Update only: the fields a call may set. Untick what only your team sets — an order’s status, a payment reference. Sent anyway, they’re ignored: a new record gets the field’s default, an update keeps the value it has.

Calculated (formula) fields are always worked out by the server. Requests aren’t refused for sending a field that isn’t written, so a site can send back a record it read.

A required field with no default must be written by a Create endpoint — otherwise every new record would be missing it. Give it a default in Models (an order’s status: pending), or tick Written.

A list’s filters and pages

A list endpoint’s Filters tab.

SettingMeans
Always filtered byRules that are part of the list — Featured is Yes. Callers can’t take them off.
Let callers filter, search and sortOn: callers add their own filters, search and sort on top. Off: the list is exactly as set here — only page, limit and fields are read.
OrderThe field it’s sorted by when the caller doesn’t say (newest first unless set).
Page size · Largest pageHow many records a page holds by default, and the most a caller’s limit can ask for (up to 100).

Linked records

A List or Read one endpoint’s Fields tab: the links a caller may have filled in.

A link — a post’s author, a hero’s button page — comes back as the linked record’s id (a top-level link with its name). A caller can ask for the whole record instead with populate=. This panel decides which links it may ask for:

SettingMeans
Every link one step awayOn (the default): every link of the model — including links inside its sections, components and content blocks.
Only the ones tickedOff: exactly the links ticked. Here you can also offer a link of a linked record — author.company — two links deep at most.

A linked record comes out as its own model’s API shows it: who may call that model’s Read one endpoint, its rules and its fields — never more. A model without a public API comes out by its name only (marked name only), and nothing past it can be filled.

Server keys

API → Keys → New key. For endpoints whose “who” is a server key.

Name the key after what will use it — “The shop’s till”, “Nightly import” — so you can revoke one without stopping the others. The key (mint_sk_…) is shown once: copy it into your server’s environment variables. After that only its last four characters show, with who made it and when it was last used.

curl 'https://mint-c5e1255519fc.herokuapp.com/public/api/till/orders?apiKey=mint_pk_…' \
  -H "Authorization: Bearer $MINT_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 3, "note": "From the till"}'

X-Api-Key: mint_sk_… works too. Any key in use opens every endpoint that needs a key. A wrong or revoked key, or none, answers 401 with code: "key_required". Revoke stops a key at once.

Never put a key in a website, an app or a message — anyone who sees the page’s code has it. Browsers call endpoints for anyone or for signed-in customers; keys are for servers.

Logs

API → Logs, or the Logs tab on an endpoint’s page. Every call to your API, kept 7 days.

Pick a period — the last hour, 24 hours or 7 days — to see how many calls came in, how many failed, how long they typically took and how slow the slowest 5% were, with a chart of calls that worked (green) and failed (red) over time. Commonest errors lists what callers were told most often. Click one to see just those calls.

Below are the calls themselves, newest first. Narrow them by answer (worked, failed, refused 4xx, server errors 5xx), endpoint, who called, or text in the address, query, error or a customer’s email. Turn on Live to check for new calls every 5 seconds while you test your site.

Open a call to see
AnswerThe status, and the error the caller got with its code — “Sign in first.” (customer_required).
EndpointThe endpoint that answered, or none: customer sign-in, the cart and shop, or an address with no endpoint (404).
Who calledAnyone, the signed-in customer (their email), or the server key (its name).
FromThe website that made the call, and the browser or client (“Chrome on macOS”, “curl/8.4.0”).
As curlThe same call as a curl command, to repeat it from a terminal.
The log never keeps what was sent, tokens, keys or IP addresses, and values of query parameters named like a token, key, secret or password show as •••. Your site’s own page loads and analytics aren’t logged.

The Overview shows the last 24 hours as a chart, with See what failed when something did. Your own AI can read the logs too, through Connect AI — ask it why a call fails.

The address

Every project has its own, made from its public name (see Projects → Its public address). Below, <project> stands for it and <model> for the model’s address:

https://mint-c5e1255519fc.herokuapp.com/public/api/<model>?apiKey=mint_pk_…

A custom endpoint has its own address under the same root — https://mint-c5e1255519fc.herokuapp.com/public/api/products/featured; every call adds the project key. GET https://mint-c5e1255519fc.herokuapp.com/public/api/?apiKey=… describes the project: its name and kind, every endpoint (method, address, who may call it, how many rules, its fields in and out), and every public model with its actions, who may call it, and its fields (key, label, kind, required, allowed values, and readOnly on those a request can’t write) — handy for checking what’s on.

Requests

ActionRequest
ListGET /<model>
Read oneGET /<model>/<id>
CreatePOST /<model>
UpdatePUT /<model>/<id>
DeleteDELETE /<model>/<id>

Bodies are JSON. Calls work from any website (CORS is open) and from servers alike.

const res = await fetch('https://mint-c5e1255519fc.herokuapp.com/public/api/products?apiKey=mint_pk_…&limit=12&sort=-createdAt');
const { doc, total, totalPages } = await res.json();

Listing and paging

GET /<model> answers one page of records at a time, with the numbers you need to page through the rest.

QueryDoes
pageWhich page, from 1. 1 unless you ask. Past the last page you get an empty list and the same total.
limitRecords per page: 20 unless you ask, 1 to 100. More than 100 counts as 100.

Every list answers { doc, total, page, limit, totalPages }: doc is this page’s records, total how many records match your filters across every page, and totalPages how many pages that makes at this limit. There’s another page while page < totalPages.

GET https://mint-c5e1255519fc.herokuapp.com/public/api/products?apiKey=mint_pk_…&page=2&limit=12

{ "doc": [ …12 records… ], "total": 37, "page": 2, "limit": 12, "totalPages": 4 }

Pages are stable: records that sort the same are always put in the same order, so paging never shows a record twice or skips one — unless records are added or removed while you page.

async function all(model, query = {}) {
  const records = [];
  for (let page = 1; ; page++) {
    const params = new URLSearchParams({ ...query, page, limit: 100 });
    const res = await fetch(`https://mint-c5e1255519fc.herokuapp.com/public/api/${model}?apiKey=mint_pk_…&${params}`);
    if (!res.ok) throw new Error((await res.json()).message);
    const { doc, totalPages } = await res.json();
    records.push(...doc);
    if (page >= totalPages) return records;
  }
}
Fetching everything costs one request per 100 records and counts toward the rate limit (see Errors and limits). For a page your visitors see, ask for just the page they’re on.

Sorting

sort names a field; a minus in front reverses it.

QueryOrder
sort=-createdAtNewest first — the default when you don’t say.
sort=priceLowest price first (A→Z for text, oldest first for dates, false before true).
sort=-priceHighest price first.
sort=-featured,priceFeatured first, then by price within each — up to three fields, comma-separated.

You can sort by any of the model’s own fields and by createdAt, updatedAt, code and _id. A field the list can’t sort by is skipped; if none is left, the default applies. The reference on the Public API page lists each model’s sortable fields.

Filters

Add a field’s key to the address to get only the records that match — the same way the panel’s own lists filter.

<field>=<value> keeps the records whose field equals the value. For anything else, add an operator to the field’s key with an underscore: <field>_<operator>=<value>.

GET https://mint-c5e1255519fc.herokuapp.com/public/api/products?apiKey=mint_pk_…&status=live&price_gte=10&price_lte=50&sort=price
OperatorKeeps records where the field…
status=liveequals the value. On a list field (tags, options you can pick several of, several links): has it. On a date: falls on that day.
status_ne=draftdoesn’t equal it (a list field: doesn’t have it).
status_in=live,soldis any of these, comma-separated. Repeating the name does the same: status=live&status=sold.
status_nin=draft,soldis none of these.
price_gt=10 · price_gte=10is greater than · greater than or equal to the value (numbers and dates).
price_lt=50 · price_lte=50is less than · less than or equal to the value (numbers and dates).
price_btwn=10_50is between the two, both included: from_to. Leave one end out for open-ended — price_btwn=100_ is 100 and up.
name_contains=shoecontains the text, any case — “Trail Shoe”, “SHOES”. The value is plain text, not a pattern.
tags_all=run,trailhas all of these (tags, options you can pick several of, several links).

How filters combine

  • Every filter must match: status=live&featured=true is live and featured.
  • Two operators on one field make a range: price_gte=10&price_lt=20.
  • For “this or that” on one field use _in. Filters on different fields can’t be OR-ed — make two requests, or use search.
  • Filters, search, sort, page, limit and fields all work together, in any order.total and totalPages count what matches.

What the API does with a name it doesn’t know

It ignores it, so cache busters like ?v=2 are harmless — but so is a typo: ?staus=live filters nothing. If a filter seems to be ignored, check the key in Models (it’s the key, not the label, and it’s case-sensitive). A value the API can’t read is an error instead, with the reason: price=cheap answers 400 “price must be a number”, and so does an operator the field doesn’t take, like status_gte.

Use underscores, not brackets: price_gte=10, never price[gte]=10 (that answers 400). Keys with an underscore of their own work as they are: contact_email=…, contact_email_contains=….

Writing the address

Values with spaces, &, +, # or accents must be encoded. Let the browser do it:

const params = new URLSearchParams({
  status: 'live',
  price_btwn: '10_50',
  name_contains: 'trail & road',   // encoded for you
  sort: '-createdAt',
  page: '1',
  limit: '12',
});
const res = await fetch(`https://mint-c5e1255519fc.herokuapp.com/public/api/products?apiKey=mint_pk_…&${params}`);

Filters by kind of field

What each kind of field takes. Fields not listed here (images, files, rich text, colours, sections, passwords) can’t be filtered.

Field kindValue and operators
Text, email, link, long textExact text (equals is exact and case-sensitive; emails ignore case). _ne, _in, _nin, _contains (any case).
Options (pick one)One of the field’s values — the value, not the label. _ne, _in, _nin.
Number, calculated (formula)A number: 10, 12.5, -3. _ne, _in, _nin, _gt, _gte, _lt, _lte, _btwn.
Yes / notrue or false (1 and 0 work too). _ne. A record saved without the box ticked counts as false.
DateSee Filtering by date. _ne, _gt, _gte, _lt, _lte, _btwn.
Link to one recordThe linked record’s _id: category=66f0c1d2e3a4b5c6d7e8f901. _ne, _in, _nin.
Tags, options (pick several), links to several recordsEquals means “has it”: tags=sale. _ne (doesn’t have it), _in (has any), _nin (has none), _all (has every one).
createdAt, updatedAtEvery model has these two dates, and filters by them like any date — handy for “new this week” and for syncing what changed.

GET https://mint-c5e1255519fc.herokuapp.com/public/api/?apiKey=… lists, for each model with List on, its filters (each field’s key, kind and operators), the fields search looks through, and the fields it can sort by.

Filtering by date

Dates take a day, a moment, or a shortcut.

ValueMeans
2026-10-04That whole day (UTC). releasedOn=2026-10-04 is anything on the 4th.
2026-10-04T09:30:00ZThat exact moment. Add a zone (Z, +06:00) — without one the server’s time zone (UTC) is used.
todayToday (UTC).
week · month · yearThe last 7 days · month · year, up to the end of today.
days_30 · months_3The last 30 days · 3 months, up to the end of today.

With a plain day, the operators work in whole days, the way people say it:

QueryKeeps
date_gte=2026-10-01From the 1st on, the 1st included.
date_lte=2026-10-31Up to the end of the 31st, the 31st included.
date_gt=2026-10-01From the 2nd on.
date_lt=2026-10-31Up to the end of the 30th.
date_btwn=2026-10-01_2026-10-31All of October, both ends included.
createdAt=weekAdded in the last 7 days.
updatedAt_gte=2026-10-04T09:30:00ZChanged since that moment — for keeping a copy in sync.

Choosing fields

fields=<keys> answers with just those fields of each record — smaller and faster, for menus, cards and dropdowns.

GET https://mint-c5e1255519fc.herokuapp.com/public/api/products?apiKey=mint_pk_…&fields=name,price&limit=100

{ "doc": [ { "_id": "66f0…", "name": "Trail shoe", "price": 89 }, … ], "total": 37, … }

_id always comes back. Keys the model doesn’t have are skipped; if none of them is known, every field comes back. Linked records named in fields still come with their name. fields only changes what comes back — you can filter and sort by fields you didn’t ask for.

Filling in links

populate=<links> answers with the linked records themselves instead of their ids.

GET https://mint-c5e1255519fc.herokuapp.com/public/api/posts/66f0…?apiKey=mint_pk_…&populate=author,hero.button_page

{
  "_id": "66f0…",
  "title": "Spring menu",
  "author": { "_id": "66a1…", "name": "Ana", "bio": "…" },
  "hero": { "heading": "Fresh bread", "button_page": { "_id": "66b2…", "title": "Order online", "slug": "order" } },
  "publishedAt": "2026-10-16T09:30:00.000Z"
}
WriteFills
populate=authorA link field.
populate=hero.button_pageA link inside a section, a component or a list of rows.
populate=content.pageA link inside content blocks — in every block type that has it.
populate=author.companyA link of the linked record — when the endpoint offers it (two links deep at most).
populate=*Everything the endpoint offers.

Several are comma-separated. A link the endpoint doesn’t offer answers 400 with the ones it does (see Linked records). A linked record the caller isn’t allowed to see is null, or left out of a list of links. A linked record’s own links stay ids unless you fill them too.

Drafts and publishedAt

A model with Drafts and publishing answers with what’s published — edits wait until someone presses Publish.

Each record of such a model carries publishedAt, when it last went live. A record that’s never been published, or was taken off, isn’t in the answers at all; filters, search and sort run on the published values.

A server — a preview build, an import — can read the drafts as they are, never-published ones too, with status=draft and a server key. If the model has a field of its own called status, write _status=draft instead (it always works).

curl "https://mint-c5e1255519fc.herokuapp.com/public/api/posts?apiKey=mint_pk_…&status=draft" \
  -H "x-api-key: $MINT_SERVER_KEY"
Never put a server key in a page’s code — anyone can read it there. Without a server key, status=draft answers 401.

Recipes

You wantRequest
A shop page: one category, a price range, cheapest first, 24 to a pageGET /products?category=<id>&price_btwn=20_100&sort=price&limit=24&page=1
The newest three posts for a home pageGET /posts?status=published&sort=-createdAt&limit=3
Featured items onlyGET /products?featured=true
Anything tagged sale or newGET /products?tags_in=sale,new
Upcoming events, soonest firstGET /events?startsOn_gte=today&sort=startsOn
Bookings for one dayGET /bookings?date=2026-10-14
What changed since your last syncGET /products?updatedAt_gt=2026-10-04T09:30:00Z&sort=updatedAt&limit=100
A dropdown of namesGET /categories?fields=name&sort=name&limit=100
How many records match, without the recordsGET /orders?status=open&fields=_id&limit=1

The last one reads total from the answer. Every list endpoint on the Public API page comes with examples made from that model’s own fields — press Try on one to send it.

let page = 0, totalPages = 1;
async function loadMore() {
  if (page >= totalPages) return;            // nothing left
  page += 1;
  const res = await fetch(`https://mint-c5e1255519fc.herokuapp.com/public/api/products?apiKey=mint_pk_…&status=live&sort=-createdAt&limit=12&page=${page}`);
  const data = await res.json();
  totalPages = data.totalPages;
  render(data.doc);                          // append to what's shown
}

What comes back

A list is { doc, total, page, limit, totalPages }; one record is the record itself. A record has _id, code (when the model numbers records), createdAt, updatedAt and the model’s own fields — nothing else. Linked records come with their name: "category": { "_id": "…", "name": "Shoes" }. Records you archive in the panel never come out — not in lists, and not by their _id.

{
  "doc": [
    { "_id": "66f0…", "name": "Trail shoe", "price": 89, "category": { "_id": "66e1…", "name": "Shoes" },
      "createdAt": "2026-09-30T10:12:00.000Z", "updatedAt": "2026-09-30T10:12:00.000Z" }
  ],
  "total": 37, "page": 1, "limit": 12, "totalPages": 4
}

Creating and updating

  • Send the model’s fields as JSON. Anything else is ignored; calculated (formula) fields are worked out on the server, and read-only fields are left to your team, whatever you send.
  • Create answers 201 with the new record. Update changes only the fields you send and answers with the record.
  • The same checks as the panel apply: required fields, allowed values, min/max, unique fields.
await fetch('https://mint-c5e1255519fc.herokuapp.com/public/api/bookings?apiKey=mint_pk_…', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ guestName: 'Ana Lima', date: '2026-10-14', guests: 2 }),
});

Calling as a customer

For models that need a signed-in customer, send their token: Authorization: Bearer <token> — from customer sign-in. With the Login widget on the page, Mint.auth.fetch('orders') does that for you. Records a signed-in customer creates are theirs, which is what own records only goes by. See Customers & sign-in.

const res = await fetch('https://mint-c5e1255519fc.herokuapp.com/public/api/orders?apiKey=mint_pk_…', {
  headers: { Authorization: 'Bearer ' + token },
});

Errors and limits

StatusMeans
400Something isn’t valid; message says what — a body (“Quantity is required”) or a list’s query (“price must be a number”, “status can’t use _gte — it takes status_ne, status_in, status_nin”).
401No project key (code: "api_key_missing") or one no project has (code: "api_key_invalid") — see the project key. Sign in first (code: "customer_required") — the endpoint is for signed-in customers and the token is missing, expired or for another project. Or code: "key_required" — it needs a server key and none, a wrong or a revoked one was sent.
403A rule refused it (code: "rule_failed") — a create whose record doesn’t meet the endpoint’s rules; message says which.
404No endpoint answers that method and address (or it’s off), the record doesn’t exist or is outside the endpoint’s rules, or the project is archived.
429Too many requests (code: "rate_limited"). Retry-After says how many seconds to wait.

Limits

300 requests a minute per visitor’s IP address, and 40 sign-in or sign-up attempts per 15 minutes. If your own server calls the API for every page view (server-side rendering), all those calls come from one address — cache the answers, or call from the browser.

The API reference

API → Reference: every endpoint your site or app can call.

It’s made from what the live API says it offers, so it always matches the Endpoints page — make or switch on an endpoint and it appears, grouped by model; Open goes to its page. Above the models, Lists: paging, sorting and filters sums up the query parameters and operators every list takes. Click an endpoint for the rest: a list shows its sortable and searchable fields, every filter its fields take (with their operators and allowed values) and example requests made from its own fields, each with Try; a create or update shows the body fields; every endpoint shows an example response. Endpoints marked Customer need a signed-in customer’s token. The customer sign-in endpoints are listed too, and for a website, the site and page endpoints.

Each endpoint’s own page has a Reference tab with the same for that endpoint alone, as JSON you can copy: the Request — its address, headers, and for a create or update the body with every field it writes (required ones named) — the Response it answers with (a list’s page, one record, or { "message": "Deleted" }), and the Errors it can answer instead, each with its message and code. It follows the endpoint as you edit it: untick a field under Fields and it leaves the sample response straight away, before you save.

Example requests

Every endpoint in the reference comes as a request you can paste: curl or fetch.

Open an endpoint and its Example request is at the top — switch between curl for a terminal and fetch for your site or app, and copy it. The address is your project’s, the body has the model’s fields filled in by example, the project key is on the address, and an endpoint for signed-in customers carries the Authorization: Bearer <customer token> header to fill in. For instance, creating a booking:

curl -X POST 'https://…/public/api/bookings?apiKey=mint_pk_…' \
  -H 'Content-Type: application/json' \
  -d '{"guest":"Ada Lovelace","nights":2}'
const res = await fetch('https://…/public/api/bookings?apiKey=mint_pk_…', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ guest: 'Ada Lovelace', nights: 2 }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message);

Trying requests

“Try it” sends a request to your live API from your browser and shows the answer — on an endpoint’s Try it tab, or under the reference.

  1. Press Try on an endpoint in the reference — or pick a method and type a path, like /products?limit=5.
  2. For a create or update, edit the example body. It starts with the model’s fields filled in by example.
  3. Press Send. You see the status, how long it took and the JSON that came back. Copy as fetch gives you the same call for your code.

To try a customer-only endpoint, send POST /auth/register or /auth/login first: the tester keeps the token and sends it with the next requests. After a list or a create, the record’s _id fills in the next :id.

Requests are real — a create, update or delete changes your project’s records.

Webhooks

Your project telling your own server when records change. Webhooks page; needs the Build permission.

Where the public API lets your site ask for records, a webhook sends them as they change: a new booking to your front-desk system, a paid order to the warehouse, anything to Zapier. Whether the change was made in the panel, through the public API or by an AI assistant, the project POSTs the record to an address of yours.

  1. On Webhooks, press Add a webhook.
  2. Pick the model and when it’s sent: a record created, changed, deleted — any of them. A model with drafts can also send when a record is published (the record as it went live) or taken off (its draft), with event: "publish" or "unpublish". Publishing also refreshes your site’s pages. A scheduled publish sends them too, with source: "schedule".
  3. Give the address your server listens on, starting with https://, and a note on what it does with it. Without an address the webhook stays off.
  4. Copy the secret that’s shown — it isn’t shown again. Your server uses it to check each request (next section). Lost it? The key button makes a new one; the old one stops matching at once.
  5. Press Send test. Your server gets an event: "test" request with the model’s newest record (or an example), and the answer shows straight away.

Each request has a JSON body like this:

{
  "delivery": "6f1c…",          // the same on every retry — skip ones you've seen
  "event": "create",            // create | update | delete | publish | unpublish | test
  "route": "orders",
  "project": { "id": "…", "slug": "acme-shop", "name": "Shop" },
  "source": "api",              // api | panel | test
  "at": "2026-10-04T09:30:00.000Z",
  "record": { "_id": "…", "createdAt": "…", "updatedAt": "…", "total": 42 }
}
How it works
SuccessAny 2xx answer. Answer quickly — after 10 seconds it counts as no answer.
RetriesAnything else is tried 3 more times, waiting longer each time (about 15 seconds, 1 minute, 4 minutes). The delivery id stays the same, so skip one you’ve already handled.
The logDeliveries on each webhook: the last 50, with what was sent, what came back and how many tries.
FieldsThe record’s own fields, as the public API gives them — never password fields.
AddressesServers on the internet only: addresses inside a private network, or the server’s own, are turned away.
An API template can make webhooks for you, asking for the address when the project is made. Off ones are waiting for an address — add it and switch them on.

Checking a webhook is real

Anyone can send your server a request; only your project can sign one with the secret.

Every request carries x-mint-event, x-mint-delivery, x-mint-timestamp (seconds) and x-mint-signature. The signature is sha256= followed by the HMAC-SHA256, in hex, of the timestamp, a dot and the body exactly as it arrived — keyed with the webhook’s secret. Work it out yourself and compare; turn away a request that doesn’t match or whose timestamp is more than a few minutes old. In Node:

import crypto from 'crypto';

// In your handler, with the body exactly as it arrived (not re-serialised):
function fromMint(rawBody, headers, secret) {
  const timestamp = headers['x-mint-timestamp'];
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');
  const given = String(headers['x-mint-signature'] || '');
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; // 5 minutes
  return fresh && given.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
Use the raw body. Parsing the JSON and turning it back into text can change spacing or order, and the signature no longer matches.

Troubleshooting

SymptomWhy, and what to do
401 for everythingThe project key is missing or out of date — every call needs ?apiKey= with the key from API → Keys.
404 for everythingCheck the address has no project name in it (/public/api/products, not /public/api/acme-store/products), and that the project isn’t archived.
A webhook says “Couldn’t reach it” or “No answer in 10 seconds”Your server is down, the address is wrong, or it answers too slowly — answer first, then do the work. Send test shows the answer straight away.
The signature never matchesUse the body exactly as it arrived (not re-serialised JSON), and the secret shown last — making a new one retires the old.
404 for one modelIt isn’t Public, or the action you’re calling isn’t ticked.
401 customer_requiredThe model is for signed-in customers — sign in with the widget, or send the customer’s token.
A field is missing from the answerOnly the model’s own fields come out; check the field’s key in Models — and that fields= doesn’t leave it out.
A filter changes nothingThe name isn’t one of the model’s filterable keys, so it’s ignored — check the spelling and case of the key (not the label) in Models. Images, files, rich text and sections can’t be filtered.
A text filter finds nothingEquals is exact and case-sensitive. Use _contains or search for “contains, any case”.
A date filter is a day offPlain days are UTC. For your own time zone, send moments with the zone: date_gte=2026-10-04T00:00:00+06:00.
A record is in the panel but not in the APIIt’s archived, it’s kept private to some of the team, or the model is own-records-only.
A customer can’t see an order made in the panelOwn-records-only models show customers only what they created.
A field I send doesn’t changeIt’s read-only on the Public API page (only your team sets it), or it’s calculated. GET / marks both readOnly.
Can’t make a field read-only: “required and has no default”With Create on, every new record needs it. Give the field a default in Models, then tick it.