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

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.

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

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"
}
StatusCodeWhat to do
400validation_failedRead errors; it's keyed by field name. Fix and resend.
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.
404resource_not_foundAlso returned for resources that belong to another account.
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.
422plan_allowance_reachedYour Metis subscription has no plans left this period. Retrying won't help.
422subscription_inactiveYour Metis subscription isn't active.
429rate_limitedBack off for Retry-After seconds.
500internal_errorSafe to retry an idempotent request with backoff. Quote the traceId.
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.

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"

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
yearinteger
optional
Only occasions whose date falls in this calendar year.
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.

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 for every POST.

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

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

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 for every POST.

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 your planners should know. Up to 4000 characters — they read this.
contactobject
optional
Who to ask about it: { name, email, phone }.
externalReferencestring
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_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"}'

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"

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/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

  • status moves requestedin_progresscompleted, 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.

Example request

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

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

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"

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

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

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

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 for every POST.

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.

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

NameTypeDescription
occasionIdstring
required
An occasion id.

Body fields

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

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

NameTypeDescription
floorplanIdstring
required
A floorplan id.

Body fields

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

Create a shareable review link Preview — returns 501 today

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. Requires a Metis subscription that includes sharing.

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 for every POST.

Body fields

FieldTypeDescription
expiresInHoursinteger
optional default 72
1–720 hours.

Returns

A link with an expiry.

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