> ## Documentation Index
> Fetch the complete documentation index at: https://developer.maatstays.wtf/llms.txt
> Use this file to discover all available pages before exploring further.

# Release a partner block

> Removes the block you pushed under this `external_uid` and frees the nights it
still holds.

It frees **only** the nights that block is holding. A night that is unavailable
for another reason — the property's own owner closed it, or a Maat booking has
since taken it — stays unavailable. Releasing a block is not the same as
declaring those dates open.

Idempotent: releasing a block that does not exist, or releasing the same one
twice, is `204`. There is nothing to retry and nothing has gone wrong.

There is no partial release. Send the whole block or move it with a `PUT`.

**Live in a sandbox workspace. Returns `501` against a production one**, where no
block was ever applied and so none can be released.




## OpenAPI

````yaml /openapi/openapi.yaml delete /blocks/{external_uid}
openapi: 3.1.0
info:
  title: Maat Stays Partner API
  version: '2026-10-16'
  summary: Manage your Maat listings from your own system.
  contact:
    name: Maat Stays developer platform
    url: https://developer.maatstays.wtf
  description: >
    The Partner API lets a host workspace — a host, or a property company with
    its own

    system, website or app — manage its own Maat listings, calendars, rates,
    bookings and

    reviews from that system. Every key belongs to one workspace, is issued from
    that

    workspace's own Maat account, and reaches that workspace's inventory and
    nothing else.


    ```

    Your system  ──  properties, photos, calendar blocks, rates, replies  ──▶ 
    Maat Stays

    Your system  ◀──  booking, calendar, listing and review notifications  ── 
    Maat Stays

    ```


    Inbound, you push **what you manage**: the properties, their photos and
    rates, and the

    nights that are already taken elsewhere — keyed by your own system's
    identifiers where

    you have them. Outbound, we push **what happened here**: a guest booked one
    of your

    listings on Maat, a booking was cancelled, a night was held, a review was
    written.


    Nothing about a guest travels inbound, so a calendar block says "these
    nights are gone"

    and nothing else. Outbound carries no guest identity either: a booking event
    tells you

    what is happening on your inventory, never who it is happening to. The only
    guest

    identity any read in this document returns is the shortened public reviewer
    label on

    `GET /listings/{listing_id}/reviews` (`"Sarah M."`), the same string the
    public listing

    page shows. See the data boundary guide at

    https://developer.maatstays.wtf/guides/data-boundary.


    # Contract status — read this before you build


    **Every operation states its own availability.** Machine-readably as

    `x-maat-availability` on the operation, and as one sentence in its
    description. Against a

    **production** workspace every contract call authenticates, rate-limits and
    validates in

    full, then returns `501 Not Implemented` with a problem document whose
    `code` is

    `not_enabled`: nothing is written, nothing is read, and nothing is
    delivered. The

    credential exchange (`POST /token`) is the one operation that also answers a
    production

    workspace, because it is how you find out which kind you hold — read
    `sandbox` in its

    response.


    `x-maat-availability` names the workspace kind an operation does real work
    on today. Its

    values are `sandbox`, `production` and `none`; every contract operation
    carries

    `sandbox`, and nothing carries `production`. The outbound half — the
    `webhooks` section —

    has no status code to annotate, so each delivery says which workspace kind
    it is

    delivered on in its own description instead.


    The split is decided by the workspace your key was issued for, not by a flag
    you can send.


    A fabricated `200` would tell you a calendar block was applied — so you
    would stop holding

    those nights on your side, and the next guest would book a room that is
    already occupied.

    That is not a mistake you could detect from our response.


    What is live everywhere: authentication, authorization, the workspace
    boundary, rate

    limiting and **every schema in this document**. Send us a malformed payload
    today and you

    get the same `validation_failed` you will get the day the contracts are
    enabled. Build

    against it now; nothing will change shape underneath you.


    One thing you can only exercise on a sandbox workspace is a **conflict**. If
    nights you

    push are already booked or held on Maat, we refuse the whole range with
    `409` and name the

    colliding dates — we never apply part of a block, because a partial apply
    you were told

    succeeded is the one failure you could not detect.


    # Authentication


    You hold a long-lived API key (`maat_partner_…`) and exchange it for a
    short-lived bearer

    token. The key never reaches a contract endpoint; the token never lives
    longer than an

    hour.


    ```bash

    # 1. Exchange the key for a token. The body is empty on purpose:

    #    the workspace, the environment and your identity all come from

    #    the key itself, never from the request.

    curl --fail-with-body -X POST https://maatstays.wtf/v1/token \
      -H "Authorization: Bearer $MAAT_PARTNER_KEY"

    # → 200

    # {

    #   "access_token": "eyJhbGciOiJIUzI1NiIs…",

    #   "token_type": "Bearer",

    #   "expires_in": 900,

    #   "workspace_id": "3f6b0f2e-1c3a-4d5e-9a70-8b2c4d6e0f11",

    #   "sandbox": true,

    #   "scope": ["partner:token.issue", "catalog:write", "calendar:read",
    "calendar:write", "webhooks:manage"]

    # }


    # 2. Call a contract with the token.

    curl --fail-with-body -X PUT https://maatstays.wtf/v1/blocks/my-res-55123 \
      -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs…" \
      -H "Content-Type: application/json" \
      -d '{"provider_id":"my-prop-9001","check_in":"2026-09-01","check_out":"2026-09-05"}'
    ```


    **Every response in this API is a problem document**, the token exchange
    included. It

    answers one indistinguishable `401 unauthorized` for an unknown key, a
    revoked key and a

    wrong secret, and `503 service_unavailable` when token signing is not
    configured on our

    side — in the same RFC 7807 shape as everything else.


    Revoking a key kills its outstanding tokens on their **next request**. We
    re-check the key

    on every call rather than waiting for a token to expire.


    # Sandbox


    Every key is issued for exactly one Maat workspace, and that workspace is
    either a

    **sandbox** one or a **production** one. Those two are what this document
    calls

    *tenancies*: a tenancy is not a server, a region or a hostname — there is
    one host — it is

    which kind of workspace your key was issued for. A sandbox workspace is a
    real tenancy

    with real endpoints and real create-then-read loops, and everything in it is
    kept off

    every public surface: it does not appear in search, on the map, in the guest
    feed, on a

    public listing page, or in any notification.


    The environment is not a parameter and there is no header that changes it.
    Your token

    carries the environment it was issued for, we compare it against the
    workspace on every

    request, and a disagreement in either direction is a
    `workspace_scope_mismatch`.


    A sandbox booking that ends in a real charge is not available: sandbox
    workspaces do not

    reach a payment provider at all, and a hold on one is refused rather than
    faked.


    # Idempotency


    The two inbound contracts are `PUT` to a URL that contains **your**
    identifier — your

    `provider_id` for a property, your `external_uid` for a block. Retrying is
    safe by

    construction; there is no idempotency header to remember and no request-body
    field that

    can be forgotten.


    Your identifiers are unique to your workspace. Two workspaces can use the
    same

    `provider_id` for different properties and neither will ever see the
    other's.


    **One operation is not safe to retry blindly**, and it is the only one:

    `POST /properties/{property_id}/photos/presign` carries no identifier of
    yours, so a

    repeat mints a second set of `photo_id`s and spends the daily photo
    allowance again. Its

    own description says what to do when a response is lost. Everything else
    here either

    writes to a URL you chose, or is a `GET` or a `DELETE` whose second call is
    a no-op.


    # Conventions


    **Paths in this reference are relative to the server URL**, which already
    carries `/v1`.

    `PUT /blocks/{external_uid}` on an operation page is `PUT
    https://maatstays.wtf/v1/blocks/{external_uid}`

    on the wire. The guides write the full path.


    **Every field and every parameter is snake_case**, every `operationId` is
    lowerCamel, and

    every object refuses a field it does not declare (`additionalProperties:
    false`), in both

    directions. A typo'd field is a `400`, never a silently ignored one.


    **`operationId` says what the call does to the resource**, and the verb
    comes from a

    closed list:


    | Verb | When |

    | :--- | :--- |

    | `get` | one resource |

    | `list` | a collection |

    | `upsert` | a `PUT` to a URL that carries **your own** identifier — it
    creates or replaces |

    | `replace` | a `PUT` that replaces a Maat-keyed resource, or one facet of
    it, whole |

    | `patch` | a `PATCH` that merges; an absent member is untouched |

    | `delete` | the thing addressed ceases to exist |

    | `release` | you give up a claim you took, and the thing itself remains |


    Eleven operations do something that is none of those, and each keeps its own
    verb:

    `registerWebhookEndpoint` (it runs a signed challenge and activates only on
    the echo),

    `rotateWebhookSecret`, `redriveWebhookDelivery`, `issuePartnerToken`,
    `presignPropertyPhotos`, `replyToReview`, `confirmBooking`,

    `rejectBooking`, `blockListingNights`, `applyQuickAction` and
    `takeListingPricingMode`.

    Nothing else invents a verb.


    **A request schema with no properties is not a mistake.**
    `BookingActionRequest`,

    `RotateWebhookSecretRequest` and `RedriveWebhookDeliveryRequest` exist so
    that the

    operation's shape is complete and its refusal of unknown fields is
    published. Their

    bodies are optional: send `{}` or send nothing.


    # Errors


    Every response in this API uses `application/problem+json`

    ([RFC 7807](https://www.rfc-editor.org/rfc/rfc7807)):


    ```json

    {
      "type": "https://developer.maatstays.wtf/errors/not-enabled",
      "title": "Not implemented",
      "status": 501,
      "detail": "The calendar-blocks contract is published but not yet enabled…",
      "instance": "urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33",
      "code": "not_enabled"
    }

    ```


    **Branch on `code`, never on `type` or on the wording of `detail`.** `code`
    is the

    contract; the rest is documentation. `code` is snake_case; `type` is derived
    from it by

    replacing underscores with hyphens (`not_enabled` → `/errors/not-enabled`),
    so the two

    never carry different vocabularies — but only one of them is the thing to
    switch on.


    | `code`                     | Status | What it
    means                                                        |

    | -------------------------- | ------ |
    -------------------------------------------------------------------- |

    | `validation_failed`        | 400    | The payload does not match this
    document. `errors[]` names the fields. |

    | `unauthorized`             | 401    | No usable token: missing, malformed,
    expired, or its key was revoked.  |

    | `workspace_scope_mismatch` | 403    | The token's environment does not
    match the workspace it addresses.     |

    | `insufficient_scope`       | 403    | The key behind the token lacks this
    operation's scope. `detail` names it. |

    | `property_not_found`       | 404    | No such property in the workspace
    your key addresses.                  |

    | `unit_not_found`           | 404    | No such unit in the workspace your
    key addresses.                      |

    | `listing_not_found`        | 404    | No such listing in the workspace
    your key addresses.                   |

    | `booking_not_found`        | 404    | No such booking in the workspace
    your key addresses. Never says which. |

    | `review_not_found`         | 404    | No such review in the workspace your
    key addresses. Never says which.  |

    | `delivery_not_found`       | 404    | No such delivery — for you. Never
    says which.                          |

    | `photo_not_found`          | 404    | No such photo on that property. One
    answer covers a photo that does not exist and one that belongs to a
    different property. |

    | `conflict`                 | 409    | The nights collide with a Maat
    booking or hold — or, on a different operation, the thing you addressed is
    not in a state that allows it. `detail` says which. |

    | `property_not_simple`      | 409    | The property is not one unit with
    one listing, so a whole-property edit has no single target. |

    | `listing_pooled_unsupported` | 409  | The listing sells a room type over
    several rooms, so a calendar block has no single unit to close. |

    | `inventory_not_supported`  | 409    | The listing is an experience or a
    service. This contract manages stays.  |

    | `pricing_mode_conflict`    | 409    | The listing's price is set by
    another source, so the write would never apply. `detail` names it. |

    | `rate_limited`             | 429    | Too many requests for this key. See
    `Retry-After`.                     |

    | `photo_quota_exceeded`     | 429    | This key has presigned its whole
    daily photo allowance. No `Retry-After` — resume after midnight UTC. |

    | `test_event_budget_exhausted` | 429 | This credential's test-event
    allowance for the trailing 24 hours is spent. See `Retry-After`. Only `POST
    /webhooks/endpoint/test` produces it. |

    | `internal_error`           | 500    | Our fault. Retry an idempotent
    request; quote `instance` if it persists.|

    | `not_enabled`              | 501    | The contract is published and not
    yet enabled. Nothing was written.    |

    | `service_unavailable`      | 503    | A capability we need is not
    configured. Nothing was written; retry.     |


    `instance` is an occurrence identifier, not a URL. Quote it when you raise
    the problem —

    https://developer.maatstays.wtf/guides/developer-access says where that
    goes.


    We do not tell you which of several refusals you hit — an unknown key, a
    revoked key and a

    wrong secret all answer identically, and so do the three ways a workspace
    scope can fail.

    Distinguishing them would let anyone enumerate our customers.


    # Rate limits


    600 requests an hour per API key across all contract endpoints, by default.
    The ceiling

    is a property of your key: your Maat contact can raise it for yours alone,
    and it

    applies on your next request. Read `RateLimit-Limit` rather than assuming
    600 — it

    always reports the figure that applies to the key you are calling with.


    Every response carries:


    | Header                |
    Meaning                                              |

    | --------------------- |
    ---------------------------------------------------- |

    | `RateLimit-Policy`    | The quota and its window, e.g.
    `600;w=3600`          |

    | `RateLimit-Limit`     | The quota for your key — 600 unless yours was
    raised |

    | `RateLimit-Remaining` | What is left in the current
    window                   |

    | `RateLimit-Reset`     | Seconds until the window
    resets                      |


    These are declared on every response in this document, so a generated client
    can read

    them without parsing headers by hand. A `429` adds `Retry-After`, in
    seconds.


    One honest exception: a `401` or `403` raised by **authentication itself**
    carries no

    `RateLimit-*` headers. That refusal happens before your key is known, so
    there is no

    bucket to report — and reporting one would be inventing a number.
    `X-Correlation-Id` is

    on those responses like every other.


    `POST /token` does not draw on the 600. It has two ceilings of its own, both
    defaults

    rather than contract: **60 exchanges an hour from one IP address**, and
    **1,000 an hour

    per API key**. Both are generous for correct use — a token is good for
    fifteen minutes,

    so one exchange covers a whole sync — and tight enough that guessing a key
    is pointless.


    The MCP documentation server (`POST /mcp`, authorized by the key itself
    rather than by a

    token) draws on the same per-key hourly ceiling and on nothing else.


    # Webhooks


    Booking, calendar, listing and review notifications are delivered to one
    endpoint you

    register with `PUT /webhooks/endpoint`. Deliveries are signed:


    ```

    X-Signature:           hex(HMAC-SHA256(secret, "v1:" + timestamp + ":" +
    raw_body))

    X-Signature-Timestamp: 1788000000

    ```


    Verify against the **raw request body, before parsing it**. Re-serialised
    JSON does not

    round-trip byte-identically and your signature will not match. Reject a
    timestamp more

    than five minutes old.


    Every delivery carries an `event_id` that is stable across our retries, so
    you can be

    idempotent. We never follow redirects, and we only ever deliver over HTTPS
    to a public

    address whose certificate chains to a publicly trusted root.


    # Example values


    Hostnames in the examples that end `.invalid` —
    `hooks.example-partner.invalid`,

    `images.example-partner.invalid` — are deliberately unroutable. `.invalid`
    is reserved by

    [RFC 2606](https://www.rfc-editor.org/rfc/rfc2606) precisely so a
    documentation example

    can never resolve to somebody's real server. They are placeholders for your
    own host, not

    typos and not endpoints to call.


    Amounts are `EGP` throughout except where an example is specifically about a
    listing

    priced in another currency; there is nothing special about either. A listing
    is priced

    in one currency, chosen by the workspace, and this API never converts
    between them — a

    rate you send must match the listing's own currency or it is refused.
servers:
  - url: https://maatstays.wtf/v1
    description: >-
      The one host this contract is published against. Whether a call lands on a
      sandbox or a production workspace is decided by the key you exchanged,
      never by the hostname.
security:
  - partnerAuth: []
tags:
  - name: authentication
    description: >
      Exchanging your API key for the short-lived token every other call
      carries.
  - name: properties
    description: >
      Push your properties into Maat from your own system, keyed by that
      system's own

      `provider_id`, and read back everything your workspace has on Maat.
  - name: units
    description: >
      The physical inventory under a property: how many it sleeps, its door
      times, its

      house-rule answers.
  - name: listings
    description: >
      The sellable view of a unit — title, rate, badges, and whether it is on
      sale.
  - name: catalog
    description: >
      The reference lists a property push has to speak — property types,
      amenities and

      house rules. The same in every workspace.
  - name: photos
    description: >
      A property's photographs: what state each one is in, how to upload more,
      how to

      arrange them, and how to remove one.
  - name: calendar
    description: >
      What each night is doing, and who is holding it — plus the **host blocks**
      your

      workspace closes nights with, which are keyed by date rather than by an
      identifier of

      yours. The blocks your system pushes under its own identifiers are under
      `blocks`.
  - name: blocks
    description: >
      **Partner blocks.** Push the nights your own system already has taken — a
      stay sold

      through your own website, an owner's week — keyed by your system's own
      `external_uid`,

      and release them again. Only the credential's workspace can release one.
      Carries no

      guest data of any kind — no name, no booking reference, no amount, no free
      text.


      Not to be confused with the **host blocks** under `calendar`, which are a
      different

      resource: keyed by date rather than by an identifier of yours.
  - name: pricing
    description: >
      The quick actions that price and gate a listing, and the rules attached to
      it.
  - name: bookings
    description: >
      What is happening on your inventory — never who it is happening to. There
      is no guest

      identity anywhere in this tag; a person is always one click away, in the
      Maat host

      console.
  - name: sandbox
    description: >
      Tools that exist only in a sandbox workspace. Today that is one: a booking
      fixture,

      which mints a synthetic booking on your own sandbox listing so you can
      watch the real

      `booking.*` webhooks arrive at your endpoint before you go live.
  - name: reviews
    description: >
      A listing's reviews in their public form, and the one reply a workspace
      can leave on

      each.
  - name: finance
    description: >
      What your inventory earned, what has been paid out, and what you owe —
      read-only, per

      settlement currency, and never converted or added across currencies.


      There is no write here and no finance notification: asking to be paid
      commits banking

      details, and a balance is a position you poll rather than an event.
  - name: webhooks
    description: >
      Operating the endpoint you registered with `PUT /webhooks/endpoint` from
      the outside:

      reading it back, rotating its secret, deleting it, and reconciling what we
      tried to

      deliver.
  - name: booking-notifications
    description: >
      Where Maat delivers a booking on one of your properties, and the payloads
      it sends.
paths:
  /blocks/{external_uid}:
    delete:
      tags:
        - blocks
      summary: Release a partner block
      description: >
        Removes the block you pushed under this `external_uid` and frees the
        nights it

        still holds.


        It frees **only** the nights that block is holding. A night that is
        unavailable

        for another reason — the property's own owner closed it, or a Maat
        booking has

        since taken it — stays unavailable. Releasing a block is not the same as

        declaring those dates open.


        Idempotent: releasing a block that does not exist, or releasing the same
        one

        twice, is `204`. There is nothing to retry and nothing has gone wrong.


        There is no partial release. Send the whole block or move it with a
        `PUT`.


        **Live in a sandbox workspace. Returns `501` against a production one**,
        where no

        block was ever applied and so none can be released.
      operationId: releaseCalendarBlock
      parameters:
        - $ref: '#/components/parameters/ExternalUid'
      responses:
        '204':
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          description: |
            The block is gone, or was never there. No body either way.
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '501':
          $ref: '#/components/responses/NotEnabled'
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >
            curl --fail-with-body -X DELETE
            https://maatstays.wtf/v1/blocks/my-res-55123 \
              -H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
              -i
        - lang: javascript
          label: Node.js
          source: >
            const res = await
            fetch('https://maatstays.wtf/v1/blocks/my-res-55123', {
                method: 'DELETE',
                headers: { Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}` },
            });


            // 204 either way — released, or it was never there. Idempotent.

            if (res.status !== 204) {
                const problem = await res.json();
                throw new Error(`${problem.code}: ${problem.detail}`);
            }
        - lang: python
          label: Python
          source: |
            import os

            import requests

            res = requests.delete(
                "https://maatstays.wtf/v1/blocks/my-res-55123",
                headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
                timeout=30,
            )

            # 204 either way — released, or it was never there. Idempotent.
            if res.status_code != 204:
                problem = res.json()
                raise RuntimeError(f"{problem['code']}: {problem.get('detail')}")
        - lang: go
          label: Go
          source: |
            package main

            import (
                "encoding/json"
                "log"
                "net/http"
                "os"
            )

            func main() {
                req, _ := http.NewRequest(http.MethodDelete,
                    "https://maatstays.wtf/v1/blocks/my-res-55123", nil)
                req.Header.Set("Authorization", "Bearer "+os.Getenv("MAAT_PARTNER_TOKEN"))

                res, err := http.DefaultClient.Do(req)
                if err != nil {
                    log.Fatal(err)
                }
                defer res.Body.Close()

                // 204 either way — released, or it was never there. Idempotent.
                if res.StatusCode != http.StatusNoContent {
                    var problem map[string]any
                    if err := json.NewDecoder(res.Body).Decode(&problem); err != nil {
                        log.Fatal(err)
                    }
                    log.Fatalf("%v: %v", problem["code"], problem["detail"])
                }
            }
components:
  parameters:
    ExternalUid:
      name: external_uid
      in: path
      required: true
      description: >
        Your own identifier for the block, typically your reservation id. Unique
        to you.
      schema:
        type: string
        minLength: 1
        maxLength: 255
      example: my-res-55123
  headers:
    CorrelationId:
      description: >
        This request's own identifier, on **every** response including the ones
        with no

        body. It is the same value as `instance` in a problem document, minus
        the

        `urn:maat:request:` prefix, and it is what to quote when you raise a
        problem —

        https://developer.maatstays.wtf/guides/developer-access says where that
        goes.


        Send your own `X-Correlation-Id` and we return it unchanged — as long as
        it is

        at most 200 characters of visible ASCII with no spaces. Anything else is

        replaced by an id of ours, returned in this same header and used as
        `instance`,

        so the id you are handed is always one that resolves on both sides.
        Nothing is

        ever truncated: a clipped id is a wrong id that looks right.
      schema:
        type: string
        maxLength: 200
      example: 3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
    RateLimitPolicy:
      description: >
        The quota and its window, e.g. `600;w=3600`. Absent on a `401` or `403`
        from

        authentication itself, which happens before your key is known.
      schema:
        type: string
      example: 600;w=3600
    RateLimitLimit:
      description: >
        The quota for the current window, for THIS key. 600 by default; a key
        whose

        ceiling has been raised reports its own figure here.
      schema:
        type: integer
      example: 600
    RateLimitRemaining:
      description: Requests left in the current window. Pace against this.
      schema:
        type: integer
      example: 587
    RateLimitReset:
      description: Seconds until the window resets.
      schema:
        type: integer
      example: 2384
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
      example: 2384
  responses:
    ValidationFailed:
      headers:
        X-Correlation-Id:
          $ref: '#/components/headers/CorrelationId'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      description: The payload does not match this document. Nothing was written.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://developer.maatstays.wtf/errors/validation-failed
            title: Validation failed
            status: 400
            instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
            code: validation_failed
            errors:
              - field: body.check_out
                code: custom
    Unauthorized:
      headers:
        X-Correlation-Id:
          $ref: '#/components/headers/CorrelationId'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      description: >
        No usable partner token. One answer covers a missing token, an expired
        one, a

        revoked key and a human credential — telling them apart would report
        which

        credentials exist.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://developer.maatstays.wtf/errors/unauthorized
            title: Unauthorized
            status: 401
            detail: A valid partner token is required.
            instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
            code: unauthorized
    Forbidden:
      headers:
        X-Correlation-Id:
          $ref: '#/components/headers/CorrelationId'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      description: >
        Authenticated, and not allowed. Two codes share this status — branch on
        `code`:


        - `workspace_scope_mismatch` — the token's environment does not match
        the
          workspace it addresses (a sandbox token against a production workspace, or the
          reverse; one answer also covers a workspace that does not resolve).
        - `insufficient_scope` — the key behind the token was not issued with
        the scope
          this operation requires (`x-maat-scope` on the operation). `detail` names the
          missing scope; ask whoever issued the key for one that carries it.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            workspaceScopeMismatch:
              summary: The token's environment does not match the workspace
              value:
                type: >-
                  https://developer.maatstays.wtf/errors/workspace-scope-mismatch
                title: Workspace scope mismatch
                status: 403
                detail: This token is not valid for the requested workspace.
                instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
                code: workspace_scope_mismatch
            insufficientScope:
              summary: The key lacks this operation's scope
              value:
                type: https://developer.maatstays.wtf/errors/insufficient-scope
                title: Insufficient scope
                status: 403
                detail: >-
                  This key was not issued with the `catalog:write` scope. Ask
                  whoever issued it to issue a key that carries it.
                instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
                code: insufficient_scope
    RateLimited:
      description: >
        Too many requests for this API key. `Retry-After` gives the wait in
        seconds.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-Correlation-Id:
          $ref: '#/components/headers/CorrelationId'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://developer.maatstays.wtf/errors/rate-limited
            title: Too many requests
            status: 429
            detail: Too many requests for this API key.
            instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
            code: rate_limited
    InternalError:
      headers:
        X-Correlation-Id:
          $ref: '#/components/headers/CorrelationId'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      description: Our fault. Retry an idempotent request.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://developer.maatstays.wtf/errors/internal-error
            title: Internal error
            status: 500
            detail: The request could not be completed.
            instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
            code: internal_error
    NotEnabled:
      headers:
        X-Correlation-Id:
          $ref: '#/components/headers/CorrelationId'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      description: >
        **What every contract endpoint returns against a production workspace.**
        The

        request was authenticated, rate-limited and validated in full; nothing
        was

        written, nothing was read and nothing will be delivered.


        Every operation in this document does real work on a **sandbox**
        workspace —

        `x-maat-availability` on the operation says so, and its description says
        what

        changes. This response is what the same call gets against a production
        one.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://developer.maatstays.wtf/errors/not-enabled
            title: Not implemented
            status: 501
            detail: >-
              The calendar-blocks contract is published but not yet enabled.
              This request was authenticated and validated; no dates were
              blocked. Keep holding these dates on your own platform.
            instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
            code: not_enabled
  schemas:
    Problem:
      type: object
      title: Problem
      description: >
        An RFC 7807 problem document. Every error in this API is one of these,
        served

        as `application/problem+json` — the token exchange included.


        Branch on `code`. `type` is a stable identifier for the same thing,
        `title` and

        `detail` are for people, and `instance` identifies this one occurrence.


        **The machine-readable facts are the typed members, never the prose.**
        `conflicts`

        carries every colliding date on a `conflict` over a calendar, and
        `errors` carries

        every rejected field on a `validation_failed`. Where one of those is
        present it is

        complete, and `detail` is a shortened rendering of it for a person to
        read.


        `type`, `title`, `status` and `code` are the four this API always sends.
        `detail`

        is sent on every problem it produces today and `instance` on every
        request that

        reached it, but neither is `required` — RFC 7807 makes both optional and
        a future

        problem may carry no `detail`. `additionalProperties` is deliberately
        left unset:

        RFC 7807 lets a problem type define its own members, `conflicts` and
        `errors` are

        two of ours, and more may be added. A member you do not recognise is
        safe to

        ignore.
      required:
        - type
        - title
        - status
        - code
      properties:
        type:
          type: string
          format: uri
          description: >-
            A stable identifier, not a URL to fetch. It is `code` with
            underscores replaced by hyphens.
          example: https://developer.maatstays.wtf/errors/not-enabled
        title:
          type: string
          description: >-
            A short, human-readable summary of the status. For people; we
            reserve the right to rewrite it.
          example: Not implemented
        status:
          type: integer
          description: The HTTP status code, repeated in the body.
          example: 501
        detail:
          type: string
          description: >-
            What went wrong on this request, in a sentence for a person. Never
            parse it — it is rewritten without notice, and every
            machine-readable fact is a sibling member. Present on every problem
            this API produces today; it is not `required` because RFC 7807 does
            not require it.
        instance:
          type: string
          description: >
            An occurrence identifier, not a URL. Quote it when you raise the

            problem — https://developer.maatstays.wtf/guides/developer-access
            says

            where that goes. It is the same value as the `X-Correlation-Id`
            response

            header, which every response carries — including the ones with no
            body.
          example: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
        code:
          $ref: '#/components/schemas/ProblemCode'
          description: >-
            The contract. Branch on this, never on `type`, `title` or `detail`.
            A closed vocabulary that only ever grows, so treat an unrecognised
            value as its HTTP status class.

            See [`ProblemCode`](/api-reference/enums/problem-code).
        conflicts:
          type: array
          description: >
            Present on `conflict`. The complete list of colliding dates — never

            truncated, unlike the sentence in `detail`. Act on this, not on the
            prose.
          items:
            type: string
            format: date
          example:
            - '2026-09-02'
            - '2026-09-03'
        errors:
          type: array
          description: >
            Present on `validation_failed`. Names the rejected fields and the
            rule

            each one broke — never the value you sent.
          items:
            $ref: '#/components/schemas/ValidationError'
    ProblemCode:
      type: string
      title: ProblemCode
      description: >
        The contract of an error. Branch on this, never on `type`, `title` or
        `detail`.


        A closed vocabulary that only ever grows, so treat an unrecognised value
        as its HTTP

        status class. A code is permanent from the moment it is published:
        renaming one is a

        breaking change.
      enum:
        - validation_failed
        - unauthorized
        - workspace_scope_mismatch
        - insufficient_scope
        - property_not_found
        - listing_not_found
        - unit_not_found
        - conflict
        - property_not_simple
        - listing_pooled_unsupported
        - inventory_not_supported
        - photo_not_found
        - rate_limited
        - photo_quota_exceeded
        - internal_error
        - not_enabled
        - service_unavailable
        - pricing_mode_conflict
        - booking_not_found
        - review_not_found
        - delivery_not_found
        - redrive_budget_exhausted
        - test_event_budget_exhausted
        - rotation_in_progress
        - sandbox_only
      x-enum-descriptions:
        validation_failed: >-
          The body, query or path failed the published schema. `errors` names
          the fields.
        unauthorized: >-
          No usable token: absent, malformed, expired, revoked, or a human
          credential.
        workspace_scope_mismatch: >-
          The token's environment claim disagrees with the workspace it
          addresses.
        insufficient_scope: Your key was not issued with the scope this route requires.
        property_not_found: >-
          No such property in this workspace. One answer for "no such" and "not
          yours".
        listing_not_found: No such listing in this workspace.
        unit_not_found: No such unit in this workspace.
        conflict: >-
          The write collides with something already on the calendar. `conflicts`
          lists the dates.
        property_not_simple: >-
          The property has a topology this contract cannot address from a
          property id alone.
        listing_pooled_unsupported: >-
          The listing sells a room type over several rooms, so a calendar block
          has no single unit to close. Retrying cannot help.
        inventory_not_supported: The listing is not a stay. See `InventoryKind`.
        photo_not_found: No such photo on that property.
        rate_limited: >-
          Too many requests. Read the rate-limit headers and retry after the
          window.
        photo_quota_exceeded: The property already holds as many photos as it may.
        internal_error: A fault on our side. Quote `instance` when you report it.
        not_enabled: The contract is published but does no work for this workspace yet.
        service_unavailable: A dependency was unreachable. Retryable.
        pricing_mode_conflict: Another source prices this listing; you do not hold it.
        booking_not_found: No such booking in this workspace.
        review_not_found: No such review in this workspace.
        delivery_not_found: No such delivery for your endpoint.
        redrive_budget_exhausted: You have spent this window's redrives.
        test_event_budget_exhausted: You have spent this window's test events.
        rotation_in_progress: >-
          A rotation overlap is still open on your endpoint, so a second
          rotation is refused until it closes. `detail` names the instant.
        sandbox_only: >-
          The operation acts on sandbox inventory only, and you named the
          listing in your own live workspace. Retrying cannot help.
      x-maat-internal-enum:
        name: ChannelPartnerProblem
        relation: equals
    ValidationError:
      type: object
      title: ValidationError
      description: >
        One rejected field on a `validation_failed`. Never carries the value you
        sent.
      required:
        - field
        - code
      additionalProperties: false
      properties:
        field:
          type: string
          description: The rejected field, as a dotted path from the request root.
          example: body.check_out
        code:
          $ref: '#/components/schemas/ValidationErrorCode'
          description: >-
            Which rule the field broke. Show it beside the field; do not switch
            on it.

            See
            [`ValidationErrorCode`](/api-reference/enums/validation-error-code).
          example: too_big
    ValidationErrorCode:
      type: string
      title: ValidationErrorCode
      description: >
        Which rule a rejected field broke.


        Show it beside the field; do not switch on it. Most are the request
        validator's own

        rule names; a few name a check that needs something your request does
        not carry, such

        as the currency a listing is priced in. Either way the list can gain
        members, and an

        unrecognised value means exactly what a recognised one does: that field
        was not

        accepted. `field` is the part to act on.
      enum:
        - invalid_type
        - invalid_value
        - invalid_format
        - too_small
        - too_big
        - not_multiple_of
        - unrecognized_keys
        - invalid_union
        - invalid_key
        - invalid_element
        - custom
        - invalid
        - currency_mismatch
        - duplicate_value
        - unsupported_param_kind
        - unavailable
        - unknown_template
      x-enum-descriptions:
        invalid_type: >-
          A string where a number was expected, an object where an array was,
          and so on.
        invalid_value: Not one of the values the field accepts.
        invalid_format: The right type, in the wrong shape — a date, a UUID, a URL.
        too_small: 'Below a minimum: a number, a length, or a count.'
        too_big: 'Above a maximum: a number, a length, or a count.'
        not_multiple_of: A number that is not a permitted step.
        unrecognized_keys: A field this contract does not accept. `field` names it.
        invalid_union: None of the shapes the field may take matched.
        invalid_key: A key in a map-shaped field was not accepted.
        invalid_element: An entry in a collection-shaped field was not accepted.
        custom: A rule specific to this contract — a cross-field constraint, usually.
        invalid: The validator gave no rule name. Read `field` and the schema.
        currency_mismatch: >-
          An amount in a currency the listing is not priced in. Amounts are
          never converted.
        duplicate_value: The same entry appears twice where each may appear once.
        unsupported_param_kind: >-
          The value this quick action takes is of a kind this contract does not
          publish.
        unavailable: >-
          Names something real that has since been withdrawn and can no longer
          be set.
        unknown_template: >-
          Names a template that is not currently published. Stop sending that
          key until it is.
      x-maat-internal-enum:
        name: ProblemFieldErrorCode
        relation: equals
  securitySchemes:
    partnerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        A short-lived partner token, obtained by exchanging your API key at

        `POST /token`. Send it as `Authorization: Bearer <access_token>`.


        Tokens live for fifteen minutes by default and never for more than an
        hour.

        Mint one per batch of work, not one per request.


        The token carries the workspace and the environment its key was issued
        for. You

        cannot change either by asking: the exchange ignores everything in the
        request

        body for exactly that reason.


        It also carries the key's **scopes** (`scope`, a list). Every operation
        names a

        scope as `x-maat-scope`, and a token whose key lacks it is refused with

        `403 insufficient_scope`, naming the scope. Scopes are fixed when a key
        is

        issued and are re-read from the key on every request, so a key narrowed
        today is

        narrowed on its next call — the token's `scope` is what the key held at
        exchange

        time, for your own reading, never what authorizes a request.


        **`x-maat-scope` is the floor, and on the quick-action operations it is
        not the

        whole requirement.** `PUT` and `DELETE
        /listings/{listing_id}/quick-actions/{template_key}`

        carry `rules:write`, and additionally require **`pricing:write` when

        `template_key` begins `pricing.`** — the capability depends on the
        template in

        the path, because a cleaning fee changes what a guest pays and a minimum
        stay

        does not. The refusal is the same `403 insufficient_scope`, naming

        `pricing:write`. The two READS apply the same requirement without
        erroring:

        `GET /listings/{listing_id}/quick-actions` still lists the `pricing.*`
        rows but

        withholds their `applied` state, and `GET /listings/{listing_id}/rules`
        omits the

        pricing rules entirely and reports `pricing_withheld` in `warnings`.
        These four

        are the only operations in this document whose requirement is not
        exactly their

        `x-maat-scope`, and each of them says so machine-readably:

        `x-maat-scope-additional` lists the second scope it can require and

        `x-maat-scope-additional-when` says what happens without it.


        Never send it as a cookie or in a query string, and never store it —
        store the

        API key, mint tokens from it.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.