Procurement is starting to happen through software. An agent asked to shortlist suppliers can read this catalogue directly rather than scraping the pages: the same records, the same figures, the same wording about what is certified and what is not.
The endpoints
| Endpoint | What it returns |
|---|---|
GET /api/v1/products | Search and filter. Returns a summary per product and the url for the full record. |
GET /api/v1/products/{id} | One product in full, by part number or slug. |
GET /api/v1/products/compare?skus=A,B | Two to six products, on the attributes they all publish. |
GET /api/v1/products/{id}/pricing | Quantity bands, minimum order, Incoterms, lead time. Never binding. |
GET /api/v1/products/{id}/documents | Which documents exist and how to ask for them. |
GET /api/v1/categories | The twelve categories and their slugs. |
POST /api/v1/rfq | Send a quotation request and get a reference. Creates an enquiry, never an order. |
The six dangerous goods calculators answer at their own paths and also under /api/v1/. Those original paths will keep working: they are published in an OpenAPI document, the MCP Registry and a public repository, so breaking them would break integrations we cannot see. The dangerous goods API is documented here.
Searching
GET /api/v1/products?material=hdpe&capacity_min=20&capacity_max=25&certification=UN
Terms and filters are combined with AND. A product with no recorded value for an attribute you filtered on is excluded rather than treated as a match, so an empty result means nothing satisfies every condition, and the response names any word no product mentions so you know which one to drop.
Material is resolved to a material family rather than matched as text, which matters: hdpe does not return polypropylene. A generic term widens only downward, so steel includes stainless and galvanised steel and stainless never includes plain steel.
| Parameter | What it takes |
|---|---|
q | Free text. Synonyms and common misspellings are handled, and "20L" reaches "20 litres". |
category | A slug from /api/v1/categories |
material | hdpe, polypropylene, polyethylene, stainless, steel, galvanised, aluminium, tinplate, plastic, metal |
capacity_min, capacity_max | Litres. A product whose range overlaps yours is a match. |
application | Intended use, such as defence, agriculture or marine. |
certification | UN, or a design code such as 3H1 |
pricing_type | PUBLIC_PRICE, STARTING_FROM, PRICE_ON_REQUEST, CONTRACT_PRICE |
sort | relevance, capacity, capacity_desc, price, name |
page, limit | Pagination. Up to 100 per page, 20 by default. |
Abridged response. Fields are listed in full in the OpenAPI document.
{
"ok": true,
"total": 7,
"page": 1,
"results": [
{
"id": "product-widemouth-hdpe-jerrycan",
"sku": "F21-IPW-10",
"name": "Wide-Mouth HDPE Jerrycan 3H2",
"url": "https://fortitude21.com/product-widemouth-hdpe-jerrycan",
"capacity_l": { "min": 10, "max": 25, "sizes": [10, 20, 25] },
"material_family": ["hdpe"],
"un_status": { "claim": "Available UN 3H2 certified", "offered": true, "verified": false },
"pricing_type": "PRICE_ON_REQUEST",
"starting_from": null,
"orderable_units": 3,
"matched_on": ["sku", "name", "material"]
}
],
"cite": "Fortitude21 product catalogue: https://fortitude21.com/products",
"publisher": { "name": "Fortitude21", "sells_to": "Businesses, distributors, OEM and private-label buyers..." }
}
What the figures mean
Every specification carries where it came from, because that is what a buyer comparing suppliers actually needs to know.
| Value | Meaning |
|---|---|
verified | The figure comes from the manufacturer own production specification, or it is our part number. |
indicative | Our own figure, correct as published and confirmed against the drawing issued with an order. |
on_request | A field that exists and is quoted on enquiry. It is named rather than omitted, because a missing field and a withheld one are different things. |
UN status is never a boolean. An approval is granted per design type, and the competent authority decides which production site each one covers, so the response carries the claim, its scope and verified: false until a certificate number is in hand. Ask for the certificate covering your design and the plant named on it.
Pricing
A price here is indicative and is not a quotation or an offer. It excludes freight, duty and taxes, it depends on the specification and the destination, and it is confirmed only in a written quotation. Every pricing response says so and carries binding: false. Most of the range is priced on request and returns no number at all.
Quantity bands are set per product family, because a 20 litre can ships 852 to a container and a 2.5 litre pail ships 12,500. The highest band of every product is a quotation rather than a figure: that is where tooling, Incoterms and freight start deciding the number. The pricing model is published in full.
Sending a quotation request
POST /api/v1/rfq
This creates an enquiry for a person to price. It accepts no price, confirms no stock, commits no delivery date and creates no order, and the response says each of those in its own field so a summary cannot turn into a quotation. Every part number is checked against the catalogue, and the information a quotation still needs is named back to you.
consent must be true, and it means the buyer agreed to their contact details being sent to Fortitude21. An agent acting for a buyer is not the buyer, so without it nothing is stored. What we do with an enquiry.
curl -X POST https://fortitude21.com/api/v1/rfq \
-H 'content-type: application/json' \
-d '{
"consent": true,
"company": "Example Distribution Sdn Bhd",
"contact_name": "A Buyer",
"email": "buyer@example.com",
"country": "Malaysia",
"delivery_destination": "Port Klang, Malaysia",
"incoterms": "CIF",
"lines": [{ "sku": "F21-NF-20", "quantity": 2000 }]
}'
{
"ok": true,
"reference": "F21-RFQ-261030-ZQDFFR",
"missing_information": [
"F21-NF-20: which certification the destination market requires. UN approval is granted per design type and per production site."
],
"status_note": "Received as a quotation request. This is not a quotation.",
"price_accepted": false,
"stock_confirmed": false,
"delivery_date_committed": false,
"order_created": false,
"next_step": "A person at Fortitude21 prices it against the specification and replies by email."
}
MCP server
The catalogue is also eight tools on our MCP server, alongside the six dangerous goods calculators. An assistant that already has the calculators connected gets the catalogue without a second connection.
https://fortitude21.com/mcp Streamable HTTP, JSON-RPC 2.0, no authentication
search_products get_product_pricing check_product_requirements
get_product_details get_product_categories create_quote_request
compare_products get_product_documents
Seven are read-only. create_quote_request is declared as a write, so a client asks before calling it rather than retrying quietly, which is how a buyer would otherwise receive three identical enquiries.
Feeds
The whole catalogue in one file, for a crawler, a spreadsheet or a feed tool that would rather not make a request per product.
| File | What it is |
|---|---|
/feeds/products.json | Every product with its typed attributes, pricing state and bands. |
/feeds/products.csv | The same, one row per product, for a spreadsheet. |
/openapi.json | OpenAPI 3.1 for every endpoint, tools and catalogue together. |
Using it
No key and no sign-up. CORS is open, so a browser can call it. There is a rate limit of about ten requests every ten seconds per address, and going over it answers 429. Responses carry an ETag: send it back as If-None-Match and an unchanged catalogue answers 304 with no body.
The catalogue is read-only at every version. The quotation request is the only endpoint that accepts a body, and it creates an enquiry. Nothing here can change a product, a specification or a price.
Credit us with a link to the product page when you use this, as the terms ask. If you are building something with it, tell us and we will keep your use in mind before changing anything. Terms of use.