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

# Close nights with a host block

> Close a set of nights on a listing, the way the Maat app's "Block dates" does.
The nights need not be contiguous — this is a set of days, not a range — and up
to 366 of them fit in one request.

**This is not `PUT /v1/blocks/{external_uid}`.** That contract records a night as
sold in YOUR system, keyed on your own reservation id and releasable only through
that id. This one writes a closure of the kind the Maat app makes — maintenance,
an owner's stay — with a `category`, keyed by date. If you are syncing
reservations from your own system, `/v1/blocks` is your endpoint; this one is for
closing nights by date.

**There is no `note` and no free-text field of any kind.** An unrecognised field
is a `validation_failed` naming it, never a silent drop — the calendar direction
of this API carries no guest data, and the obvious thing anybody writes in a note
is a guest's name.

Dates must be today or later, in **UTC**. A property has its own time zone, but a
bound derived from it would make the same request behave differently for two
listings in one account.

**Conflicts refuse the whole range.** If any night you name carries a Maat
booking or a live hold, nothing at all is written and the `409` names every
colliding night in `conflicts`. We never apply the free part of a range: a
partial apply you were told succeeded is the one failure you cannot detect.

A night that is already closed by something else — a calendar feed, or a block you
pushed under your own `external_uid` — is **not** a conflict. It is already unavailable, which is
what you asked for, so it comes back under `already_held` instead: no host block
was written there, and that closure will lift when its own source lifts.

Re-sending the same nights is a no-op and answers the same receipt.

A listing that sells a room type over several rooms answers
`409 listing_pooled_unsupported` and nothing is written: a host block here closes
one unit, and such a listing's room is chosen per booking.

**Live in a sandbox workspace. Returns `501` against a production one, where no
night is blocked and everything you named stays on sale.**




## OpenAPI

