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

  1. Create the occasion (the event) POST /v1/occasions — the event, its date, type and guest count.
  2. Pick a room GET /v1/venues then GET /v1/venues/{venueId}/rooms. Cache these; they change rarely.
  3. Request the plan POST /v1/occasions/{occasionId}/plan-requests with the room, layout style, numbers and times. Status: requested.
  4. Your planners do the work Your team opens the request in Metis, finishes the plan and marks it complete. Status: in_progress → completed.
  5. Collect the result Poll GET /v1/plan-requests?status=completed, then take the newest entry in versions and download the PDF — or store its signed pdfShareUrl.
  6. Changes happen PATCH the plan request when numbers or times move. Metis shows your team exactly what changed and the request goes back to changes_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.

  1. Sign in to Metis Room Planner as an owner or administrator of the account.
  2. Go to Profile → Integrations.
  3. 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.

The prefix says which kind of key you are holding — mk_test_ for one you made to develop against, mk_live_ for one in production use — and nothing more. It is not a sandbox. Both act on this same account: an occasion created with a mk_test_ key is a real occasion, and a plan request made with one spends a real plan from your allowance. Name anything you create while testing so you can find it and delete it afterwards.

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

The prefix is a label, not a mode. mk_test_ and mk_live_ keys are handled identically — nothing in Metis branches on the prefix, and writes made with either land in the real account — so it is there purely so you can tell at a glance which key is in front of you. 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.

Every key carries a set of scopes, and each call checks the one it needs: outside them you get 403 insufficient_scope. That is not something to retry — it means the key was issued without that permission.

The set is fixed when the key is created, and nothing can change it afterwards. Generating a key from the Integrations screen without naming scopes gives it all six, so if you want a narrower key — a read-only one for a reporting job, say — say so when you create it, and revoke and reissue if you need to change your mind later.

ScopeLets you
venues:readList venues, rooms and layout styles.
occasions:readRead occasions.
occasions:writeCreate and update occasions.
floorplans:readRead floorplans and plan requests, and download PDFs and PNGs.
floorplans:writeRaise, change and cancel plan requests.
links:createCreate 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.

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

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.

KindFormatExampleMeaning
Calendar date
date
YYYY-MM-DD
ISO 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 day
startTime, 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.
Timestamp
createdAt, 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

Three lists are paged, because they grow without limit: GET /v1/venues, GET /v1/occasions and GET /v1/plan-requests. They 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.

The other three lists are bounded by their nature — the rooms in one venue, the floorplans on one occasion, the layout styles Metis offers — so GET /v1/venues/{venueId}/rooms, GET /v1/occasions/{occasionId}/floorplans and GET /v1/layout-styles take neither parameter and always return the whole set, with hasMore: false and nextCursor: null. They use the same envelope, so one piece of client code reads both kinds.

Idempotency

Four POSTs read Idempotency-Key: <uuid> — the ones that create something you would not want a second copy of:

Send one on each of those. 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.

The remaining POSTs ignore the header, and don't need it. The two /links calls hand back a link to something that already exists, and /cancel reaches the same state however many times you call it — so a repeat costs you nothing either way.

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"
}
StatusCodeWhat to do
400validation_failedRead errors; it's keyed by field name. Fix and resend.
400invalid_idThe path id isn't an id — usually a name pasted where room_26 belongs. Note that this is a 400, not a 404: nothing was looked up.
400invalid_cursorStart the list again from the first page.
400connection_requiredYour key serves several accounts — send Metis-Connection.
401invalid_api_keyStop retrying. Generate a replacement on the Integrations screen.
403insufficient_scopeThe key wasn't granted this scope. Ask for a key that has it.
403connection_not_foundThe connection is unknown or revoked.
403invalid_signatureA signed share or download URL has expired or been edited. Read the plan request again for fresh links.
404resource_not_foundA well-formed id nothing matches — including things that belong to another account.
409no_floorplanThe occasion has nothing to link to yet. Request a plan first.
409duplicate_external_referenceYou already created this. Fetch it instead.
409idempotency_key_reusedSame key, different body. Fix your key generation.
409idempotency_request_in_progressThe first attempt is still running. Wait and retry.
409render_pendingThe PDF isn't ready. Retry after Retry-After.
412precondition_failedRe-read, re-apply, retry.
415unsupported_media_typeSet Content-Type correctly.
422layout_not_foundThat saved layout isn't one of this account's. Re-read the room's layouts.
422plan_allowance_reachedYour Metis subscription has no plans left this period. Retrying won't help.
422subscription_inactiveYour Metis subscription isn't active.
422planner_rejectedMetis refused the request for a reason the API can't restate. Quote the traceId to support.
429rate_limitedBack off for Retry-After seconds.
500internal_errorSafe to retry an idempotent request with backoff. Quote the traceId.
503planner_not_configuredThis server isn't wired to Metis for that operation yet. Retrying won't help — it needs a setting changed.
503planner_unavailableRetry 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"

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

