MBNF Stock API
Live, read-only access to the sneaker stock of MBNF SRL for our B2B partners. Plug it into your shop, ERP or spreadsheet and always see what we can ship, size by size.
Quick start#
- Get your API key
MBNF sends it to you personally. It looks like
mbnf_AbCd1234_…— the first 8 characters aftermbnf_are the key ID (safe to quote to us), the rest is secret. - Check that it works
Call
/v1/me: it returns your account name, your limits and when the stock was last updated.curl https://mbnf.supply/api/v1/me \ -H "Authorization: Bearer $MBNF_API_KEY"import os, requests r = requests.get("https://mbnf.supply/api/v1/me", headers={"Authorization": f"Bearer {os.environ['MBNF_API_KEY']}"}, timeout=15) r.raise_for_status() print(r.json())const r = await fetch("https://mbnf.supply/api/v1/me", { headers: { Authorization: `Bearer ${process.env.MBNF_API_KEY}` }, }); if (!r.ok) throw new Error(`MBNF API ${r.status}`); console.log(await r.json());$ch = curl_init("https://mbnf.supply/api/v1/me"); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("MBNF_API_KEY")], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, ]); $me = json_decode(curl_exec($ch), true); - Read the stock
Call
/v1/stockand walk through the pages. That's it — see Keeping your stock in sync for a complete, production-ready loop.curl "https://mbnf.supply/api/v1/stock?per_page=200&page=1" \ -H "Authorization: Bearer $MBNF_API_KEY"r = requests.get("https://mbnf.supply/api/v1/stock", headers={"Authorization": f"Bearer {os.environ['MBNF_API_KEY']}"}, params={"per_page": 200, "page": 1}, timeout=15) for p in r.json()["data"]: print(p["sku"], p["total_quantity"], [(s["size"], s["quantity"]) for s in p["sizes"]])const r = await fetch("https://mbnf.supply/api/v1/stock?per_page=200&page=1", { headers: { Authorization: `Bearer ${process.env.MBNF_API_KEY}` }, }); const { data, meta } = await r.json(); for (const p of data) console.log(p.sku, p.total_quantity);$ch = curl_init("https://mbnf.supply/api/v1/stock?per_page=200&page=1"); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("MBNF_API_KEY")], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true); foreach ($res["data"] as $p) { echo $p["sku"], " ", $p["total_quantity"], PHP_EOL; }
Sandbox#
Want to build before you get your key? Use the public sandbox key: it works on every endpoint and
always answers with the same sample products — never our real stock. Responses carry
"sandbox": true in meta and the header X-MBNF-Sandbox: true.
mbnf_Sandbox1_public-test-key-sample-data-onlyIt is limited to 30 requests per minute and 1,000 per day per IP address. When you switch to your own key nothing else changes: same URLs, same fields — only real data.
Prefer clicking to reading? The interactive API reference lists every endpoint with its parameters and responses; the sandbox key is already filled in — open an endpoint, press Test Request and see the answer.
Try it#
Paste your key — or press Use the sandbox key — and send a real request: you see exactly what your system will receive. The key stays in this page: it is not saved anywhere and disappears when you close the tab. Requests count against your limits like any other.
Authentication#
Send the key in the Authorization header of every request:
Authorization: Bearer mbnf_AbCd1234_your-secret-partX-API-Key: <key> works too. Keys in the URL (?api_key=, ?token= …) are
rejected with 400, because URLs end up in logs and browser history.
A missing, wrong, revoked or expired key always gets the same 401 invalid_api_key — we never say which.
Endpoints#
All endpoints are GET (and HEAD), return JSON and live under https://mbnf.supply/api/v1.
/v1/stock
The products we can offer you right now, paginated and ordered by brand and name. Only products with at least one size available are listed.
| Query parameter | Description |
|---|---|
pageinteger · default 1 | Page number, starting from 1. |
per_pageinteger · 1–200 · default 100 | Products per page. |
brandstring | Exact brand, case-insensitive — e.g. Nike, Asics. |
skustring | Exact SKU, case-insensitive. |
qstring · max 100 | Free text searched in SKU and name. |
{
"data": [
{
"sku": "DD1391-100",
"name": "Nike Dunk Low Retro White Black",
"brand": "Nike",
"image_url": "https://image.goat.com/…/DD1391-100.png",
"price": 145,
"currency": "EUR",
"price_vat_included": false,
"total_quantity": 7,
"sizes": [
{ "size": "9", "size_system": "US", "size_eu": "42.5", "quantity": 3 },
{ "size": "10", "size_system": "US", "size_eu": "44", "quantity": 4 }
]
}
],
"meta": {
"page": 1, "per_page": 100, "total": 48, "total_pages": 1,
"stock_updated_at": "2026-10-14T09:12:00+00:00",
"stale": false
}
}/v1/stock/{sku}
One product, same shape as an item of data above, wrapped as {"data": {…}, "meta": {…}}.
The SKU is case-insensitive. 404 not_found means it is not available for you right now.
/v1/me
Your key and account. The cheapest way to test a key or check the freshness of the data.
{
"client": "Your Company B.V.",
"key_id": "AbCd1234",
"key_expires": null,
"prices_included": true,
"limits": { "requests_per_minute": 60, "requests_per_day": 10000 },
"usage_today": { "requests": 42, "remaining": 9958 },
"products": 48,
"stock_updated_at": "2026-10-14T09:12:00+00:00",
"stale": false
}/v1/openapi.json
The machine-readable OpenAPI 3.1 specification (no key needed). Import it in Postman, Insomnia or your code generator — or browse it as an interactive reference.
Response fields#
Product
| Field | Description |
|---|---|
skustring | Manufacturer style code (e.g. DD1391-100). Unique in the feed: use it as your key. |
namestring | Product name. |
brandstring | null | Brand. |
image_urlstring | null | HTTPS product image, when we have one. |
priceinteger | null | Price per pair, whole units of currency. Only present if prices are enabled for your account.
null means we have no price for that product yet: ask us. |
currencystring | Always EUR (present with price). |
price_vat_includedboolean | Whether price includes Italian VAT (22%). |
total_quantityinteger | Sum of sizes[].quantity. |
sizesarray | Available sizes, smallest first. See below. |
Size
| Field | Description |
|---|---|
sizestring | null | The size as printed on the box (US for most sneakers). null for one-size items. |
size_systemstring | null | US, US Men, US Women, US Youth, US Kids, EU or Apparel. |
size_eustring | null | EU equivalent, when a conversion exists (e.g. 42.5, 36 2/3). |
quantityinteger ≥ 1 | Pairs available in that size. It may be capped for your account (we never show more than we have). |
Meta
| Field | Description |
|---|---|
page, per_pageinteger | Echo of your pagination. |
total, total_pagesinteger | Products matching your filters, and pages at this per_page. |
stock_updated_atISO 8601, UTC | When this data left our warehouse system. |
staleboolean | true if that is more than 24 hours ago. The data is still served, but treat it with care. |
Keeping your stock in sync#
- Poll every 5–10 minutes. Our data does not change faster than that.
- Send the ETag back. If page 1 answers
304 Not Modified, nothing changed anywhere: stop there. A 304 does not count against your daily quota. - Read every page, then replace. The feed is the full truth: a SKU or a size that is not in it is sold out — set it to 0 on your side.
- Check
meta.stale. If it istrue, pause automatic selling. - Quantities are an offer, not a reservation. Every order is confirmed by MBNF.
import os, time, requests
BASE = "https://mbnf.supply/api/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['MBNF_API_KEY']}"
etag = None
def get(params, extra=None):
while True:
r = S.get(f"{BASE}/stock", params=params, headers=extra or {}, timeout=20)
if r.status_code == 429: # too fast: wait as told
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
return r
def fetch_all():
"""{sku: product} for everything on offer, or None if nothing changed."""
global etag
first = get({"page": 1, "per_page": 200}, {"If-None-Match": etag} if etag else None)
if first.status_code == 304:
return None
first.raise_for_status()
body = first.json()
products = body["data"]
for page in range(2, body["meta"]["total_pages"] + 1):
r = get({"page": page, "per_page": 200})
r.raise_for_status()
products += r.json()["data"]
etag = first.headers.get("ETag") # only after a complete read
if body["meta"]["stale"]:
print("warning: MBNF stock older than 24h")
return {p["sku"]: p for p in products}
while True:
stock = fetch_all()
if stock is not None:
update_my_catalog(stock) # your code: anything NOT in `stock` is sold out
time.sleep(300)Rate limits#
Each key has a per-minute allowance (with a small burst) and a daily quota; /v1/me shows yours.
Every authenticated response tells you where you stand:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests per minute for your key. |
RateLimit-Remaining | Requests you can make right now (burst left). |
RateLimit-Reset | Seconds until your allowance is full again. |
X-Daily-Quota-Limit | Requests per day (UTC). |
X-Daily-Quota-Remaining | Requests left today. Resets at 00:00 UTC. |
Retry-After | On 429 and 503: seconds to wait before retrying. |
ETag | Version of this response; send it back as If-None-Match. |
X-Request-Id | Unique ID of the request — quote it when you contact us. |
Ten invalid keys from the same IP address within ten minutes block that address for fifteen minutes
(429 too_many_failed_attempts), even for valid keys.
Errors#
Errors are JSON with a stable machine-readable error code:
{ "error": "rate_limited", "message": "Too many requests per minute. …", "request_id": "9f2c41d07e3a4b18" }| Status · error | What to do |
|---|---|
400 · invalid_parameter | Fix the query parameter named in message. |
400 · key_in_query | Move the key from the URL to the Authorization header. |
401 · invalid_api_key | Check the key. It may have been revoked or have expired: ask us for a new one. |
403 · ip_not_allowed | Your key is restricted to some IP addresses: call from one of them, or tell us your new IP. |
404 · not_found | SKU not available for you right now, or wrong path. |
405 · method_not_allowed | Only GET and HEAD. |
429 · rate_limited | Too fast. Wait Retry-After seconds. |
429 · daily_quota_exceeded | Quota used up. It resets at 00:00 UTC; use ETags to save requests. |
429 · too_many_failed_attempts | Too many invalid keys from your IP. Wait Retry-After seconds and fix the key. |
429 · server_busy | Retry in a second. |
503 · stock_unavailable | Data not loaded yet. Retry in a few minutes. |
503 · service_suspended | The API is temporarily switched off by MBNF. Retry later. |
500 · internal_error | Our fault. Retry later; if it persists, send us the request_id. |
Security#
- Server-side only. Never put the key in a website, a mobile app or anything that runs on your customers' devices: anyone could read it there. The API intentionally does not support browser CORS requests.
- Store it like a password — in an environment variable or a secret manager, not in your source code.
- Rotation without downtime. You can have two active keys: ask for a new one, deploy it, then tell us to revoke the old one.
- IP allow-list. On request we lock your key to your servers' IP addresses.
- Leaked? Tell us: revocation is immediate.
Versioning#
/v1 is stable. We may add fields and endpoints at any time, so ignore fields you don't know.
We will never rename or remove a field, or change its meaning, inside v1: that would be a new version,
announced in advance.
| Date | Change |
|---|---|
| 2026-10-14 | v1 launched: /v1/stock, /v1/stock/{sku}, /v1/me,
sandbox key and interactive reference. |
Support#
Reply to the message you
received your key with, quoting your key ID (the 8 characters after mbnf_) and the
request_id of the failing call. Never send us the full key.