````yaml /openapi/openapi.yaml post /listings/{listing_id}/calendar/blocks
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:
  /listings/{listing_id}/calendar/blocks:
    post:
      tags:
        - calendar
      summary: Close nights with a host block
      description: >
        Close a set of nights on a listing, the way the Maat app's "Block dates"
        does.

        The nights need not be contiguous — this is a set of days, not a range —
        and up

        to 366 of them fit in one request.


        **This is not `PUT /v1/blocks/{external_uid}`.** That contract records a
        night as

        sold in YOUR system, keyed on your own reservation id and releasable
        only through

        that id. This one writes a closure of the kind the Maat app makes —
        maintenance,

        an owner's stay — with a `category`, keyed by date. If you are syncing

        reservations from your own system, `/v1/blocks` is your endpoint; this
        one is for

        closing nights by date.


        **There is no `note` and no free-text field of any kind.** An
        unrecognised field

        is a `validation_failed` naming it, never a silent drop — the calendar
        direction

        of this API carries no guest data, and the obvious thing anybody writes
        in a note

        is a guest's name.


        Dates must be today or later, in **UTC**. A property has its own time
        zone, but a

        bound derived from it would make the same request behave differently for
        two

        listings in one account.


        **Conflicts refuse the whole range.** If any night you name carries a
        Maat

        booking or a live hold, nothing at all is written and the `409` names
        every

        colliding night in `conflicts`. We never apply the free part of a range:
        a

        partial apply you were told succeeded is the one failure you cannot
        detect.


        A night that is already closed by something else — a calendar feed, or a
        block you

        pushed under your own `external_uid` — is **not** a conflict. It is
        already unavailable, which is

        what you asked for, so it comes back under `already_held` instead: no
        host block

        was written there, and that closure will lift when its own source lifts.


        Re-sending the same nights is a no-op and answers the same receipt.


        A listing that sells a room type over several rooms answers

        `409 listing_pooled_unsupported` and nothing is written: a host block
        here closes

        one unit, and such a listing's room is chosen per booking.


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

        night is blocked and everything you named stays on sale.**
      operationId: blockListingNights
      parameters:
        - $ref: '#/components/parameters/ListingId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostBlockWrite'
            example:
              dates:
                - '2026-09-14'
                - '2026-09-15'
              category: maintenance
      responses:
        '200':
          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 receipt. Every night you named is closed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HostBlockReceipt'
              example:
                listing_id: 7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24
                blocked:
                  - '2026-09-14'
                  - '2026-09-15'
                already_held: []
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/HostBlockForbidden'
        '404':
          $ref: '#/components/responses/ListingNotFound'
        '409':
          $ref: '#/components/responses/HostBlockConflict'
        '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 POST
            "https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/calendar/blocks"
            \
              -H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"dates":["2026-09-14","2026-09-15"],"category":"maintenance"}'
        - lang: javascript
          label: Node.js
          source: >
            const res = await
            fetch('https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/calendar/blocks',
            {
                method: 'POST',
                headers: {
                    Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ dates: ['2026-09-14', '2026-09-15'], category: 'maintenance' }),
            });


            const body = await res.json();

            if (res.status === 409) throw new Error(`still on sale:
            ${body.conflicts.join(', ')}`);

            if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);


            console.log(body.blocked, body.already_held);
        - lang: python
          label: Python
          source: |
            import os

            import requests

            res = requests.post(
                "https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/calendar/blocks",
                headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
                json={"dates": ["2026-09-14", "2026-09-15"], "category": "maintenance"},
                timeout=30,
            )
            body = res.json()
            if res.status_code == 409:
                raise RuntimeError("still on sale: " + ", ".join(body["conflicts"]))
            if res.status_code >= 400:
                raise RuntimeError(f"{body['code']}: {body.get('detail')}")

            print(body["blocked"], body["already_held"])
        - lang: go
          label: Go
          source: |
            package main

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

            type receipt struct {
                Blocked     []string `json:"blocked"`
                AlreadyHeld []string `json:"already_held"`
                Conflicts   []string `json:"conflicts"`
            }

            func main() {
                payload, _ := json.Marshal(map[string]any{
                    "dates":    []string{"2026-09-14", "2026-09-15"},
                    "category": "maintenance",
                })

                req, _ := http.NewRequest(http.MethodPost, "https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/calendar/blocks", bytes.NewReader(payload))
                req.Header.Set("Authorization", "Bearer "+os.Getenv("MAAT_PARTNER_TOKEN"))
                req.Header.Set("Content-Type", "application/json")

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

                var body receipt
                if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
                    log.Fatal(err)
                }
                if res.StatusCode == http.StatusConflict {
                    log.Fatalf("still on sale: %v", body.Conflicts)
                }
                fmt.Println(body.Blocked, body.AlreadyHeld)
            }
