---
title: "Developers - Air Market"
description: "The developer portal for Air Market: a quickstart, the described HTTP API, the MCP endpoint, Markdown content negotiation, and why there is no API key."
canonical: "https://airmarket.ai/developers"
---

[Air Market](/) / Developers

# Developers - Air Market

Everything this store renders, it also serves in a form a program can read: a described HTTP API, a Model Context Protocol endpoint, two feeds, and a Markdown copy of every page. Reads need no account, no API key and no preflight, and nothing on this page is a plan - every address below is answering now.

## Quickstart

Read the catalogue with one request. No header, no key, no account.

```
curl -s https://airmarket.ai/v1/listings
```

That answers a page of listings with facet counts and a keyset cursor. One listing in full, by its slug:

```
curl -s https://airmarket.ai/v1/listings/interview-me
```

Every route, every parameter and every response shape is described at [/v1/openapi.json](/v1/openapi.json). That document is generated from the routes themselves, so it cannot describe a route this store does not serve.

## Connect an agent

The store answers the Model Context Protocol at `https://airmarket.ai/mcp` over streamable HTTP, with four read-only tools: `search_catalog`, `get_listing`, `get_security_report` and `list_categories`. One command adds it to Claude Code:

```
claude mcp add --transport http airmarket https://airmarket.ai/mcp
```

The walkthrough, including what is not sent, is on [Connect Claude to Air Market](/connect).

## Authentication

**There is no API key, and there is nothing to sign up for.** Every read route on this store is anonymous: the catalogue, search, categories, jobs, a listing, a security report, the feeds and the MCP tools all answer an ordinary unauthenticated request. What a program can see is exactly what a visitor to the website can see - an unpublished listing is absent rather than hidden.

The routes that DO need a session are the ones that act as a person: publishing, reviewing, saving, and the creator studio. Those use the same first-party session cookie the website uses and are not a public integration surface. If you need one, say so - a key programme is a decision this store has not made, and inventing one here would be documenting a door that does not open.

## The endpoints

**What is served, and what each one is for**

|  |  |
| --- | --- |
| GET /v1/openapi.json | [Every route](/v1/openapi.json), its parameters and its response shape. |
| GET /v1/listings | Browse the catalogue: filters, facet counts, a keyset cursor. |
| GET /v1/listings/:slug | One listing in full, including what its author declared it needs. |
| GET /v1/search | Words, and the job a phrase resolves to. |
| GET /v1/categories | Every category with how many listings it holds. |
| GET /v1/jobs | The job tags a listing can carry. |
| GET /v1/catalog.json | [The whole catalogue as one object](/v1/catalog.json) - what the website itself renders from. |
| GET /feed.json | [Recently published and updated listings](/feed.json), as JSON Feed 1.1. |
| GET /feed.xml | [The same listings](/feed.xml), as Atom. |
| POST /mcp | The Model Context Protocol endpoint. Streamable HTTP, four read-only tools. |
| GET /llms.txt | [What this catalogue is](/llms.txt) and where to read it, for a model. |
| GET /sitemap.xml | [Every URL worth a request](/sitemap.xml), from the same catalogue. |
| GET /robots.txt | [Which crawlers are welcome](/robots.txt), named one by one. |

## Markdown instead of HTML