NameTypeDescription
querystring
optional
Case-insensitive substring match on the venue or room name.
limitinteger
optional default 25
How many items to return, 1–100.
cursorstring
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"

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

NameTypeDescription
venueIdstring
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"

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

NameTypeDescription
venueIdstring
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

  • structureVersion tells 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.
  • A version number is not by itself a guarantee of a usable room: versions published before the planner's 2026-09-22 release could be saved with no walls, and a plan request against a wall-less structure succeeds but generates its layout into an empty page. The planner now refuses wall-less publishes, so newer versions always contain walls. If plans for a room come back empty, ask the venue to redraw the room's walls and publish again.

Example request

curl -sS "$METIS_BASE/v1/venues/ven_8/rooms" \
  -H "Authorization: Bearer $METIS_API_KEY" \
  -H "Metis-Connection: $METIS_CONNECTION"

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

NameTypeDescription
roomIdstring
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"

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. Every style also accepts the three extras (top table, dance floor, stage) — Metis does not place them, it flags each one to the venue to add by hand. 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",
          "minimum": 1,
          "maximum": 40,
          "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"

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

NameTypeDescription
externalReferencestring
optional
Your own reference, matched exactly — how you find an occasion you created without having stored the Metis id.
fromdate
optional
Earliest occasion date, inclusive. yyyy-MM-dd.
todate
optional
Latest occasion date, inclusive — an occasion on this day is returned.
venueIdstring
optional
A venue id from GET /v1/venues.
typeenum
optional
wedding, corporate, not_for_profit, bar_bat_mitzvah, dinner_party, other.
querystring
optional
Case-insensitive substring match on the occasion name — the same spelling as on GET /v1/venues.
yearinteger
optional
Shorthand for a whole calendar year. Ignored when from or to is given.
limitinteger
optional default 25
How many items to return, 1–100.
cursorstring
optional
The nextCursor from the previous page. Opaque — pass it back unchanged.

Returns

A page of occasions ordered by date. Filters are optional and combine.

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"

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

HeaderTypeDescription
Idempotency-Keystring
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 on this call — it is one of the four that read the header.

Body fields

FieldTypeDescription
namestring
required
What the event is called, as it appears on the booking. 1–300 characters.
datedate
required
The day it is held, YYYY-MM-DD. A plain calendar date — no time, no time zone. See Dates and times.
typeenum
required
One of wedding, corporate, not_for_profit, bar_bat_mitzvah, dinner_party, other.
guestCountinteger
required
Expected number of guests, 0–100000.
venueIdstring
optional
Which of your venues it is at. Optional, but set it if you know — it filters the rooms your team sees.
externalReferencestring
optional
Your identifier for this event (booking number, CRM id). Up to 200 characters, and unique among your connection's occasions.
createdByobject
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_referenceYou've already created an occasion with that reference. Fetch it instead of creating another.

Notes

  • Retrying with the same Idempotency-Key returns the original occasion and the header Idempotent-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]"}}'

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

NameTypeDescription
occasionIdstring
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 cheap 304 Not Modified when nothing has changed.

Example request

curl -sS "$METIS_BASE/v1/occasions/occ_42" \
  -H "Authorization: Bearer $METIS_API_KEY" \
  -H "Metis-Connection: $METIS_CONNECTION"

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

NameTypeDescription
occasionIdstring
required
An occasion id.

Headers

HeaderTypeDescription
If-Matchstring
optional
The ETag you last read. The update fails with 412 if someone changed the resource in the meantime.