components:
  parameters:
    ListingId:
      name: listing_id
      in: path
      required: true
      description: Maat's identifier for the listing.
      schema:
        type: string
        format: uuid
      example: 7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24
  schemas:
    HostBlockWrite:
      type: object
      title: HostBlockWrite
      description: >
        The nights to close, and why. There is no `note` and no other free-text
        field —

        an unrecognised property is refused by name.
      required:
        - dates
        - category
      additionalProperties: false
      properties:
        dates:
          type: array
          description: >
            The nights to close, `YYYY-MM-DD`. Need not be contiguous. Each must
            be a

            real calendar date, today or later in UTC.
          minItems: 1
          maxItems: 366
          items:
            type: string
            format: date
          example:
            - '2026-09-14'
            - '2026-09-15'
        category:
          $ref: '#/components/schemas/HostBlockCategory'
          description: |
            Why the nights are closed. A closed set, and required.

            See [`HostBlockCategory`](/api-reference/enums/host-block-category).
          example: maintenance
    HostBlockReceipt:
      type: object
      title: HostBlockReceipt
      required:
        - listing_id
        - blocked
        - already_held
      additionalProperties: false
      properties:
        listing_id:
          type: string
          format: uuid
          description: The listing whose nights were blocked.
        blocked:
          type: array
          description: >
            Every night you named that carries a host block now — including ones
            that

            were already blocked before this call. The question is "is it
            closed?",

            not "did this call close it?".
          items:
            type: string
            format: date
        already_held:
          type: array
          description: >
            Nights you named that are closed by something else — a calendar
            feed, or a

            block you pushed under your own `external_uid` — and so carry no
            host block

            of their own.

            Not a failure: the night is unavailable, which is what you asked
            for. It

            is reported because that closure lifts when its own source lifts.


            A night that is BOOKED or HELD never appears here. It refuses the
            whole

            request with `409 conflict` instead.
          items:
            type: string
            format: date
    HostBlockCategory:
      type: string
      title: HostBlockCategory
      description: >
        Why nights on your own listing are closed. Required, and a closed set —
        there is no

        free-text alternative, because the obvious thing to write in one is a
        guest's name.
      enum:
        - maintenance
        - personal_use
        - renovation
        - cleaning
        - other
      x-enum-descriptions:
        maintenance: Repairs or upkeep.
        personal_use: The owner or someone they invited is staying.
        renovation: Building work.
        cleaning: A turnaround that needs the night.
        other: Anything else.
      x-maat-internal-enum:
        name: BlockReasonCategory
        relation: equals
    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
  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
    HostBlockForbidden:
      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.
        - `insufficient_scope` — the key was not issued with `calendar:write`.
      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 `calendar: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
    ListingNotFound:
      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 such listing in the workspace your key addresses. ONE answer for "it
        does not

        exist" and "it is not yours".
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://developer.maatstays.wtf/errors/listing-not-found
            title: Listing not found
            status: 404
            detail: No such listing in this workspace.
            instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
            code: listing_not_found
    HostBlockConflict:
      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: >
        Two different refusals share this status. **Branch on `code`, never on
        the

        status alone** — one is worth retrying with a narrower range and the
        other

        never is.


        `conflict` — the nights collide with a Maat booking or a live hold.
        **Nothing

        was written**: the whole range is refused rather than the part of it
        that was

        free, because a block you were told had applied is a block you would
        stop

        holding yourself. `conflicts` carries every colliding date. Re-push a
        range

        that excludes them, or leave the block off if the stay cannot be
        honoured.


        `inventory_not_supported` — the listing is an experience or a service,
        and

        calendar blocks are stays-only. Retrying cannot help; a listing does not

        change its inventory kind.


        `listing_pooled_unsupported` — the listing sells a room type over
        several

        rooms, and a host block here closes one unit. Nothing was written.
        Retrying

        cannot help.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            conflict:
              summary: Nights already booked or held
              value:
                type: https://developer.maatstays.wtf/errors/conflict
                title: Conflict
                status: 409
                detail: >-
                  These nights are already booked or held on Maat, so the block
                  was not applied: 2026-09-02, 2026-09-03. Nothing was written —
                  the whole range was refused rather than part of it.
                instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
                code: conflict
                conflicts:
                  - '2026-09-02'
                  - '2026-09-03'
            inventory_not_supported:
              summary: Not a stay listing
              value:
                type: https://developer.maatstays.wtf/errors/inventory-not-supported
                title: Inventory kind not supported
                status: 409
                detail: >-
                  This listing is an experience, and calendar blocks are
                  stays-only. Nothing was blocked.
                instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
                code: inventory_not_supported
            listing_pooled_unsupported:
              summary: A room type with several rooms
              value:
                type: >-
                  https://developer.maatstays.wtf/errors/listing-pooled-unsupported
                title: Listing sells a pool of rooms
                status: 409
                detail: >-
                  This listing sells a room type over several rooms, and a host
                  block closes one unit. Nothing was blocked.
                instance: urn:maat:request:3b1f7a90-0d4c-4d4e-9b2a-6c1f0a2e5d33
                code: listing_pooled_unsupported
    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
  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.