Every page of this site answers its own canonical URL with Markdown when you ask for it, per the `Accept: text/markdown` convention ([acceptmarkdown.com](https://acceptmarkdown.com), RFC 9110 content negotiation with the RFC 7763 media type). That is the same URL a browser gets HTML from - there is no second address to discover.

```
curl -s -H "Accept: text/markdown" https://airmarket.ai/developers
```

- The response carries `Content-Type: text/markdown; charset=utf-8` and `Vary: Accept`, so a cache between us keeps the two representations apart.
- Quality values are honoured: `Accept: text/markdown;q=0, text/html` gets HTML, not Markdown.
- An `Accept` this site cannot satisfy at all gets `406 Not Acceptable` with a plain-text list of what it can serve, rather than a silent fallback.
- Each page also advertises its Markdown sibling as `<link rel="alternate" type="text/markdown">`, for a client that looks for a link rather than sending a header.

## Errors, limits and caching

- Errors are **RFC 7807 problem details**: a JSON object with `type`, `title`, `status` and `detail`, under `Content-Type: application/problem+json`.
- A path this store does not serve answers a real `404`, not the app shell. A probe is a probe, and it gets an honest answer.
- Reads carry a generous per-caller ceiling, set per deployment rather than published as a figure here. Over it, the answer is `429` with the same problem-details body.
- The catalogue object is `ETag`ed and revalidates: send `If-None-Match` and a caller that already has the current catalogue gets `304` and no body.
- Agents reading the catalogue in a loop are traffic this store wants. There is no crawl-delay in `robots.txt` and no bot wall in front of any read route.

### Throttle from the headers, not from a guess

Every response says where the caller stands against the ceiling, so a client can slow down before it is refused rather than after:

- `RateLimit-Limit`: the ceiling for the tier this request fell into.
- `RateLimit-Remaining`: how many are left in the current window.
- `RateLimit-Reset`: seconds until the window resets.
- `RateLimit-Policy`: the ceiling and its window together, as `<quota>;w=<seconds>`.
- On a `429`, `Retry-After` as well, in seconds.
- The older `X-RateLimit-*` spellings still travel beside these, so nothing that reads them breaks.

Reads and writes are counted separately, so a burst of reads cannot spend a caller's write allowance - and the policy header names the tier that actually counted the request rather than a single figure for the whole API.

## Versioning and deprecation

**The version is in the path.** Everything under `/v1` is version one. A change that would break a client written against `/v1` gets a new prefix rather than being made in place; a change that only ADDS a field does not, so write clients that ignore fields they do not recognise.

**A route on its way out says so in its own responses.** There is no mailing list to join and no changelog to poll - the notice travels on the response, which is the one thing every caller already reads:

- `Deprecation: <HTTP-date>` ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745.html)): the date the route became deprecated. A date rather than a boolean, so a new notice is distinguishable from one you have already seen.
- `Sunset: <HTTP-date>` ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594.html)): the date it stops answering. Absent until a date is actually decided - a sunset header is a promise, and inventing one is worse than saying nothing.
- `Link: <url>; rel="deprecation"`: where the replacement is described.
- The same declaration marks the operation `deprecated: true` in [/v1/openapi.json](/v1/openapi.json), with the dates beside it, so a generated client warns at compile time.

**Nothing in `/v1` is deprecated today**, so no response carries either header yet. That is the honest state rather than an omission: watch for those two headers and a removal cannot arrive without warning.

## Trying it without changing anything

There is no separate sandbox host, and that is a statement about the API rather than a gap in it: **every route a stranger can reach is a read**. There is no public write endpoint to guard, nothing to bill, and no state a request can leave behind, so the live catalogue IS the sandbox and any of the commands on this page is safe to run against it.

To develop against a catalogue of your own instead of this one, the whole store runs locally: it is a Postgres database, an API, a worker and a static site, brought up with one `docker compose up` and seeded from a committed fixture. The catalogue you get is the fixture, so nothing you do reaches a real listing.

## Where to go next

- [/v1/openapi.json](/v1/openapi.json) if you are writing a client.
- [/llms.txt](/llms.txt) if you are pointing a model at the domain.
- [/.well-known/agent-instructions.md](/.well-known/agent-instructions.md) if you are writing the agent itself: when to reach for this catalogue, and the calling conventions in one file.
- [Connect Claude to Air Market](/connect) if you want the catalogue inside a conversation.
- [Discover](/discover) if you would rather just look at what is here.
- [About](/about) and [Contact](/contact) for who operates this store and how to reach them.

---

Canonical URL: <https://airmarket.ai/developers>