Body fields

FieldTypeDescription
namestring
optional
1–300 characters. Cannot be cleared.
datedate
optional
YYYY-MM-DD, a plain calendar date. Cannot be cleared.
typeenum
optional
As on create. Cannot be cleared.
guestCountinteger
optional
0–100000. Cannot be cleared.
venueIdstring
optional
Send null to clear it.
externalReferencestring
optional
Send null to clear it.

Returns

The updated occasion with a new ETag.

Errors worth handling

412 precondition_failedSomeone changed the occasion after the ETag you sent. Re-read it, re-apply your change and try again.

Notes

  • The request Content-Type may be application/merge-patch+json or plain application/json.
  • Omitting If-Match is 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"}'

Plan requests

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

NameTypeDescription
statusenum
optional
requested, in_progress, completed, changes_requested or cancelled.
occasionIdstring
optional
Only requests for this occasion.
limitinteger
optional default 25
How many items to return, 1–100.
cursorstring
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"

Create a plan request

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

NameTypeDescription
occasionIdstring
required
The occasion this plan is for.

Headers

HeaderTypeDescription
Idempotency-Keystring
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 on this call — it is one of the four that read the header.

Body fields

FieldTypeDescription
roomIdstring
required
The room to plan, from GET /v1/venues/{venueId}/rooms.
layoutStyleenum
optional
A key from GET /v1/layout-styles, or null for an empty room.
layoutOptionsobject
optional
Options for that style, e.g. {"tableSize": 10, "danceFloor": true}. Values are whole numbers or true/false.
guestCountinteger
optional
Guests to lay out for. Defaults to the occasion's guest count.
startTimestring
optional
When it starts, in venue local time, 24-hour HH:mm (e.g. 19:00).
endTimestring
optional
When it finishes, same format. Earlier than startTime means it runs past midnight.
notesstring
optional
Anything the venue's team should know. Up to 4000 characters, shown in full on their row for this request — wrapped, not truncated.
contactobject
optional
Who they should talk to: { name, email, phone }. Shown on the same row, with the email and phone clickable. name is also what Metis records as the plan's customer.
externalReferencestring
optional
Your reference, e.g. the BEO number. Used in the download file names and shown to the venue as ref BEO-7781, so it is the number they will quote back at you.

Returns

201 Created with the plan request in status requested.

Errors worth handling

422 plan_allowance_reachedYour Metis subscription has no plans left this period. Retrying won't help — it needs a plan freed up or an upgrade.
422 subscription_inactiveYour Metis subscription isn't active.
503 planner_unavailableMetis 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"}'

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

NameTypeDescription
planRequestIdstring
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/plan-requests/pr_12/versions/1.pdf?expires=1792886400&signature=6f21c4a9…",
      "pngShareUrl": "https://metisroomplanner.com/MetisEventsApi/files/plan-requests/pr_12/versions/1.png?expires=1792886400&signature=a03c7be1…"
    }
  ],
  "plannerUrl": "https://metisroomplanner.com/planner/?plan=plan-be8d2591",
  "createdAt": "2026-09-17T09:02:10Z",
  "updatedAt": "2026-09-17T15:13:02Z"
}

Notes

  • status moves requested → in_progress → completed, and back to changes_requested if you change the brief afterwards. cancelled is final.
  • currentVersion is 0 until the first time the venue completes the plan.
  • Share links expire after 30 days; retrieve the plan request again for fresh ones. They are signed URLs — tamper with expires or signature and you get 403 invalid_signature.
  • pdfShareUrl and pngShareUrl are null — with no error — on a server where rendering has not been configured, and that is the shipped default. Check for null before you build a feature on them, and use the authenticated pdfUrl/pngUrl if you only need the file server-side.

Example request

curl -sS "$METIS_BASE/v1/plan-requests/pr_12" \
  -H "Authorization: Bearer $METIS_API_KEY" \
  -H "Metis-Connection: $METIS_CONNECTION"

Update 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

NameTypeDescription
planRequestIdstring
required
A plan request id.

Headers

HeaderTypeDescription
If-Matchstring
optional
The ETag you last read. The update fails with 412 if someone changed the resource in the meantime.

