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

ParameterDescriptionExample
queryFree-text relevance search across title, description, qualifications, responsibilities, skills, experience, culture and benefits. Titles are weighted most heavily.atelier+manager
slugThe 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
similarThe slug of a listing. Switches to similarity mode — see below.chanel-tailoring-atelier-manager-dubai-2026-03-16
langfr returns French titles and descriptions where a translation exists. Anything else is English.fr

Filtering

All filters are exact matches against a lowercased index.

ParameterDescriptionExample
locationMatches a city, region or country — whichever the value happens to name.paris — a city
lombardy — a region
france — a country
countryCountry only. Narrower than location, and combinable with it.italy
brandBrand slug. See all brands.chanel
employment-typeOne of nine values — see below.full-time
seniority-levelOne of ten values — see below.senior
industryOne of seventeen values — see below.high-jewelry
departmentOne of twenty-six values — see below.creative-design

Paging

ParameterDefaultMaximumExample
limit124848
offset010,00024

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-leatherFashion, Apparel & Leather Goods
watches-horologyFine Watches & Horology
high-jewelryHigh Jewelry
perfumes-cosmetics-beautyPerfumes, Cosmetics & Prestige Beauty
eyewear-vision-careEyewear & Vision Care
wines-spirits-gastronomyWines, Spirits & Gastronomy
luxury-hospitality-travelLuxury Hospitality & Experiential Travel
selective-retailingSelective Retailing & Department Stores
luxury-real-estateLuxury Real Estate, Property & Development
yachting-aviation-mobilityYachting, Aviation & Luxury Mobility
pre-owned-vintage-luxuryPre-Owned, Vintage & Circular Luxury
media-art-cultureMedia, Art & Cultural Institutions
luxury-automotiveLuxury Automotive
luxury-furniture-homewaresLuxury Furniture & Homewares
high-end-audio-electronicsHigh-End Audio & Consumer Electronics
premium-sporting-equipmentPremium Sporting & Outdoor Equipment
photography-opticsPhotography & Precision Optics †

department

Value
retail-operationsRetail & Boutique Operations
wholesale-b2bWholesale, Commercial & B2B Sales
merchandising-buyingMerchandising, Buying & Planning
ecommerce-digitalE-Commerce, Digital & Data Analytics
clienteling-crmClienteling, CRM & VIP Relations
creative-designCreative, Design & Styling
research-innovationResearch, Innovation & Product Development
visual-merchandising-architectureVisual Merchandising, Store Design & Architecture
heritage-archivesHeritage, Patrimony & Archives
manufacturing-artisanalManufacturing, Artisanal & Industrial Operations
supply-chain-logisticsSupply Chain, Logistics & Inventory
culinary-food-productionCulinary & Food Production
food-beverage-serviceFood & Beverage Service
front-office-guest-experienceFront Office, Concierge & Guest Experience
housekeeping-cleaningHousekeeping, Cleaning & Sanitation
spa-wellnessSpa, Wellness & Recreation
engineering-facilitiesEngineering, Facilities & Maintenance
real-estate-asset-managementReal Estate, Development & Asset Management
corporate-affairs-csrCorporate Affairs, Sustainability & CSR
human-resourcesHuman Resources, People & Culture
finance-accountingFinance, Accounting & Revenue Management
it-technologyIT & Technology Systems
medical-clinicalMedical, Clinical & Specialized Technical
legal-complianceLegal & Compliance
executive-managementExecutive & General Management
otherOther

† 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.

CodeRaised when
clampedlimit 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.
truncatedA parameter value was longer than 200 characters and was cut before searching.
unknown_enum_valueA value outside the fixed set for employment-type, seniority-level, industry or department. It matched nothing.
unknown_parameterA 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_foundThe 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:

ToolWhat it does
search_jobsThe endpoint above, with every filter as an optional argument
get_jobOne listing by slug
find_similar_jobsThe neighbours of a listing, by meaning and geography

The arguments are the parameters documented on this page, written snake_case rather than kebab-caseemployment_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 — and Mcp-Name for tools/call — alongside Content-Type: application/json and an Accept carrying both application/json and text/event-stream;
  • protocolVersion, clientInfo and clientCapabilities inside params._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/mcp is new: a Model Context Protocol server over the same index, described in The MCP server. Nothing about /api/search changed, and X-Cerulean-Api-Version still reads 2.
  • The OpenAPI document gains an /api/mcp path covering the HTTP envelope the protocol requires.

2.0.0 — 20 August 2026

  • url and paintingUrl are now absolute https://ceruleanjobs.com/… links. They were paths, and every client had to know the host to use them.
  • brand.hasLogo, a boolean, is gone. brand.logoUrl replaces it and carries the same present-or-absent fact along with the address.
  • brand.url is new: the house’s careers page on Cerulean.
  • localizedUrl is new, and appears when the listing has a page on a localised site — today, cerulean.fr with lang=fr.
  • indexBuiltAt is new: when the index that answered you was built.
  • warnings is new: what the endpoint clamped, cut, ignored or did not recognise.
  • slug is new as a query parameter — the exact lookup of a single listing.
  • datePosted is omitted when we do not know it. It used to read 1970-01-01T00:00:00Z.
  • results is now always an array. A degraded response used to send null, which broke clients that called .length on it.
  • Two new response headers: X-Cerulean-Api-Version, and a Link header pointing at the OpenAPI document.
  • The documented values of employmentType, seniorityLevel, industry and department are corrected. This page and the OpenAPI document showed display labels such as Fashion; the API has always returned filter slugs such as fashion-apparel-leather. Only the documentation changed — see filter values round-trip.