Developers
The search behind Cerulean is open to everyone. No key, no token, no application form.
The Search API
/api/search is the endpoint our own search page calls. It is public, it needs
no authentication, and it is not rate limited. If you are building a job
aggregator, a research project, or a browser extension that wants to know what
the luxury houses are hiring for, this is the front door.
There is no database behind it. The entire catalogue lives in memory on regional servers in Singapore, Virginia, Oregon and Paris, and each request is answered by whichever is nearest. A typical query is served in a few milliseconds.
GET https://ceruleanjobs.com/api/search?query=atelier&location=paris
The machine-readable definition of everything below is published as an OpenAPI 3.1 document. Point your client generator at it rather than transcribing this page by hand.
If you are pointing an AI agent at Cerulean rather than writing a client by
hand, there are two shorter routes. /llms.txt
is a Markdown index of the site, in the llms.txt convention, that opens with
this endpoint and the OpenAPI document above. And /api/mcp is the same search
engine as a Model Context Protocol server, which most agents can connect to
without being told anything else — see The MCP server.
A first request
With no parameters at all, you get the most recent listings — every parameter below is optional.
curl "https://ceruleanjobs.com/api/search?query=atelier&location=paris&limit=2"
Parameters
Every parameter is optional, including limit and offset. There is no
required parameter and no minimum query — omitting a filter simply means no
constraint on that field, and omitting limit returns 12 results.
Every example below is a live link — click it to see the JSON in your browser.
Searching
| Parameter | Description | Example |
|---|---|---|
query | Free-text relevance search across title, description, qualifications, responsibilities, skills, experience, culture and benefits. Titles are weighted most heavily. | atelier+manager |
slug | The slug of a single listing. Returns that one listing, so a url you followed leads back to structured data. It is a filter like any other, not a bypass — combine it with a brand that does not match and you get nothing. | chanel-tailoring-atelier-manager-dubai-2026-03-16 |
similar | The slug of a listing. Switches to similarity mode — see below. | chanel-tailoring-atelier-manager-dubai-2026-03-16 |
lang | fr returns French titles and descriptions where a translation exists. Anything else is English. | fr |
Filtering
All filters are exact matches against a lowercased index.
| Parameter | Description | Example |
|---|---|---|
location | Matches a city, region or country — whichever the value happens to name. | paris — a citylombardy — a regionfrance — a country |
country | Country only. Narrower than location, and combinable with it. | italy |
brand | Brand slug. See all brands. | chanel |
employment-type | One of nine values — see below. | full-time |
seniority-level | One of ten values — see below. | senior |
industry | One of seventeen values — see below. | high-jewelry |
department | One of twenty-six values — see below. | creative-design |
Paging
| Parameter | Default | Maximum | Example |
|---|---|---|---|
limit | 12 | 48 | 48 |
offset | 0 | 10,000 | 24 |
Omit limit and you get 12 results, the same page size the site’s own search
uses; omit offset and you start at the first result.
Paging is native to the index, so a request at offset 9,000 costs no more than
one at offset 0. A large limit is cheaper than several small ones — please
prefer it.
Filter values
employment-type, seniority-level, industry and department each accept
a fixed set of values. Anything outside these sets is not an error — it simply
matches nothing. Every value below is a live link.
employment-type
| Value |
|---|
full-time |
part-time |
contract |
temporary |
seasonal |
internship |
volunteer † |
per-diem † |
other |
seniority-level
| Value |
|---|
intern |
entry-level |
junior |
mid-level |
senior |
lead |
supervisor |
manager |
director |
executive |
industry
| Value | |
|---|---|
fashion-apparel-leather | Fashion, Apparel & Leather Goods |
watches-horology | Fine Watches & Horology |
high-jewelry | High Jewelry |
perfumes-cosmetics-beauty | Perfumes, Cosmetics & Prestige Beauty |
eyewear-vision-care | Eyewear & Vision Care |
wines-spirits-gastronomy | Wines, Spirits & Gastronomy |
luxury-hospitality-travel | Luxury Hospitality & Experiential Travel |
selective-retailing | Selective Retailing & Department Stores |
luxury-real-estate | Luxury Real Estate, Property & Development |
yachting-aviation-mobility | Yachting, Aviation & Luxury Mobility |
pre-owned-vintage-luxury | Pre-Owned, Vintage & Circular Luxury |
media-art-culture | Media, Art & Cultural Institutions |
luxury-automotive | Luxury Automotive |
luxury-furniture-homewares | Luxury Furniture & Homewares |
high-end-audio-electronics | High-End Audio & Consumer Electronics |
premium-sporting-equipment | Premium Sporting & Outdoor Equipment |
photography-optics | Photography & Precision Optics † |
department
| Value | |
|---|---|
retail-operations | Retail & Boutique Operations |
wholesale-b2b | Wholesale, Commercial & B2B Sales |
merchandising-buying | Merchandising, Buying & Planning |
ecommerce-digital | E-Commerce, Digital & Data Analytics |
clienteling-crm | Clienteling, CRM & VIP Relations |
creative-design | Creative, Design & Styling |
research-innovation | Research, Innovation & Product Development |
visual-merchandising-architecture | Visual Merchandising, Store Design & Architecture |
heritage-archives | Heritage, Patrimony & Archives |
manufacturing-artisanal | Manufacturing, Artisanal & Industrial Operations |
supply-chain-logistics | Supply Chain, Logistics & Inventory |
culinary-food-production | Culinary & Food Production |
food-beverage-service | Food & Beverage Service |
front-office-guest-experience | Front Office, Concierge & Guest Experience |
housekeeping-cleaning | Housekeeping, Cleaning & Sanitation |
spa-wellness | Spa, Wellness & Recreation |
engineering-facilities | Engineering, Facilities & Maintenance |
real-estate-asset-management | Real Estate, Development & Asset Management |
corporate-affairs-csr | Corporate Affairs, Sustainability & CSR |
human-resources | Human Resources, People & Culture |
finance-accounting | Finance, Accounting & Revenue Management |
it-technology | IT & Technology Systems |
medical-clinical | Medical, Clinical & Specialized Technical |
legal-compliance | Legal & Compliance |
executive-management | Executive & General Management |
other | Other |
† Valid values that currently have no open listings, so they return
total: 0.
brand and country are open-ended rather than fixed — browse
all brands and all locations for the available
slugs.
How values are encoded
Send filter values lowercased, using + or %20 for spaces.
✓ location=new+york
✗ location=New%20York
Values are trimmed and truncated to 200 characters.
The CDN canonicalises the query string before the request reaches the service, so that equivalent requests share one cache entry: it drops any parameter not in the tables above, drops parameters with an empty value, collapses a repeated parameter to its first value, and lowercases the values it keeps. A parameter the endpoint does not read is therefore normally removed before it arrives, and produces no warning in production.
Nothing returns an error
The endpoint always answers 200. A filter that matches nothing gives you
total: 0, not a 404. A limit of banana falls back to 12. An offset of
five million is clamped to 10,000. This is deliberate — a search that finds
nothing is a valid answer to a valid question, and clients should not need
error handling for a typo in a query string.
But the endpoint tells you what it did. Anything ignored, clamped, cut short or
not recognised comes back in a warnings array beside the results, so you never
have to guess whether total: 0 is a typo of yours or an empty shelf of ours.
curl "https://ceruleanjobs.com/api/search?industry=fashion&limit=1"
{
"results": [], "total": 0, "hasMore": false, "limit": 1, "offset": 0,
"indexBuiltAt": "2026-08-20T04:12:37Z",
"warnings": [
{ "code": "unknown_enum_value", "parameter": "industry", "message": "\"fashion\" is not one of the 17 values industry accepts, so it matched nothing." }
]
}
industry=fashion is the classic one. It is close enough to look right and it
matches nothing, because the value the endpoint wants is
fashion-apparel-leather.
| Code | Raised when |
|---|---|
clamped | limit or offset was out of range, or was not a number at all. The value actually used is in the response’s own limit and offset fields. |
truncated | A parameter value was longer than 200 characters and was cut before searching. |
unknown_enum_value | A value outside the fixed set for employment-type, seniority-level, industry or department. It matched nothing. |
unknown_parameter | A parameter this endpoint does not read. It was ignored. Up to ten are reported. Rarely seen in production: the CDN normally strips an unrecognised parameter before the request arrives. |
similar_not_found | The similar slug has no embedding, so results were not ranked by similarity. If you also sent brand, it was applied rather than ignored — the one case where brand is not suppressed in similarity mode. |
Each warning carries a code you can branch on, the parameter it concerns
where there is one, and a message written for a human reading a log. The
array is absent entirely when there is nothing to report, which is the ordinary
case, and its order is fixed — the same request always produces the same bytes.
A warning is never an error. The status is still 200 and the results, if there
are any, are still there.
The response
This is a real response, from
?query=atelier&limit=1.
{
"results": [
{
"title": "Tailoring Atelier Manager",
"slug": "chanel-tailoring-atelier-manager-dubai-2026-03-16",
"url": "https://ceruleanjobs.com/jobs/chanel-tailoring-atelier-manager-dubai-2026-03-16/",
"description": "Chanel seeks a Tailoring Atelier Manager in Dubai to oversee couture tailoring operations, lead a team of artisans and ensure the highest standards of construction and client fit for bespoke pieces. The role combines technical leadership, atelier administration and cross-functional collaboration wit",
"city": "Dubai",
"country": "UAE",
"datePosted": "2026-03-16T12:00:29Z",
"paintingUrl": "https://ceruleanjobs.com/images/paintings/chanel-7.webp",
"brand": {
"slug": "chanel",
"name": "Chanel",
"url": "https://ceruleanjobs.com/careers/chanel/",
"logoUrl": "https://ceruleanjobs.com/images/logos/chanel.webp"
},
"region": "Dubai",
"employmentType": "other",
"seniorityLevel": "manager",
"industry": "",
"department": ""
}
],
"total": 451,
"hasMore": true,
"limit": 1,
"offset": 0,
"indexBuiltAt": "2026-08-20T04:12:37Z"
}
total is the number of listings matching your query across every page, not
the length of results.
url is absolute and canonical. It is the English ceruleanjobs.com page for
the listing, it is where our own canonical tags point, and it is the link to put
in your database and show to a reader. There is nothing to prepend and no host
to know.
localizedUrl sits beside it, and only appears when a localised page for that
listing genuinely exists — today, a French listing asked for with lang=fr,
which comes back with its cerulean.fr/emplois/ address as well as its
canonical one. Where there is no localised page there is no key, so
localizedUrl || url is always the right link to follow.
slug is the stable identifier. It is the listing’s primary key, it is what you
pass back to slug and similar, and it survives changes to the URL scheme —
which is why it is a field of its own rather than something you cut out of a
url with a regular expression.
description is a plain-text excerpt truncated to 300 characters, and is absent
when a listing has none. Fields we could not classify come back as empty strings
rather than being omitted — industry and department above are unclassified,
not missing.
Only seven keys can be absent altogether: description, localizedUrl,
datePosted, paintingUrl, brand.logoUrl, indexBuiltAt and warnings.
Everything else is always present, and results is always an array, even when
it is empty.
Filter values round-trip
employmentType, seniorityLevel, industry and department come back as the
same slugs you filter with — full-time, manager, fashion-apparel-leather,
ecommerce-digital. They are not display labels, and this page and the OpenAPI
document used to say otherwise. They were wrong; the API was always right.
The practical consequence is that any value in a result can be sent straight back in as a parameter.
curl "https://ceruleanjobs.com/api/search?industry=fashion-apparel-leather&department=ecommerce-digital"
brand.slug round-trips into brand, and slug into slug or similar, on
the same principle. So “more like this one, but only the internships” needs no
lookup table at your end — read the fields off a result and resend them.
city, region and country are the one presentational exception: they come
back title-cased for reading. location and country fold case before
matching, so those round-trip anyway.
Similar jobs
Pass a slug as similar and the ranking changes entirely. Instead of
matching words, it ranks by meaning and geography — semantic similarity against
a 512-dimension embedding, how close the location is, and how recent the
listing is.
curl "https://ceruleanjobs.com/api/search?similar=chanel-tailoring-atelier-manager-dubai-2026-03-16"
The subject job is excluded from its own results, so total needs no
adjustment. Any brand filter is ignored, because the point is to cross
between houses. If the slug is unknown, or too new to have an embedding, the
request quietly falls back to an ordinary recency-ranked search rather than
failing — and in that one case the brand filter is applied after all, with a
similar_not_found warning to tell you so.
The MCP server
Everything above describes a REST endpoint you call from code you write. If what
you are building is an agent rather than a client, there is a second door onto
the same index: /api/mcp, a
Model Context Protocol
server. Add it to any MCP client and the tools appear:
{
"mcpServers": {
"cerulean-jobs": {
"type": "http",
"url": "https://ceruleanjobs.com/api/mcp"
}
}
}
No key, no token, no sign-up — the same as the REST endpoint, and for the same reason.
Three tools, all read-only:
| Tool | What it does |
|---|---|
search_jobs | The endpoint above, with every filter as an optional argument |
get_job | One listing by slug |
find_similar_jobs | The neighbours of a listing, by meaning and geography |
The arguments are the parameters documented on this page, written snake_case
rather than kebab-case — employment_type rather than employment-type — and
the responses are the same envelope, warnings and indexBuiltAt included. The
slug in a result is what get_job and find_similar_jobs take, so a model can
follow a thread through the catalogue without composing a single URL.
The four taxonomy vocabularies are written into the tool schemas as enumerations.
That is the one place the MCP server behaves differently from /api/search: a
filter value outside its vocabulary is refused, with the valid values named in
the refusal, where the REST endpoint would return total: 0 and an
unknown_enum_value warning. Both are honest answers to the same mistake. The
refusal is the more useful one when the caller is a model, because it can correct
itself and ask again rather than having to interpret an empty shelf. limit and
offset are still clamped rather than refused.
It is stateless, and that is the point
The server runs at MCP revision 2026-07-28, which removed protocol-level
sessions. There is no handshake to complete before the first call, no session
identifier to hold, and no connection to keep open — a tool call is one HTTP
POST and one complete answer. Nothing is stored on our side between your calls,
so nothing can expire under you, and a client that drops a connection has lost
nothing.
The cost of having no handshake is that every request has to describe itself. Both of these travel on each call, and the server rejects a request where they disagree:
- the headers
MCP-Protocol-Version,Mcp-Method— andMcp-Namefortools/call— alongsideContent-Type: application/jsonand anAcceptcarrying bothapplication/jsonandtext/event-stream; protocolVersion,clientInfoandclientCapabilitiesinsideparams._meta.
A client library handles all of it. If you are debugging rather than building,
the exact shape is in
the OpenAPI document under
/api/mcp, and the server also publishes that document to connected clients as
a resource.
How results are ranked
An ordinary search blends three signals: text relevance, how recently the listing appeared weighted by the standing of the house, and how close the listing sits to your query in meaning. Similarity mode drops text relevance and leans on meaning and location instead.
The practical consequence is that recency matters. A perfect keyword match from three months ago will sit below a good match from this morning.
Freshness and caching
The index is rebuilt roughly every four hours, so results can trail the site by up to one cycle. Responses are cached at the edge for an hour.
You do not have to guess which cycle you got. Every response carries
indexBuiltAt, an RFC 3339 timestamp of the index build that answered it.
"indexBuiltAt": "2026-08-20T04:12:37Z"
If you are diffing our data against your own, that is the number to compare —
not the time you made the request, which the edge cache can put an hour ahead of
it. Two identical requests inside the same cached hour report the same
indexBuiltAt, and a change in that value is the honest signal that there is
something new to fetch.
Terms and attribution
The listings belong to the houses that are hiring. We gather them, translate and classify them, and keep them current. We do not own them and we do not license them onward.
The data from this endpoint is offered for personal and editorial use. Search it, cache it, quote it, and build a tool, a study or an agent on top of it. If what you have in mind is commercial, that is not a no — write to us first, so we both know where we stand.
In return, two things. Credit Cerulean where the data is visible. And link each
listing to its url, so a reader can reach the house’s own application form.
That link is the reason the endpoint is open at all.
One caution, and it matters more than the rest. Listings go stale quickly. A closed listing keeps its page for ninety days and is then gone, so a copy you refresh monthly will be showing jobs nobody can apply for. Re-query rather than republish an old snapshot.
Being a good citizen
Crawlers are welcome here — we have deliberately left the endpoint open. Two requests in return.
Send a User-Agent that identifies your project and gives us a way to reach
you, so we can get in touch if something goes wrong at your end or ours. And
ask for large pages rather than many small ones; limit=48 costs us no more
than limit=1.
Three more things, each of which will save you requests.
paintingUrl returns the full-size painting, 1024 square. Insert a size segment
for a smaller one — 256, 512 or 768:
https://ceruleanjobs.com/images/paintings/chanel-7.webp
https://ceruleanjobs.com/images/paintings/256/chanel-7.webp
Every open /jobs/{slug}/ page carries a schema.org JobPosting block as
JSON-LD in its head. It is the richest single-listing record we publish, with
more in it than the search response returns, so if you have followed a url
there, read that rather than the HTML around it.
And sitemap-index.xml is the
route to the whole corpus if you truly want every page. If what you want is the
data, though, limit=48 and offset will get you there in a small fraction of
the requests.
If you are building something substantial, we would genuinely like to hear about it. Write to us via the about page.
Changelog
Every response carries X-Cerulean-Api-Version, which reads 2 today. There is
no key and no sign-up, so there is no list of you for us to email — this section
is the notification. If you depend on this endpoint, watch this page.
Additive changes ship whenever they are ready: a new field, a new parameter, a new warning code, a new filter value. Ignore what you do not recognise and nothing of yours breaks.
Breaking changes get a version bump and an entry here. Removing a field, renaming one, or changing what one means.
2.1.0 — 21 August 2026
/api/mcpis new: a Model Context Protocol server over the same index, described in The MCP server. Nothing about/api/searchchanged, andX-Cerulean-Api-Versionstill reads2.- The OpenAPI document gains an
/api/mcppath covering the HTTP envelope the protocol requires.
2.0.0 — 20 August 2026
urlandpaintingUrlare now absolutehttps://ceruleanjobs.com/…links. They were paths, and every client had to know the host to use them.brand.hasLogo, a boolean, is gone.brand.logoUrlreplaces it and carries the same present-or-absent fact along with the address.brand.urlis new: the house’s careers page on Cerulean.localizedUrlis new, and appears when the listing has a page on a localised site — today,cerulean.frwithlang=fr.indexBuiltAtis new: when the index that answered you was built.warningsis new: what the endpoint clamped, cut, ignored or did not recognise.slugis new as a query parameter — the exact lookup of a single listing.datePostedis omitted when we do not know it. It used to read1970-01-01T00:00:00Z.resultsis now always an array. A degraded response used to sendnull, which broke clients that called.lengthon it.- Two new response headers:
X-Cerulean-Api-Version, and aLinkheader pointing at the OpenAPI document. - The documented values of
employmentType,seniorityLevel,industryanddepartmentare corrected. This page and the OpenAPI document showed display labels such asFashion; the API has always returned filter slugs such asfashion-apparel-leather. Only the documentation changed — see filter values round-trip.