Body fields

FieldTypeDescription
layoutStyleenum
optional
A different style, or null for an empty room.
layoutOptionsobject
optional
Replaces the options wholesale. null clears them.
guestCountinteger
optional
0–100000.
startTimestring
optional
HH:mm or null.
endTimestring
optional
HH:mm or null.
notesstring
optional
Up to 4000 characters, or null.
contactobject
optional
{ name, email, phone }, or null.
externalReferencestring
optional
Up to 200 characters, or null.

Returns

The updated plan request, including the new pendingChanges entry.

Errors worth handling

409 plan_request_cancelledA cancelled request can't be changed.
412 precondition_failedIt changed after the ETag you sent.

Notes

  • Changing layoutStyle without also sending layoutOptions drops 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}}'

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

NameTypeDescription
planRequestIdstring
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"

Redraw the plan from the current brief

POST/v1/plan-requests/{planRequestId}/regeneratescope floorplans:write

Changing a plan takes two calls, on purpose. PATCH changes what you asked for — the guest count went from 120 to 130 — and this redraws the plan to match. They are separate because redrawing throws away any arrangement the venue made by hand, and a change that only needs the venue told should not flatten their work. Read warnings on the response: that is where a capacity shortfall or an extra that could not be placed shows up. The venue still completes the plan before a new version and a new PDF exist.

Path parameters

NameTypeDescription
planRequestIdstring
required
A plan request id.

Query parameters

NameTypeDescription
replaceHandArrangementboolean
optional default false
Redraw even though someone arranged this plan by hand, discarding their arrangement. Without it, a hand-arranged plan is refused.

Returns

The plan request, redrawn.

Example response

{
  "id": "pr_12",
  "guestCount": 130,
  "status": "changes_requested",
  "warnings": [
    {
      "code": "capacity_shortfall",
      "message": "Placed 120 of 130 seats."
    }
  ]
}

Errors worth handling

409 plan_hand_arrangedSomeone arranged this plan by hand. Repeat with ?replaceHandArrangement=true to discard it.
409 plan_in_useSomeone at the venue has the plan open. Try again in a few minutes.
409 no_layout_styleThe request asks for an empty room, so there is no layout to generate. PATCH a layoutStyle first.
409 plan_request_cancelledThe request was cancelled and its plan is no longer maintained.
502 planner_unavailableMetis could not reach the planner. Try again shortly.

Example request

curl -sS -X POST "$METIS_BASE/v1/plan-requests/pr_12/regenerate" \
  -H "Authorization: Bearer $METIS_API_KEY" \
  -H "Metis-Connection: $METIS_CONNECTION"

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

NameTypeDescription
planRequestIdstring
required
A plan request id.
versioninteger
required
Which completed version, starting at 1.

Returns

200 with application/pdf.

Errors worth handling

409 render_pendingThe PDF is still being produced. Wait Retry-After seconds and ask again.
409 render_failedMetis 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.pdf

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

NameTypeDescription
planRequestIdstring
required
A plan request id.
versioninteger
required
Which completed version, starting at 1.

Returns

200 with image/png.

Errors worth handling

409 render_pendingStill 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.png

Download the current version as PDF

GET/v1/plan-requests/{planRequestId}/versions/latest/pdfscope floorplans:read

The newest completed version, worked out when you ask rather than when you last read the request. A numbered version address freezes one revision: store it against a booking and it keeps serving that revision after the plan has been redrawn. This one follows the plan, so you can keep a single address for the life of the booking and never track version numbers. For a link you can put in a document without your API key, use latest.pdfShare from the plan request.

Path parameters

NameTypeDescription
planRequestIdstring
required
A plan request id.

Returns

200 with application/pdf.

Errors worth handling

409 no_version_yetThe plan has never been completed, so there is nothing to download. Waiting will not help — the venue completes it in Metis.
409 render_pendingThe current version is still being produced. Wait Retry-After seconds and ask again.

Example request

curl -sS "$METIS_BASE/v1/plan-requests/pr_12/versions/latest/pdf" \
  -H "Authorization: Bearer $METIS_API_KEY" \
  -H "Metis-Connection: $METIS_CONNECTION" \
  -o plan-v1.pdf

Download the current version as PNG

