Developer API
Momberger Data API
Airport intelligence as JSON: editions and articles, the projects database, airports, tenders and the events calendar. 69 operations a subscriber can call, 62 more for editorial accounts, one bearer token for all of them.
At a glance
REST + OpenAPI
One JSON API, described by its own 3.1 document
Bearer auth
Session tokens or long-lived mbg_live_ API keys
Entitlements
GET /v1/me tells you what your token may reach
No quota yet
No per-key request limit is enforced today
Authentication
Every request carries a bearer token. It is either a browser session token or a long-lived API key that begins mbg_live_, minted with POST /v1/account/api-keys. The plaintext key is in that one response and is never retrievable again — the API stores a hash and the first sixteen characters, so a lost key is revoked and replaced rather than recovered.
curl -H "Authorization: Bearer mbg_live_..." /v1/me
A malformed or expired token is 401. A valid token missing the entitlement is 403 — except under /v1/admin, which answers 404 to a non-admin, because a 403 would confirm that the resource exists. Errors are application/problem+json:
{ "title": "Forbidden", "status": 403, "detail": "archive is not included in this subscription" }Endpoints
Grouped by the tag each operation carries in the spec, so the same sections appear in the OpenAPI reference. Lists take limit (maximum 200) and offset, and answer with one named array — { "articles": [...] } — with no total and no page metadata. Plural query parameters are comma-separated, never repeated: a repeated one silently keeps only the first value and returns a wider result set that looks plausible.
Identity
authWho the token belongs to and what it may reach. Call /v1/me first: the features array it returns is the same one the gates below are checked against, so an integration can find out what it is allowed to ask for instead of discovering it as a 403.
The publication
editorialEditions, the articles in them, and the archive search. This is the read path the newsletter itself is built on.
Airport projects
projectsThe projects database: what is being built where, by whom, at what value. facets returns the filter vocabulary with counts, so a client never has to hard-code a status or a country list.
Airports
airportsAn airport is addressable by database id or by IATA code, so /v1/airports/AMS works without a lookup first. Coverage and projects hang off the same reference.
Worldwide Airport Tenders
tendersGated on the wat add-on. The list and its CSV export take the same filters, so an export matches the list that produced it. There is no category: this schema records that idea as taxonomy terms, so use terms.
Events calendar
calendarIndustry events, both curated and extracted from reporting. upcoming=true drops everything in the past.
Reference data
referenceOne call for every controlled vocabulary the write endpoints validate against: regions, countries, project types and topics. Fetch it once and cache it — it changes when a migration changes it, not per request.
Reading list and watchlist
libraryPer-user state: saved articles, watched projects, read history and delivery preferences. Everything here is scoped to the token's own user, so there is no id to pass and no way to read someone else's.
Alerts and notifications
alertsStanding queries that mail a digest when something matches, plus the status-change feed for watched projects. /v1/notifications/count is the cheap call — poll that, and fetch the list when it moves.
Account, seats and keys
accountThe subscription's own administration. /v1/plans is the only operation on this page that needs no token at all — and it omits inactive plans, so it is a narrower list than the one on the pricing page. Key management is gated on api or mcp, either of which is enough: MCP is sold as its own add-on and uses the same bearer credential.
Imagery
mediaCover images, resolved per article. Ask for a batch by passing a comma-separated list of article ids to /v1/media/covers — one call for a whole page of rows — then fetch each image by its own id, optionally at a width with ?w=. The API resolves a pinned cover if an editor set one and derives one from the article's airport, country or company otherwise, so a client never has to decide what to show.
Subscriptions and checkout
billingPrices come from the catalogue tables, so a page cannot show a total the API would not charge: ask /v1/billing/quote rather than computing one. /v1/billing/config and /v1/billing/quote need no credential — a client reads mode first and does not offer a checkout button a deployment would answer 503 to. Mollie's own notification endpoint is not listed here; see the header of this file.
Invoicing and procurement
procurementThe one unauthenticated write in this API, and deliberately so: the person filling the form in works in accounts payable and has no account, so requiring one would mean "sign in to ask us to invoice you". A resubmission from the same address inside ten minutes returns the request already on file rather than filing a second one. The queue an editor works it in is admin-only.
System
systemUnauthenticated. Reports the API's own health and whether it can reach its database.
Editorial endpoints
62 operations under /v1/admin, for the publication's own editors. Same bearer auth; the token's user has to be an administrator, and a token that is not gets 404 rather than 403. Summarised by resource here rather than listed one operation at a time — the request bodies are in the OpenAPI reference, and they change more often than this page could track.
Editions
5 opsPOST · PATCH · DELETE /v1/admin/editionsCreate a draft, update it, publish it, delete it, and bulk-import a historical issue from a pasted export (POST /v1/admin/editions/import).
Newsletter
4 opsGET · POST /v1/admin/editions/{id}/newsletter/…Dry-run an edition's digest to see who would receive it and send nothing, send one copy to a named address, queue it for every entitled subscriber, or read back its delivery state — sent, queued, failed, dead.
Articles
3 opsPOST · PATCH · DELETE /v1/admin/articlesAdd an article to an edition, edit it, remove it.
Article entity links
4 opsGET · PUT /v1/admin/articles/{id}/…Replace the set of topics and tags, airports, or companies attached to an article, and read all three back together.
Article ↔ project links
4 opsGET · PUT · POST /v1/admin/articles/{id}/projectsRead and replace an article's project links, ask for ranked candidate matches, or auto-link every confident one.
Article covers
2 opsPUT · DELETE /v1/admin/articles/{id}/coverPin an image to an article, or release it back to the cover the API resolves automatically from the airport, country or company.
Media library
5 opsGET · POST · PATCH · DELETE /v1/admin/mediaUpload, relabel and delete image assets. The list can be filtered to the ones bound to nothing, which is how orphans get found.
Projects
7 opsPOST · PATCH · DELETE · PUT /v1/admin/projectsFull CRUD, plus three replace-the-set endpoints for a project's airports, companies and terms, and one that reads all three back.
Tenders
8 opsGET · POST · PATCH · DELETE · PUT /v1/admin/tendersThe same shape as projects, plus a read of one tender in any state — including unpublished.
Editorial inbox
6 opsGET · PATCH · POST /v1/admin/inboxPre-screened RSS candidates, highest score first. Approve, skip, reopen, bulk-skip, or promote one into a draft article. Nothing here publishes itself: promotion produces a draft, and a person publishes it.
News sources
5 opsGET · POST · PATCH · DELETE /v1/admin/sourcesThe ingest whitelist, plus POST /v1/admin/sources/poll to run every enabled source now.
Calendar
4 opsGET · POST · PATCH · DELETE /v1/admin/calendarEvent CRUD, and a list that includes events already in the past.
Metadata suggestion
1 opsPOST /v1/admin/suggest/metadataExtracts topics, tags, airports, region and country from a block of text. It suggests; it never writes to an article.
GET /v1/admin/email/usageOutbox depth, dead letters, and what has been sent in the last hour and day against the configured send budget.
Procurement
3 opsGET · POST /v1/admin/procurementThe queue behind POST /v1/procurement-requests: a filtered list, one request with its review trail, and a review that appends to that trail. The review is a POST rather than a PATCH because sending it twice records two reviews — it is not idempotent and should not look like it is.
Agent-ready spec
Paste this whole block into Claude, ChatGPT, Cursor or any coding agent. It is self-contained: auth, the response envelope, pagination, the entitlement model, every query parameter that is easy to get wrong, and curl examples that run against this deployment.
# Momberger Airport Information — REST API
Base URL:
Spec: /openapi.json (OpenAPI 3.1)
Docs: /docs (the same spec, rendered)
The spec is the contract. This block is a hand-written summary of it and the
API is under active development, so fetch /openapi.json if the two disagree.
AUTH
Every request except GET /healthz and GET /v1/plans:
Authorization: Bearer <token>
<token> is either a browser session token or a long-lived API key beginning
`mbg_live_`. Mint a key with POST /v1/account/api-keys, which needs
the api or the mcp entitlement. The plaintext value is in that one response
and is never retrievable again — the API keeps a hash and the first
16 characters.
A malformed or expired token is 401. A valid token that lacks the entitlement
is 403 — except under /v1/admin, which answers 404 to a non-admin so that the
response does not confirm the resource exists.
CONTENT
Requests: application/json
Responses: application/json
Errors: application/problem+json (RFC 9457)
{ "title": "...", "status": 403, "detail": "..." }
RESPONSE SHAPE
A list endpoint answers with one named array, not a generic envelope:
GET /v1/editions -> { "editions": [...] }
GET /v1/search -> { "articles": [...] }
GET /v1/projects -> { "projects": [...] }
GET /v1/airports -> { "airports": [...] }
GET /v1/tenders -> { "tenders": [...] }
GET /v1/calendar -> { "events": [...] }
GET /v1/alert-rules -> { "rules": [...] }
GET /v1/notifications -> { "notifications": [...], "unread": 3 }
GET /v1/account/api-keys -> { "keys": [...] }
GET /v1/plans -> { "plans": [...] }
An empty collection may come back as null rather than []. Normalise it.
There is no total count and no page metadata anywhere: page by asking for
limit+1 rows and seeing whether you got them.
PAGINATION
limit (default varies by endpoint, maximum 200)
offset (default 0)
A limit above 200 is rejected with 422 and a detail of "validation failed",
which names nothing — so clamp client-side.
LIST FILTERS ARE COMMA-SEPARATED, NOT REPEATED
?terms=capex,ppp correct
?terms=capex&terms=ppp silently keeps only the first, and returns a WIDER
result set that looks plausible
This applies to every plural query parameter.
ENTITLEMENTS
GET /v1/me returns { user_id, subscription_id, is_admin, features[] }.
features[] is drawn from: archive, projects, alerts, wat, api, mcp, exports,
ai_assistant. Check it before offering a call rather than handling the 403.
archive -> /v1/search, /v1/search/facets
projects -> /v1/projects*
alerts -> /v1/alert-rules*
wat -> /v1/tenders*
exports -> the three CSV endpoints, in addition to the gate covering the
content: projects+exports, wat+exports, archive+exports.
Holding one without the other is a real state, so both are
checked, and the download button is hidden on exports.
Admins carry every feature.
A trial is a third answer, not a missing one: /v1/me also returns previews[],
and a gate listed there is narrowed rather than refused — the archive to the
latest published edition, projects to five rows, tenders to three.
READING THE PUBLICATION
GET /v1/editions?limit=&offset=
Editions newest first: id, number, publish_date, status, summary,
top_developments[], key_trends[], article_count.
GET /v1/editions/{id}
GET /v1/editions/{id}/articles
An article carries: id, headline, slug, content, takeaways[],
why_it_matters, region, region_slug, country_code, source_name,
edition_id, publish_date, featured, is_saved.
GET /v1/articles/{id}
GET /v1/articles/{id}/related?limit= (default 4)
Scored on shared topics, airports, tags and place. An article carrying no
entity links returns an empty list — the honest answer, not four
arbitrary stories from the same region.
GET /v1/search
q websearch syntax — quoted phrases and OR are supported
region region SLUG, not the display name
terms comma-separated topic or tag slugs. ALL must match:
terms=a,b means both, never either.
airport airport reference (database id or IATA code)
year four-digit year, matched on the edition's publish date
edition edition NUMBER, not its id
mine true restricts to the reader's favourite topics,
regions and airports
include_events calendar events are excluded by default — they are a
calendar, not reporting. true folds them in.
limit default 50, maximum 200
offset default 0
GET /v1/search/facets
The same filter vocabulary with counts ({ value, label, count }), so a
client never hard-codes a region or topic list.
GET /v1/search/export [archive + exports]
The same filters, as text/csv, streamed and uncapped. Headline, why it
matters, topics, source and a link — not the article text.
AIRPORT PROJECTS
GET /v1/projects
q free text
country ISO 3166-1 alpha-2 country code
status planned | ongoing | complete | cancelled | on_hold
region region slug, as /v1/projects/facets reports it
type project type slug, from the same facets
terms COMMA-SEPARATED topic or tag slugs. ALL must match, so
terms=cargo,technology narrows rather than widens. A
repeated ?terms=a&terms=b keeps only the first.
min_value lowest reported value. NOT converted to a common
max_value currency: each project carries the amount its source
stated, in value_currency. A project with no stated
value is excluded by either bound.
year_from expected completion year, inclusive
year_to
sort title_asc (default) | value_desc | value_asc |
year_desc | year_asc | airport_asc | updated_desc
company_id uuid; resolves through aliases and subsidiaries.
A malformed one is 400, an unmatched one is [].
parent_project_id uuid; the packages under a programme
limit default 50, maximum 200
offset default 0
A project carries: id, title, description, airport, iata_code,
country_code, region, project_type, status, value_amount, value_currency,
value_basis, announced_on, expected_completion_year, parent_project_id,
is_watched.
GET /v1/projects/facets
GET /v1/projects/{id}
GET /v1/projects/export [projects + exports]
The same filters, as text/csv, streamed and uncapped. Ask for the
export rather than paging /v1/projects to build one — there is no row
limit here, and a truncated export is indistinguishable from a complete
one, which is the bug this endpoint exists to avoid.
AIRPORTS
GET /v1/airports?q=&limit=&offset=
GET /v1/airports/{ref} ref is a database id OR an IATA code
GET /v1/airports/{ref}/articles?limit=&offset=
GET /v1/airports/{ref}/projects
TENDERS [requires the wat entitlement]
GET /v1/tenders
q free text
region region slug, as /v1/tenders/facets reports it
country ISO 3166-1 alpha-2 country code
type rfp | tender | framework | eoi
status open | closing_soon | closed | awarded | cancelled
terms COMMA-SEPARATED topic or tag slugs; ALL must match. This is
where production's "category" went — there is no separate
category vocabulary here.
sort deadline_asc (default) | deadline_desc | value_desc |
published_desc
limit default 50, maximum 200
offset default 0
GET /v1/tenders/facets
The same vocabulary with counts. topics stands in for categories.
GET /v1/tenders/{id}
GET /v1/tenders/export [wat + exports]
The same filters, as text/csv, streamed and uncapped. It also carries a
"bidders" column the list does not — who bid and lost is the buying
signal, and it is only in the export.
EVENTS
GET /v1/calendar?upcoming=&limit=
upcoming defaults to TRUE, so the past is hidden unless you ask.
limit defaults to 200. Answers { "events": [...] }.
REFERENCE
GET /v1/reference
Regions, countries, project types and topics — every controlled
vocabulary the write endpoints validate against. Cache it.
PER-USER STATE
GET /v1/library/saved?limit=&offset=
PUT /v1/articles/{id}/save
DELETE /v1/articles/{id}/save
POST /v1/articles/{id}/view
GET /v1/library/watchlist
PUT /v1/projects/{id}/watch
DELETE /v1/projects/{id}/watch
GET /v1/dashboard/most-read?limit=
GET /v1/dashboard/recently-viewed?limit=
GET /v1/preferences
PUT /v1/preferences
digest_frequency, digest_format, email_headlines_only, attach_pdf
ALERTS [requires the alerts entitlement]
GET /v1/alert-rules
POST /v1/alert-rules
PATCH /v1/alert-rules/{id}
DELETE /v1/alert-rules/{id}
GET /v1/notifications?unread_only=&limit=&offset=
GET /v1/notifications/count
POST /v1/notifications/read
ACCOUNT
GET /v1/plans no token required.
Inactive plans are omitted, and the price book travels with each plan
(per currency and interval) along with the buyable add-ons. Enterprise
is inactive and carries no prices, so it cannot be checked out.
GET /v1/account/profile
PATCH /v1/account/profile
GET /v1/account/subscription
GET /v1/account/members
POST /v1/account/members
DELETE /v1/account/members/{user_id}
GET /v1/account/interests
PUT /v1/account/interests
PUT /v1/account/interests/terms/{slug}
DELETE /v1/account/interests/terms/{slug}
GET /v1/account/api-keys
POST /v1/account/api-keys
DELETE /v1/account/api-keys/{id}
IMAGERY
GET /v1/media/covers?articles=<id>,<id>,<id>
Resolved covers for a batch of articles in one call. Pinned if an editor
set one, derived from the airport/country/company otherwise.
GET /v1/media/airports?airports=<id>,<id>
Imagery bound to airports themselves, for an airport profile — absent is
normal, and the caller draws its own cover from the airport id.
GET /v1/media/{id}?w=
The image bytes. ?w= is a closed ladder — 144, 288, 320, 640, 960, 1280 —
and anything else is a 400, because an arbitrary width is both a
cache-buster and a decode per request. Omit it for the original.
SUBSCRIPTIONS AND CHECKOUT
GET /v1/billing/config no token required.
{ checkout_enabled, mode } where mode is test | live | disabled. Read it
before offering a checkout button rather than offering one that 503s.
GET /v1/billing/quote?plan=¤cy=&interval=&extra_seats=&add_wat=
no token required. The authoritative total. Every figure comes from the
catalogue tables, so a page cannot show a price we would not charge.
POST /v1/billing/trial 409 if this account already has
a subscription.
POST /v1/billing/checkout owner or admin member. Returns a
Mollie redirect URL, which is not stored anywhere and expires.
GET /v1/billing/checkout/{id} the state of one checkout. It
reports; the provider's notification is what activates a subscription,
whether or not anyone returns to the page. 404, never 403, for a
well-formed id belonging to someone else.
POST /v1/billing/cancel owner only. Access continues to
current_period_end, because that period is paid for.
INVOICING
POST /v1/procurement-requests no token required.
For an organisation that cannot pay by card. A resubmission from the
same address inside ten minutes returns the request already on file
rather than filing a second one.
ADMIN [editorial accounts, 404 to others]
/v1/admin/editions (and their newsletter dry-run, test, send and stats),
/v1/admin/articles (and their terms, airports, companies, project links and
covers), /v1/admin/media, /v1/admin/projects, /v1/admin/tenders,
/v1/admin/inbox, /v1/admin/sources, /v1/admin/calendar,
/v1/admin/procurement, /v1/admin/suggest/metadata, /v1/admin/email/usage
62 operations. See /docs for their request bodies.
EXAMPLES
# 1. What am I allowed to call?
curl -H "Authorization: Bearer mbg_live_..." /v1/me
# 2. The newest edition and everything in it
curl -H "Authorization: Bearer mbg_live_..." "/v1/editions?limit=1"
curl -H "Authorization: Bearer mbg_live_..." /v1/editions/<id>/articles
# 3. Capex and PPP coverage of Africa in 2026
curl -H "Authorization: Bearer mbg_live_..." \
"/v1/search?q=capex®ion=Africa&year=2026&terms=infrastructure-capex,ppp-finance&limit=50"
# 4. Ongoing projects in Kenya
curl -H "Authorization: Bearer mbg_live_..." \
"/v1/projects?country=KE&status=ongoing&limit=100"
# 5. Everything Momberger has written about Schiphol
curl -H "Authorization: Bearer mbg_live_..." /v1/airports/AMS/articles
# 6. Industry events still to come
curl -H "Authorization: Bearer mbg_live_..." "/v1/calendar?upcoming=true"
MCP server
Not available yet
There is no MCP endpoint on this API today. mcp is a real entitlement and appears in GET /v1/me, but nothing serves the protocol behind it — so rather than print a configuration snippet that would fail to connect, this section says what the server will be when it lands.
Two tools, the whole API
The design is settled even though the code is not. One tool per endpoint would spend most of a model's context on descriptions, so the server will expose two and let the agent compose calls itself:
search_api(query)— find operations in the catalogue with their parameters and examples.call_api(method, path, …)— invoke any/v1/*operation. Authentication and the entitlement gates apply exactly as they do over HTTP.
In the meantime
Every capable agent already knows how to make an HTTP request. Give it the spec block above and an API key and it can work the whole API today, without a protocol in between.
The OpenAPI document itself is at /openapi.json, which most agent frameworks can ingest directly.
API access is included in Professional
Ready to integrate?
Start a subscription, then mint a key from your account.