Metis Events API
Connect your booking, catering or CRM system to your Metis Room Planner account. Your software asks for a floorplan; your own team finishes it properly in Metis; the plan comes back as a PDF to attach to the BEO or send to the client.
What this API is for
You already run your events in your own booking system. What you don't have is a quick way to turn a booking into a room plan without re-typing everything into a separate drawing tool. That's the gap this API closes.
The unit of work is the plan request. Your system sends a room, a layout style, guest numbers and the event details. Metis creates the plan in your Metis account and puts it in front of your own planners — the people who know where the pillar is and which door the band loads in through. When they mark it complete, the plan is rendered to a PDF and a PNG your system pulls straight back into the booking.
Everything else — listing your venues and rooms, keeping occasions in step, opening Metis for someone — exists to support that loop.
"Occasion" means the event. A wedding, a conference, a gala dinner — the thing the booking is for. The API calls it an occasion rather than an event because "event" is already overloaded in most booking systems (and in HTTP), and because the name matches the Prismm/AllSeated contract that existing integrations are written against. Wherever you read occasion, think event.
Rooms are structure, not furniture. A Metis room holds walls, doors, windows, pillars and fixed features, with no tables or chairs. Furniture belongs to the event, so every plan starts from the empty room and is laid out for the numbers on the night.
The main flow
- Create the occasion (the event)
POST /v1/occasions— the event, its date, type and guest count. - Pick a room
GET /v1/venuesthenGET /v1/venues/{venueId}/rooms. Cache these; they change rarely. - Request the plan
POST /v1/occasions/{occasionId}/plan-requestswith the room, layout style, numbers and times. Status:requested. - Your planners do the work
Your team opens the request in Metis, finishes the plan and marks it complete.
Status:
in_progress→completed. - Collect the result
Poll
GET /v1/plan-requests?status=completed, then take the newest entry inversionsand download the PDF — or store its signedpdfShareUrl. - Changes happen
PATCHthe plan request when numbers or times move. Metis shows your team exactly what changed and the request goes back tochanges_requested, producing a new version when they finish.
Getting access
You issue your own keys, from your own Metis account. Nobody has to ask Metis for one.
- Sign in to Metis Room Planner as an owner or administrator of the account.
- Go to Profile → Integrations.
- Name the integration after the system that will use it — "our booking system", "the website" — and generate the key.
One key per integration. Each can be revoked on its own, so retiring one system never disturbs another, and you can see on that screen when each key was last used.
The key is shown once, at the moment it is created. Metis stores only a hash of it and genuinely cannot show it again — if it is lost, revoke it and generate another. Copy it with the button in the reveal dialog; the list afterwards shows only the first few characters, which identify the key but will not authenticate.
Keys beginning mk_test_ act on test data, so build against one of those first.
Writing software you intend to sell to other Metis subscribers? Talk to Metis about a partner key instead — one credential that works across the accounts that authorise you, rather than a key per customer.
Authentication
Every call carries your key as a bearer token over HTTPS:
Authorization: Bearer mk_live_YOUR_KEY_HERE
Keys beginning mk_test_ act on test data; mk_live_ keys act on the real account.
After the prefix come 40 random characters — never parse them, and never derive anything from them.
A key belongs to a connection: one Metis account. A key you generated on your own Integrations screen is pinned to your account, so you can ignore this entirely. Only a partner key serving several accounts needs to name the one it means:
Metis-Connection: con_8fK2mQpL4xRz9TbW3vNcYh7J
You don't need to look your connection id up, and you shouldn't hard-code it. Send the
key on its own and Metis resolves the account it is pinned to. If you want the id — to log it, or for
the Prismm-compatible routes, which take it as their user parameter — read it from
GET /v1/connection, which returns it as id.
Sending a stale one is the only way to get this wrong. Note that generating a brand-new
integration, rather than rotating an existing key, creates a new connection and revokes
the old — at which point a hard-coded id starts returning
403 connection_not_found even though the new key is perfectly valid.
Each key is granted only the scopes it needs. A call outside them returns
403 insufficient_scope — not something to retry, but a sign the key was issued without that
permission. Generate one that has it.
| Scope | Lets you |
|---|---|
venues:read | List venues, rooms and layout styles. |
occasions:read | Read occasions. |
occasions:write | Create and update occasions. |
floorplans:read | Read floorplans and plan requests, and download PDFs and PNGs. |
floorplans:write | Raise, change and cancel plan requests. |
links:create | Create links that open Metis for a person. |
GET /v1/connection tells you which account a key acts on and which scopes it holds. It's the
right call behind a "Test connection" button.
Keeping keys safe
An API key is a password to a venue's data. Treat it as one.
- Server-side only. Never put a key in browser JavaScript, a mobile app, a desktop binary or anything else an end user can read. If your front end needs data from Metis, proxy it through your own backend.
- Never in source control, log files, error reports, screenshots or support tickets. Load it from a secret store or an environment variable.
- One key per integration. Don't share a single key between two systems — the point of separate keys is that one can be revoked without disturbing the rest.
- Rotate when someone leaves, and on any suspicion. Generating a replacement and revoking the old key takes seconds on the Integrations screen, and revocation is immediate.
- Handle 401 properly. If a key stops working, stop retrying it and check the Integrations screen. Hammering a rejected key will get your IP rate-limited.
- Don't pass share links off as private. A
pdfShareUrlworks without a key for anyone who has it, for 30 days. That's the point, but treat it accordingly.
Quick start
Set two environment variables and check the key works:
export METIS_BASE="https://metisroomplanner.com/MetisEventsApi"
export METIS_API_KEY="mk_test_…" # Profile -> Integrations, in your Metis account
export METIS_CONNECTION="con_…" # only if your key is not pinned to one account
curl -sS "$METIS_BASE/v1/connection" -H "Authorization: Bearer $METIS_API_KEY"
The samples further down assume one small helper. In Node:
const BASE = process.env.METIS_BASE;
async function metisRaw(path, { method = "GET", body, idempotencyKey, ifMatch } = {}) {
const headers = {
"Authorization": `Bearer ${process.env.METIS_API_KEY}`,
"Metis-Connection": process.env.METIS_CONNECTION,
};
if (body) headers["Content-Type"] = method === "PATCH"
? "application/merge-patch+json"
: "application/json";
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
if (ifMatch) headers["If-Match"] = ifMatch;
return fetch(BASE + path, { method, headers, body: body ? JSON.stringify(body) : undefined });
}
async function metis(path, options) {
const response = await metisRaw(path, options);
if (!response.ok) {
// Errors are RFC 9457 problem documents. Branch on `code`, never on the message.
const problem = await response.json();
throw Object.assign(new Error(problem.title), problem, { status: response.status });
}
return response.status === 204 ? null : response.json();
}
And in C#:
var http = new HttpClient { BaseAddress = new Uri(Environment.GetEnvironmentVariable("METIS_BASE") + "/") };
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("METIS_API_KEY"));
http.DefaultRequestHeaders.Add("Metis-Connection", Environment.GetEnvironmentVariable("METIS_CONNECTION"));
Requests and responses
- Base URL
https://metisroomplanner.com/MetisEventsApi. HTTPS only; plain HTTP is redirected and then refused. - JSON in and out, UTF-8,
camelCaseproperty names. - Identifiers are opaque prefixed strings —
ven_8,room_26,occ_42,fp_118,pr_12,lay_58,con_…. Store them whole. Don't parse them, don't assume they're numeric, don't assume a length. - Dates and times come in three distinct shapes — see below. Read that section before you send a date.
- Ignore properties you don't recognise. New ones are added within
v1and that is not a breaking change. - Every response carries
X-Request-Id, echoed if you send your own. Log it. It's the fastest way for support to find your call.
Dates and times
Three different shapes, and mixing them up is the most common cause of a
400 validation_failed. Every one of them is a plain string in JSON.
| Kind | Format | Example | Meaning |
|---|---|---|---|
Calendar datedate |
YYYY-MM-DDISO 8601 / RFC 3339 full-date |
2027-06-12 |
The day the event is held. No time and no time zone — it is the date on the wall calendar at the venue, and it never shifts. Zero-pad month and day. |
Time of daystartTime, endTime |
HH:mm, 24-hour |
19:00, 23:30 |
Local time at the venue. No seconds, no offset, no am/pm. Zero-pad the
hour (09:00, not 9:00). An endTime earlier than
startTime means the event runs past midnight. |
TimestampcreatedAt, updatedAt,
completedAt, expiresAt… |
RFC 3339, always UTC, always Z |
2026-09-16T10:44:07Z |
A moment in time, recorded by Metis. Read-only — you never send one. Convert to local time for display if you need to. |
Why the event date has no time zone. A wedding on 12 June is on 12 June regardless of
where the server, your process or the reader happens to be. Sending a full timestamp and letting a
library convert it is how an event silently moves to the previous evening. Format the calendar date
yourself — in JavaScript, d.toISOString().slice(0, 10) is UTC, not local, and will be the
wrong day for anyone east or west of Greenwich late in the day. Build it from the local parts instead.
Pagination
Lists take limit (1–100, default 25) and an opaque cursor. Follow
nextCursor until hasMore is false; the same next page is also given
as an RFC 8288 Link: <…>; rel="next" header. Never build a cursor yourself — an invalid
one returns 400 invalid_cursor.
Idempotency
Send Idempotency-Key: <uuid> on every POST. If the connection drops before
you see the response, retry with the same key: Metis returns the original response and adds
Idempotent-Replayed: true rather than doing the work twice. Keys are remembered for 24 hours.
Reusing a key with a different body returns 409 idempotency_key_reused — that's a bug
in your key generation, not a transient failure. Generate one key per logical operation and keep it with
the retry.
ETags and updates
Single resources come back with an ETag. Send it as If-None-Match on a read to
get a cheap 304 Not Modified, or as If-Match on a PATCH so your
update is rejected with 412 precondition_failed if someone changed the resource first. On a
412, re-read, re-apply your change and retry.
Updates are JSON Merge Patch (RFC 7396): send only the fields you're changing. null clears a
field that is allowed to be empty.
Rate limits
Authenticated partners get 600 requests per 60 seconds, shared across all of that
partner's keys and connections. Requests without a valid key are limited far more tightly, per IP address.
Over the limit you get 429 rate_limited with a Retry-After header — wait that
long, then retry with exponential backoff and jitter.
Don't poll harder than you need to. Once every few minutes per connection is plenty for
GET /v1/plan-requests?status=completed; a venue takes minutes or hours to finish a plan, not
seconds.
Errors
Errors are RFC 9457 problem documents (application/problem+json) with a stable, machine-readable
code. Branch on code, never on title — titles are
written for humans and may be reworded.
{
"type": "https://metisroomplanner.com/docs/events-api/errors#validation_failed",
"title": "One or more request parameters are invalid.",
"status": 400,
"code": "validation_failed",
"errors": { "guestCount": ["Must be between 0 and 100000."] },
"traceId": "3579c94390d9066755d9d544c9cbccd8"
}
| Status | Code | What to do |
|---|---|---|
| 400 | validation_failed | Read errors; it's keyed by field name. Fix and resend. |
| 400 | invalid_cursor | Start the list again from the first page. |
| 400 | connection_required | Your key serves several accounts — send Metis-Connection. |
| 401 | invalid_api_key | Stop retrying. Generate a replacement on the Integrations screen. |
| 403 | insufficient_scope | The key wasn't granted this scope. Ask for a key that has it. |
| 403 | connection_not_found | The connection is unknown or revoked. |
| 404 | resource_not_found | Also returned for resources that belong to another account. |
| 409 | duplicate_external_reference | You already created this. Fetch it instead. |
| 409 | idempotency_key_reused | Same key, different body. Fix your key generation. |
| 409 | idempotency_request_in_progress | The first attempt is still running. Wait and retry. |
| 409 | render_pending | The PDF isn't ready. Retry after Retry-After. |
| 412 | precondition_failed | Re-read, re-apply, retry. |
| 415 | unsupported_media_type | Set Content-Type correctly. |
| 422 | plan_allowance_reached | Your Metis subscription has no plans left this period. Retrying won't help. |
| 422 | subscription_inactive | Your Metis subscription isn't active. |
| 429 | rate_limited | Back off for Retry-After seconds. |
| 500 | internal_error | Safe to retry an idempotent request with backoff. Quote the traceId. |
| 503 | planner_unavailable | Retry with the same Idempotency-Key. |
Connection
Check your credentials
GET/v1/connectionno scope needed
Returns the Metis account your key is acting on and the scopes it was granted. Call it once at start-up, or whenever a new key is entered in your settings, to prove the key works before you try anything else. It needs no scope of its own.
Returns
A connection object.
Example response
{
"id": "con_8fK2mQpL4xRz9TbW3vNcYh7J",
"partner": {
"name": "Our booking system"
},
"account": {
"name": "Your Venues Ltd"
},
"scopes": [
"venues:read",
"occasions:read",
"occasions:write",
"floorplans:read",
"floorplans:write",
"links:create"
]
}
Notes
- This is the call to use in a 'Test connection' button.
Example request
curl -sS "$METIS_BASE/v1/connection" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/connection");
console.log(result);using var response = await http.GetAsync("v1/connection");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Venues and rooms
List venues
GET/v1/venuesscope venues:read
Your venues — the buildings or sites you run events in. Use it to populate a venue picker in your own system. Rooms live underneath a venue.
Query parameters
| Name | Type | Description |
|---|---|---|
query | string optional | Case-insensitive substring match on the venue or room name. |
limit | integer optional default 25 | How many items to return, 1–100. |
cursor | string optional | The nextCursor from the previous page. Opaque — pass it back unchanged. |
Returns
A page of venues, each with the number of rooms it has.
Example response
{
"data": [
{
"id": "ven_8",
"name": "Grand Hotel",
"roomCount": 6
}
],
"hasMore": false,
"nextCursor": null
}
Example request
curl -sS "$METIS_BASE/v1/venues?limit=25" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/venues");
console.log(result);using var response = await http.GetAsync("v1/venues");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Retrieve a venue
GET/v1/venues/{venueId}scope venues:read
One venue by id. Useful when you store the venue id against your own site record and want to show its current name.
Path parameters
| Name | Type | Description |
|---|---|---|
venueId | string required | A venue id from GET /v1/venues, e.g. ven_8. |
Returns
A venue object.
Example response
{
"id": "ven_8",
"name": "Grand Hotel",
"roomCount": 6
}
Example request
curl -sS "$METIS_BASE/v1/venues/ven_8" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/venues/ven_8");
console.log(result);using var response = await http.GetAsync("v1/venues/ven_8");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();List a venue's rooms
GET/v1/venues/{venueId}/roomsscope venues:read
Every room at a venue, with its dimensions and any layouts already saved for it in Metis. A room is structure only — walls, doors, windows, pillars and fixed features, with no furniture; furniture is laid out per event. This is the call that feeds your room picker, and the room id is what you send when you ask for a plan.
Path parameters
| Name | Type | Description |
|---|---|---|
venueId | string required | A venue id from GET /v1/venues. |
Returns
All of that venue's rooms in a single page.
Example response
{
"data": [
{
"id": "room_26",
"venueId": "ven_8",
"name": "Ballroom",
"structureVersion": 3,
"dimensions": {
"widthCm": 1800,
"lengthCm": 2400,
"ceilingHeightCm": 420
},
"layouts": [
{
"id": "lay_58",
"name": "Cabaret Banquet, Dancefloor & Disco",
"updatedAt": "2026-08-30T14:02:11Z"
}
]
}
],
"hasMore": false,
"nextCursor": null
}
Notes
structureVersiontells you which published version of the room a plan was built from. It changes whenever the room is re-measured or re-drawn in Metis.
Example request
curl -sS "$METIS_BASE/v1/venues/ven_8/rooms" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/venues/ven_8/rooms");
console.log(result);using var response = await http.GetAsync("v1/venues/ven_8/rooms");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Retrieve a room
GET/v1/rooms/{roomId}scope venues:read
One room by id, with the same detail as the list above. Handy for refreshing a room you have stored without re-reading the whole venue.
Path parameters
| Name | Type | Description |
|---|---|---|
roomId | string required | A room id, e.g. room_26. |
Returns
A room object.
Example response
{
"id": "room_26",
"venueId": "ven_8",
"name": "Ballroom",
"structureVersion": 3,
"dimensions": {
"widthCm": 1800,
"lengthCm": 2400,
"ceilingHeightCm": 420
},
"layouts": []
}
Example request
curl -sS "$METIS_BASE/v1/rooms/room_26" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/rooms/room_26");
console.log(result);using var response = await http.GetAsync("v1/rooms/room_26");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Occasions
List occasions
GET/v1/occasionsscope occasions:read
The events your system has told Metis about, earliest date first. Use it to reconcile after an outage, or to show what Metis already knows.
Query parameters
| Name | Type | Description |
|---|---|---|
year | integer optional | Only occasions whose date falls in this calendar year. |
limit | integer optional default 25 | How many items to return, 1–100. |
cursor | string optional | The nextCursor from the previous page. Opaque — pass it back unchanged. |
Returns
A page of occasions ordered by date.
Example response
{
"data": [
{
"id": "occ_42",
"name": "Smith-Jones Wedding",
"date": "2027-06-12",
"type": "wedding",
"guestCount": 150,
"venueId": "ven_8",
"externalReference": "CRM-10492",
"createdBy": {
"name": "Alex Planner",
"email": "[email protected]"
},
"createdAt": "2026-09-17T08:34:06Z",
"updatedAt": "2026-09-17T08:34:06Z"
}
],
"hasMore": false,
"nextCursor": null
}
Example request
curl -sS "$METIS_BASE/v1/occasions?year=2027&limit=25" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/occasions");
console.log(result);using var response = await http.GetAsync("v1/occasions");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Create an occasion
POST/v1/occasionsscope occasions:write
An occasion is an event — the wedding, conference or dinner the plan will be for. This is normally the first call you make for a booking; everything else (plan requests, floorplans) hangs off the occasion it returns. Put your own booking reference in externalReference so you can find it again.
Headers
| Header | Type | Description |
|---|---|---|
Idempotency-Key | string optional | A unique value per logical operation (a UUID is ideal). Retrying with the same key within 24 hours returns the original response instead of doing the work twice. Strongly recommended for every POST. |
Body fields
| Field | Type | Description |
|---|---|---|
name | string required | What the event is called, as it appears on the booking. 1–300 characters. |
date | date required | The day it is held, YYYY-MM-DD. A plain calendar date — no time, no time zone. See Dates and times. |
type | enum required | One of wedding, corporate, not_for_profit, bar_bat_mitzvah, dinner_party, other. |
guestCount | integer required | Expected number of guests, 0–100000. |
venueId | string optional | Which of your venues it is at. Optional, but set it if you know — it filters the rooms your team sees. |
externalReference | string optional | Your identifier for this event (booking number, CRM id). Up to 200 characters, and unique among your connection's occasions. |
createdBy | object optional | Who raised it in your system: { name, email }. Shown to your team so they know who to ask about it. |
Returns
201 Created with the occasion, a Location header and an ETag.
Errors worth handling
409 duplicate_external_reference | You've already created an occasion with that reference. Fetch it instead of creating another. |
Notes
- Retrying with the same
Idempotency-Keyreturns the original occasion and the headerIdempotent-Replayed: true, so a network timeout never creates a duplicate.
Example request
curl -sS -X POST "$METIS_BASE/v1/occasions" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name": "Smith-Jones Wedding", "date": "2027-06-12", "type": "wedding", "guestCount": 150, "venueId": "ven_8", "externalReference": "CRM-10492", "createdBy": {"name": "Alex Planner", "email": "[email protected]"}}'const created = await metis("/v1/occasions", { method: "POST", body: {
"name": "Smith-Jones Wedding",
"date": "2027-06-12",
"type": "wedding",
"guestCount": 150,
"venueId": "ven_8",
"externalReference": "CRM-10492",
"createdBy": {
"name": "Alex Planner",
"email": "[email protected]"
}
}, idempotencyKey: crypto.randomUUID() });
console.log(created);var payload = JsonSerializer.Deserialize<JsonElement>("""
{
"name": "Smith-Jones Wedding",
"date": "2027-06-12",
"type": "wedding",
"guestCount": 150,
"venueId": "ven_8",
"externalReference": "CRM-10492",
"createdBy": {
"name": "Alex Planner",
"email": "[email protected]"
}
}
""");
using var content = JsonContent.Create(payload, new MediaTypeHeaderValue("application/json"));
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/occasions") { Content = content };
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Retrieve an occasion
GET/v1/occasions/{occasionId}scope occasions:read
One occasion by id, with an ETag you'll need if you want to update it safely.
Path parameters
| Name | Type | Description |
|---|---|---|
occasionId | string required | An occasion id, e.g. occ_42. |
Returns
An occasion object.
Example response
{
"id": "occ_42",
"name": "Smith-Jones Wedding",
"date": "2027-06-12",
"type": "wedding",
"guestCount": 150,
"venueId": "ven_8",
"externalReference": "CRM-10492",
"createdBy": {
"name": "Alex Planner",
"email": "[email protected]"
},
"createdAt": "2026-09-17T08:34:06Z",
"updatedAt": "2026-09-17T08:34:06Z"
}
Notes
- Send
If-None-Match: "<etag>"to get a cheap304 Not Modifiedwhen nothing has changed.
Example request
curl -sS "$METIS_BASE/v1/occasions/occ_42" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/occasions/occ_42");
console.log(result);using var response = await http.GetAsync("v1/occasions/occ_42");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Update an occasion
PATCH/v1/occasions/{occasionId}scope occasions:write
Changes an event Metis already knows about — the date moved, the numbers went up, it switched venue. Send only the fields that changed (JSON Merge Patch, RFC 7396).
Path parameters
| Name | Type | Description |
|---|---|---|
occasionId | string required | An occasion id. |
Headers
| Header | Type | Description |
|---|---|---|
If-Match | string optional | The ETag you last read. The update fails with 412 if someone changed the resource in the meantime. |
Body fields
| Field | Type | Description |
|---|---|---|
name | string optional | 1–300 characters. Cannot be cleared. |
date | date optional | YYYY-MM-DD, a plain calendar date. Cannot be cleared. |
type | enum optional | As on create. Cannot be cleared. |
guestCount | integer optional | 0–100000. Cannot be cleared. |
venueId | string optional | Send null to clear it. |
externalReference | string optional | Send null to clear it. |
Returns
The updated occasion with a new ETag.
Errors worth handling
412 precondition_failed | Someone changed the occasion after the ETag you sent. Re-read it, re-apply your change and try again. |
Notes
- The request
Content-Typemay beapplication/merge-patch+jsonor plainapplication/json. - Omitting
If-Matchis allowed — it just means last write wins.
Example request
curl -sS -X PATCH "$METIS_BASE/v1/occasions/occ_42" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-H 'If-Match: "the-etag-you-last-read"' \
-H "Content-Type: application/merge-patch+json" \
-d '{"guestCount": 165, "date": "2027-06-19"}'const updated = await metis("/v1/occasions/occ_42", { method: "PATCH", body: {
"guestCount": 165,
"date": "2027-06-19"
}, ifMatch: occasion.etag });
console.log(updated);var payload = JsonSerializer.Deserialize<JsonElement>("""
{
"guestCount": 165,
"date": "2027-06-19"
}
""");
using var content = JsonContent.Create(payload, new MediaTypeHeaderValue("application/merge-patch+json"));
using var request = new HttpRequestMessage(HttpMethod.Patch, "v1/occasions/occ_42") { Content = content };
request.Headers.Add("If-Match", etag); // the ETag you last read
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Plan requests
List layout styles
GET/v1/layout-stylesscope venues:read
The room layouts Metis can generate — banquet rounds, cabaret, theatre, classroom and so on — with the options each one takes and the extras (top table, dance floor, stage) it supports. Read this to build your layout picker instead of hard-coding the list, because styles get added.
Returns
Every layout style in one page.
Example response
{
"data": [
{
"key": "banquet",
"name": "Banquet (round tables)",
"generatesFurniture": true,
"options": [
{
"name": "tableSize",
"type": "integer",
"allowed": [
8,
10,
12
],
"default": 10
}
],
"extras": [
"topTable",
"danceFloor",
"stage"
]
}
],
"hasMore": false,
"nextCursor": null
}
Notes
generatesFurniture: false(e.g. reception) means Metis prepares the room but your team places the furniture by hand.
Example request
curl -sS "$METIS_BASE/v1/layout-styles" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/layout-styles");
console.log(result);using var response = await http.GetAsync("v1/layout-styles");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Request a floorplan
POST/v1/occasions/{occasionId}/plan-requestsscope floorplans:write
The heart of the integration. Your system sends the room, the layout style and the numbers; Metis creates the plan in your Metis account and puts it on your planners' to-do list. One of them opens it, finishes it properly and marks it complete — and your system gets back a PDF and a PNG to attach to the BEO or the client's proposal. Leave layoutStyle out and Metis just opens the empty room for them.
Path parameters
| Name | Type | Description |
|---|---|---|
occasionId | string required | The occasion this plan is for. |
Headers
| Header | Type | Description |
|---|---|---|
Idempotency-Key | string optional | A unique value per logical operation (a UUID is ideal). Retrying with the same key within 24 hours returns the original response instead of doing the work twice. Strongly recommended for every POST. |
Body fields
| Field | Type | Description |
|---|---|---|
roomId | string required | The room to plan, from GET /v1/venues/{venueId}/rooms. |
layoutStyle | enum optional | A key from GET /v1/layout-styles, or null for an empty room. |
layoutOptions | object optional | Options for that style, e.g. {"tableSize": 10, "danceFloor": true}. Values are whole numbers or true/false. |
guestCount | integer optional | Guests to lay out for. Defaults to the occasion's guest count. |
startTime | string optional | When it starts, in venue local time, 24-hour HH:mm (e.g. 19:00). |
endTime | string optional | When it finishes, same format. Earlier than startTime means it runs past midnight. |
notes | string optional | Anything your planners should know. Up to 4000 characters — they read this. |
contact | object optional | Who to ask about it: { name, email, phone }. |
externalReference | string optional | Your reference, e.g. the BEO number. It's used in the download file names, so it's worth setting. |
Returns
201 Created with the plan request in status requested.
Errors worth handling
422 plan_allowance_reached | Your Metis subscription has no plans left this period. Retrying won't help — it needs a plan freed up or an upgrade. |
422 subscription_inactive | Your Metis subscription isn't active. |
503 planner_unavailable | Metis couldn't be reached. Retry with the same Idempotency-Key after Retry-After seconds. |
Notes
- Each plan request uses one of your plan allowance. Don't create speculative requests.
- Room structures themselves never count against the allowance — only the plans made from them.
Example request
curl -sS -X POST "$METIS_BASE/v1/occasions/occ_42/plan-requests" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"roomId": "room_26", "layoutStyle": "banquet", "layoutOptions": {"tableSize": 10, "danceFloor": true}, "guestCount": 120, "startTime": "19:00", "endTime": "23:30", "notes": "Top table for 12 on the stage side.", "contact": {"name": "Alex Planner", "email": "[email protected]", "phone": "+44 1234 567890"}, "externalReference": "BEO-7781"}'const created = await metis("/v1/occasions/occ_42/plan-requests", { method: "POST", body: {
"roomId": "room_26",
"layoutStyle": "banquet",
"layoutOptions": {
"tableSize": 10,
"danceFloor": true
},
"guestCount": 120,
"startTime": "19:00",
"endTime": "23:30",
"notes": "Top table for 12 on the stage side.",
"contact": {
"name": "Alex Planner",
"email": "[email protected]",
"phone": "+44 1234 567890"
},
"externalReference": "BEO-7781"
}, idempotencyKey: crypto.randomUUID() });
console.log(created);var payload = JsonSerializer.Deserialize<JsonElement>("""
{
"roomId": "room_26",
"layoutStyle": "banquet",
"layoutOptions": {
"tableSize": 10,
"danceFloor": true
},
"guestCount": 120,
"startTime": "19:00",
"endTime": "23:30",
"notes": "Top table for 12 on the stage side.",
"contact": {
"name": "Alex Planner",
"email": "[email protected]",
"phone": "+44 1234 567890"
},
"externalReference": "BEO-7781"
}
""");
using var content = JsonContent.Create(payload, new MediaTypeHeaderValue("application/json"));
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/occasions/occ_42/plan-requests") { Content = content };
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();List plan requests
GET/v1/plan-requestsscope floorplans:read
Your open and finished plan requests, oldest first. Poll this with ?status=completed to pick up plans your team has finished — the simplest way to keep your system in step until webhooks arrive.
Query parameters
| Name | Type | Description |
|---|---|---|
status | enum optional | requested, in_progress, completed, changes_requested or cancelled. |
occasionId | string optional | Only requests for this occasion. |
limit | integer optional default 25 | How many items to return, 1–100. |
cursor | string optional | The nextCursor from the previous page. Opaque — pass it back unchanged. |
Returns
A page of plan requests. warnings is only filled in when you retrieve a single request.
Example response
{
"data": [
{
"id": "pr_12",
"occasionId": "occ_42",
"roomId": "room_26",
"status": "completed",
"currentVersion": 2,
"externalReference": "BEO-7781"
}
],
"hasMore": false,
"nextCursor": null
}
Notes
- Poll politely — once every few minutes per connection is plenty. See Rate limits.
Example request
curl -sS "$METIS_BASE/v1/plan-requests?status=completed&occasionId=occ_42" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/plan-requests");
console.log(result);using var response = await http.GetAsync("v1/plan-requests");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Retrieve a plan request
GET/v1/plan-requests/{planRequestId}scope floorplans:read
The full state of one request: where it has got to, what your planners have been told, any warnings raised while laying out the furniture, and every completed version with fresh download links. This is the call to make when someone opens the booking in your system.
Path parameters
| Name | Type | Description |
|---|---|---|
planRequestId | string required | A plan request id, e.g. pr_12. |
Returns
The plan request, its versions and their links.
Example response
{
"id": "pr_12",
"occasionId": "occ_42",
"roomId": "room_26",
"floorplanId": "fp_118",
"status": "completed",
"statusChangedAt": "2026-09-17T15:12:44Z",
"layoutStyle": "banquet",
"layoutOptions": {
"tableSize": 10,
"danceFloor": true
},
"guestCount": 120,
"startTime": "19:00",
"endTime": "23:30",
"notes": "Top table for 12 on the stage side.",
"contact": {
"name": "Alex Planner",
"email": "[email protected]",
"phone": "+44 1234 567890"
},
"externalReference": "BEO-7781",
"warnings": [
{
"code": "capacity_shortfall",
"message": "Seated 110 of 120 guests; your team will adjust."
}
],
"pendingChanges": [],
"currentVersion": 1,
"versions": [
{
"version": 1,
"completedAt": "2026-09-17T15:12:44Z",
"renderStatus": "rendered",
"renderedAt": "2026-09-17T15:13:02Z",
"pdfUrl": "https://metisroomplanner.com/MetisEventsApi/v1/plan-requests/pr_12/versions/1/pdf",
"pngUrl": "https://metisroomplanner.com/MetisEventsApi/v1/plan-requests/pr_12/versions/1/png",
"pdfShareUrl": "https://metisroomplanner.com/MetisEventsApi/files/pr_12/1/BEO-7781-floorplan-v1.pdf?e=1789..&s=6f21..",
"pngShareUrl": "https://metisroomplanner.com/MetisEventsApi/files/pr_12/1/BEO-7781-floorplan-v1.png?e=1789..&s=a03c.."
}
],
"plannerUrl": "https://metisroomplanner.com/metisroomplanner/planner/?plan=...",
"createdAt": "2026-09-17T09:02:10Z",
"updatedAt": "2026-09-17T15:13:02Z"
}
Notes
statusmovesrequested→in_progress→completed, and back tochanges_requestedif you change the brief afterwards.cancelledis final.currentVersionis0until the first time the venue completes the plan.- Share links expire after 30 days; retrieve the plan request again for fresh ones.
Example request
curl -sS "$METIS_BASE/v1/plan-requests/pr_12" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/plan-requests/pr_12");
console.log(result);using var response = await http.GetAsync("v1/plan-requests/pr_12");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Change a plan request
PATCH/v1/plan-requests/{planRequestId}scope floorplans:write
The numbers changed, the dance floor is out, the times moved. Send only what changed and Metis shows your planners exactly which fields differ from the plan they already drew, moving the request to changes_requested if they had finished it. The room can't be changed — cancel and raise a new request instead.
Path parameters
| Name | Type | Description |
|---|---|---|
planRequestId | string required | A plan request id. |
Headers
| Header | Type | Description |
|---|---|---|
If-Match | string optional | The ETag you last read. The update fails with 412 if someone changed the resource in the meantime. |
Body fields
| Field | Type | Description |
|---|---|---|
layoutStyle | enum optional | A different style, or null for an empty room. |
layoutOptions | object optional | Replaces the options wholesale. null clears them. |
guestCount | integer optional | 0–100000. |
startTime | string optional | HH:mm or null. |
endTime | string optional | HH:mm or null. |
notes | string optional | Up to 4000 characters, or null. |
contact | object optional | { name, email, phone }, or null. |
externalReference | string optional | Up to 200 characters, or null. |
Returns
The updated plan request, including the new pendingChanges entry.
Errors worth handling
409 plan_request_cancelled | A cancelled request can't be changed. |
412 precondition_failed | It changed after the ETag you sent. |
Notes
- Changing
layoutStylewithout also sendinglayoutOptionsdrops options that don't apply to the new style.
Example request
curl -sS -X PATCH "$METIS_BASE/v1/plan-requests/pr_12" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-H 'If-Match: "the-etag-you-last-read"' \
-H "Content-Type: application/merge-patch+json" \
-d '{"guestCount": 140, "layoutOptions": {"tableSize": 12, "danceFloor": true}}'const updated = await metis("/v1/plan-requests/pr_12", { method: "PATCH", body: {
"guestCount": 140,
"layoutOptions": {
"tableSize": 12,
"danceFloor": true
}
}, ifMatch: occasion.etag });
console.log(updated);var payload = JsonSerializer.Deserialize<JsonElement>("""
{
"guestCount": 140,
"layoutOptions": {
"tableSize": 12,
"danceFloor": true
}
}
""");
using var content = JsonContent.Create(payload, new MediaTypeHeaderValue("application/merge-patch+json"));
using var request = new HttpRequestMessage(HttpMethod.Patch, "v1/plan-requests/pr_12") { Content = content };
request.Headers.Add("If-Match", etag); // the ETag you last read
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Cancel a plan request
POST/v1/plan-requests/{planRequestId}/cancelscope floorplans:write
The booking fell through. Your planners see the request as cancelled and stop work; the plan itself stays in your Metis account. Calling it twice is harmless.
Path parameters
| Name | Type | Description |
|---|---|---|
planRequestId | string required | A plan request id. |
Returns
The cancelled plan request.
Example response
{
"id": "pr_12",
"status": "cancelled",
"statusChangedAt": "2026-09-18T11:00:03Z"
}
Example request
curl -sS -X POST "$METIS_BASE/v1/plan-requests/pr_12/cancel" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const created = await metis("/v1/plan-requests/pr_12/cancel", { method: "POST" });
console.log(created);using var content = new StringContent(string.Empty);
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/plan-requests/pr_12/cancel") { Content = content };
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Download a version as PDF
GET/v1/plan-requests/{planRequestId}/versions/{version}/pdfscope floorplans:read
The finished plan as an A4 landscape PDF with a title block — the thing you attach to the BEO or send to the client. Authenticated with your API key, so use it for server-side fetches; for a link you can store or email, use the version's pdfShareUrl instead.
Path parameters
| Name | Type | Description |
|---|---|---|
planRequestId | string required | A plan request id. |
version | integer required | Which completed version, starting at 1. |
Returns
200 with application/pdf.
Errors worth handling
409 render_pending | The PDF is still being produced. Wait Retry-After seconds and ask again. |
409 render_failed | Metis couldn't render this version. Contact support with the traceId. |
Example request
curl -sS "$METIS_BASE/v1/plan-requests/pr_12/versions/1/pdf" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-o plan-v1.pdfconst response = await metisRaw("/v1/plan-requests/pr_12/versions/1/pdf");
const bytes = Buffer.from(await response.arrayBuffer()); // save it, or stream it onvar bytes = await http.GetByteArrayAsync("v1/plan-requests/pr_12/versions/1/pdf");
await File.WriteAllBytesAsync("plan-v1.pdf", bytes);Download a version as PNG
GET/v1/plan-requests/{planRequestId}/versions/{version}/pngscope floorplans:read
The same page as a 1754×1240 image — good for a thumbnail in your booking screen or for embedding in an email.
Path parameters
| Name | Type | Description |
|---|---|---|
planRequestId | string required | A plan request id. |
version | integer required | Which completed version, starting at 1. |
Returns
200 with image/png.
Errors worth handling
409 render_pending | Still rendering; retry after Retry-After seconds. |
Example request
curl -sS "$METIS_BASE/v1/plan-requests/pr_12/versions/1/png" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-o plan-v1.pngconst response = await metisRaw("/v1/plan-requests/pr_12/versions/1/png");
const bytes = Buffer.from(await response.arrayBuffer()); // save it, or stream it onvar bytes = await http.GetByteArrayAsync("v1/plan-requests/pr_12/versions/1/png");
await File.WriteAllBytesAsync("plan-v1.png", bytes);Floorplans
List an occasion's floorplans
GET/v1/occasions/{occasionId}/floorplansscope floorplans:read
Every floorplan attached to an occasion, including ones your team created directly in Metis rather than through the API.
Path parameters
| Name | Type | Description |
|---|---|---|
occasionId | string required | An occasion id. |
Returns
The occasion's floorplans in a single page.
Example response
{
"data": [
{
"id": "fp_118",
"occasionId": "occ_42",
"name": "Smith-Jones Wedding — Ballroom",
"roomId": "room_26",
"externalReference": "BEO-7781",
"isShared": false,
"thumbnailUrl": null,
"updatedAt": "2026-09-17T15:12:44Z"
}
],
"hasMore": false,
"nextCursor": null
}
Example request
curl -sS "$METIS_BASE/v1/occasions/occ_42/floorplans" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/occasions/occ_42/floorplans");
console.log(result);using var response = await http.GetAsync("v1/occasions/occ_42/floorplans");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Retrieve a floorplan
GET/v1/floorplans/{floorplanId}scope floorplans:read
One floorplan by id.
Path parameters
| Name | Type | Description |
|---|---|---|
floorplanId | string required | A floorplan id, e.g. fp_118. |
Returns
A floorplan object.
Example response
{
"id": "fp_118",
"occasionId": "occ_42",
"name": "Smith-Jones Wedding — Ballroom",
"roomId": "room_26",
"externalReference": "BEO-7781",
"isShared": false,
"thumbnailUrl": null,
"updatedAt": "2026-09-17T15:12:44Z"
}
Example request
curl -sS "$METIS_BASE/v1/floorplans/fp_118" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION"const result = await metis("/v1/floorplans/fp_118");
console.log(result);using var response = await http.GetAsync("v1/floorplans/fp_118");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Create a floorplan from a saved layout Preview — returns 501 today
POST/v1/occasions/{occasionId}/floorplansscope floorplans:write
Copies one of your own saved layouts for a room straight into the occasion, skipping the request-and-complete cycle. Use it when you already have exactly the plan you want. To start from an empty room and have Metis lay it out, use a plan request instead.
Path parameters
| Name | Type | Description |
|---|---|---|
occasionId | string required | An occasion id. |
Headers
| Header | Type | Description |
|---|---|---|
Idempotency-Key | string optional | A unique value per logical operation (a UUID is ideal). Retrying with the same key within 24 hours returns the original response instead of doing the work twice. Strongly recommended for every POST. |
Body fields
| Field | Type | Description |
|---|---|---|
layoutId | string required | A layout id from GET /v1/rooms/{roomId}. |
name | string optional | Defaults to the occasion name and date. |
externalReference | string optional | Your reference; unique within the occasion. |
Returns
201 Created with the floorplan.
Notes
- Counts toward your Metis plan allowance.
Example request
curl -sS -X POST "$METIS_BASE/v1/occasions/occ_42/floorplans" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"layoutId": "lay_58", "name": "Smith-Jones Wedding — Ballroom", "externalReference": "BEO-7781"}'const created = await metis("/v1/occasions/occ_42/floorplans", { method: "POST", body: {
"layoutId": "lay_58",
"name": "Smith-Jones Wedding — Ballroom",
"externalReference": "BEO-7781"
}, idempotencyKey: crypto.randomUUID() });
console.log(created);var payload = JsonSerializer.Deserialize<JsonElement>("""
{
"layoutId": "lay_58",
"name": "Smith-Jones Wedding — Ballroom",
"externalReference": "BEO-7781"
}
""");
using var content = JsonContent.Create(payload, new MediaTypeHeaderValue("application/json"));
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/occasions/occ_42/floorplans") { Content = content };
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Links
Create a link to an occasion Preview — returns 501 today
POST/v1/occasions/{occasionId}/linksscope links:create
A short-lived URL that opens the occasion in Metis for a person — put it behind an 'Open in Metis' button in your UI.
Path parameters
| Name | Type | Description |
|---|---|---|
occasionId | string required | An occasion id. |
Body fields
| Field | Type | Description |
|---|---|---|
purpose | enum required | view or edit. |
Returns
A link with an expiry.
Example response
{
"url": "https://metisroomplanner.com/metisroomplanner/planner/?occasion=...",
"expiresAt": "2026-09-18T12:00:00Z"
}
Example request
curl -sS -X POST "$METIS_BASE/v1/occasions/occ_42/links" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-H "Content-Type: application/json" \
-d '{"purpose": "edit"}'const created = await metis("/v1/occasions/occ_42/links", { method: "POST", body: {
"purpose": "edit"
} });
console.log(created);var payload = JsonSerializer.Deserialize<JsonElement>("""
{
"purpose": "edit"
}
""");
using var content = JsonContent.Create(payload, new MediaTypeHeaderValue("application/json"));
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/occasions/occ_42/links") { Content = content };
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Create a link that opens a floorplan Preview — returns 501 today
POST/v1/floorplans/{floorplanId}/linksscope links:create
edit opens the Metis planner — the person must be signed in to your Metis account. view opens a read-only 2D/3D review page.
Path parameters
| Name | Type | Description |
|---|---|---|
floorplanId | string required | A floorplan id. |
Body fields
| Field | Type | Description |
|---|---|---|
purpose | enum required | view or edit. |
Returns
A link with an expiry.
Example request
curl -sS -X POST "$METIS_BASE/v1/floorplans/fp_118/links" \
-H "Authorization: Bearer $METIS_API_KEY" \
-H "Metis-Connection: $METIS_CONNECTION" \
-H "Content-Type: application/json" \
-d '{"purpose": "view"}'const created = await metis("/v1/floorplans/fp_118/links", { method: "POST", body: {
"purpose": "view"
} });
console.log(created);var payload = JsonSerializer.Deserialize<JsonElement>("""
{
"purpose": "view"
}
""");
using var content = JsonContent.Create(payload, new MediaTypeHeaderValue("application/json"));
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/floorplans/fp_118/links") { Content = content };
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();Coming from Prismm or AllSeated?
If you already integrate with Prismm (formerly AllSeated), Metis exposes a separate compatibility surface
under /prismm-api/ that mirrors those calls and their response envelopes, so an existing
integration can point at Metis by changing the base URL and the credentials. It is a migration aid: new
work should use /v1, which is where new capability lands. Ask Metis for the compatibility
contract (/openapi/prismm-compat.yaml) if you need it.
Versioning
The version is in the path (/v1). Within v1 we will add endpoints, add optional
request fields and add response properties — so write a tolerant client. We will not remove or rename
anything, change a type, or make an optional field required without a new version and notice.
The machine-readable contract is OpenAPI 3.1 at /openapi/v1.yaml, with a browsable reference
at /docs/. Generate your client from it if you'd rather not hand-write one. A few operations
are marked Preview below: they're published and stable in shape, but return
501 not_implemented until they're switched on.
Support
Include the X-Request-Id of the failing call, or the traceId from the problem
document, and we can find it immediately. Never include an API key in a support message — if you think one
has been exposed, revoke it on the Integrations screen first and tell us second.