GET/v1/plan-requests/{planRequestId}/versions/latest/pngscope floorplans:read

As the latest PDF, but the image — for a thumbnail beside a booking that should stay in step with the plan without your system tracking which version is current.

Path parameters

NameTypeDescription
planRequestIdstring
required
A plan request id.

Returns

200 with image/png.

Errors worth handling

409 no_version_yetNothing has been completed yet.
409 render_pendingStill rendering; retry after Retry-After seconds.

Example request

curl -sS "$METIS_BASE/v1/plan-requests/pr_12/versions/latest/png" \
  -H "Authorization: Bearer $METIS_API_KEY" \
  -H "Metis-Connection: $METIS_CONNECTION" \
  -o plan-v1.png

Floorplans

List 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

NameTypeDescription
occasionIdstring
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"

Retrieve a floorplan

GET/v1/floorplans/{floorplanId}scope floorplans:read

One floorplan by id.

Path parameters

NameTypeDescription
floorplanIdstring
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"

Create a floorplan

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

NameTypeDescription
occasionIdstring
required
An occasion id.

Headers

HeaderTypeDescription
Idempotency-Keystring
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 on this call — it is one of the four that read the header.

Body fields

FieldTypeDescription
layoutIdstring
required
A layout id from GET /v1/rooms/{roomId}.
namestring
optional
Defaults to the occasion name and date.
externalReferencestring
optional
Your reference; unique within the occasion.

Returns

201 Created with the floorplan.

Notes

  • Counts toward your Metis plan allowance.
  • Nothing is generated and nobody at the venue is asked to finish anything: the copy is an ordinary plan their team can open, edit and print. A layout that is not one of your own returns 422 layout_not_found.

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"}'

Create a link to an occasion

POST/v1/occasions/{occasionId}/linksscope links:create

Opens the occasion's current floorplan — put it behind an 'Open in Metis' button in your UI. An occasion with no floorplan yet returns 409 no_floorplan, which is a different answer from 'no such occasion' and tells you to request one first.

Path parameters

NameTypeDescription
occasionIdstring
required
An occasion id.

Body fields

FieldTypeDescription
purposeenum
required
view or edit.

Returns

A link, with an expiry when there is one.

Example response

{
  "url": "https://metisroomplanner.com/planner/?plan=plan-be8d2591",
  "expiresAt": null
}

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"}'

Create a link that opens a floorplan

POST/v1/floorplans/{floorplanId}/linksscope links:create

edit opens the Metis planner — the person must be signed in to your Metis account, so this is for the venue's own team, and the link does not expire. view returns the same key-less, expiring review link as share-links, for everyone else.

Path parameters

NameTypeDescription
floorplanIdstring
required
A floorplan id.

Body fields

FieldTypeDescription
purposeenum
required
view or edit.

Returns

A link, with an expiry when there is one.

Example response

{
  "url": "https://metisroomplanner.com/review/shr-7f3a9c21",
  "expiresAt": "2026-09-24T12:00:00Z"
}

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"}'

Create a shareable review link

POST/v1/floorplans/{floorplanId}/share-linksscope links:create

A link anyone can open without a Metis account, to walk round the plan in 2D and 3D until it expires. Good for sending to the client. The link appears in the account owner's shares list in Metis, named after your integration, and they can revoke it there at any time.

Path parameters

NameTypeDescription
floorplanIdstring
required
A floorplan id.

Headers

HeaderTypeDescription
Idempotency-Keystring
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 on this call — it is one of the four that read the header.

Body fields

FieldTypeDescription
expiresInHoursinteger
optional
1–720 hours. Leave it out and Metis uses the planner's own default for a share link — this API does not impose one of its own, so read expiresAt in the response rather than assuming a value.

Returns

A link with an expiry.

Example response

{
  "url": "https://metisroomplanner.com/review/shr-7f3a9c21",
  "expiresAt": "2026-09-28T12:00:00Z"
}

Example request

curl -sS -X POST "$METIS_BASE/v1/floorplans/fp_118/share-links" \
  -H "Authorization: Bearer $METIS_API_KEY" \
  -H "Metis-Connection: $METIS_CONNECTION" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"expiresInHours": 168}'

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.

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.