curl --fail-with-body -X PUT "https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/quick-actions/pricing.cleaning_fee" \
-H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":{"value":"25.00","currency":"EGP"}}'const res = await fetch('https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/quick-actions/pricing.cleaning_fee', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ value: { value: '25.00', currency: 'EGP' } }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);
console.log(body.template_key, body.value, body.active);
import os
import requests
res = requests.put(
"https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/quick-actions/pricing.cleaning_fee",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
json={"value": {"value": "25.00", "currency": "EGP"}},
timeout=30,
)
body = res.json()
if res.status_code >= 400:
raise RuntimeError(f"{body['code']}: {body.get('detail')}")
print(body["template_key"], body["value"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]any{
"value": map[string]string{"value": "25.00", "currency": "EGP"},
})
req, _ := http.NewRequest(http.MethodPut, "https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/quick-actions/pricing.cleaning_fee", 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()
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated {
var problem struct {
Code string `json:"code"`
Detail string `json:"detail"`
}
_ = json.NewDecoder(res.Body).Decode(&problem)
log.Fatalf("%d %s: %s", res.StatusCode, problem.Code, problem.Detail)
}
var body map[string]any
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
fmt.Println(body["template_key"], body["value"])
}
{
"template_key": "pricing.cleaning_fee",
"family": "pricing",
"result": "updated",
"value": {
"value": "200.00",
"currency": "EGP"
},
"active": true
}Switch a quick action on, or move its value
Switch a quick action on, or move an already-applied one to a new value.
Idempotent: the identifier is in the path, so a retry re-points the same rule
rather than creating a second one. 201 the first time, 200 after that.
Omit value to take the action’s own default — you do not have to know the
number to switch something on.
value is typed by the action’s param.kind, and the typing is enforced:
| Kind | Value |
|---|---|
number, percent | a number within min–max, on step |
days_of_week | day names from the closed seven, no duplicates |
currency | a Money object whose currency equals the listing’s |
A Money object in the wrong currency is a validation_failed naming
body.value.currency. There is no conversion on this path and there will not be
one: an amount we converted for you is a number the host never chose, on every
guest’s receipt.
The scope this needs
rules:write for every key, plus pricing:write for a pricing.* key. This
is the only operation in this document whose requirement depends on the path: a
cleaning fee changes what a guest pays and a minimum stay does not, so the second
half is checked against your key’s scopes when the request arrives and refused
with the same insufficient_scope, naming what is missing.
When it is refused
pricing_mode_conflict (409) — the listing’s price is set by another source, so
a rule on it would never take effect. Release that source in the Maat app first.
Nothing is written.
validation_failed (400) — the value does not suit the action, or the action is
retired.
Live in a sandbox workspace. Returns 501 against a production one, where no
rule is applied and the listing prices exactly as it did before.
curl --fail-with-body -X PUT "https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/quick-actions/pricing.cleaning_fee" \
-H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":{"value":"25.00","currency":"EGP"}}'const res = await fetch('https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/quick-actions/pricing.cleaning_fee', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ value: { value: '25.00', currency: 'EGP' } }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);
console.log(body.template_key, body.value, body.active);
import os
import requests
res = requests.put(
"https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/quick-actions/pricing.cleaning_fee",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
json={"value": {"value": "25.00", "currency": "EGP"}},
timeout=30,
)
body = res.json()
if res.status_code >= 400:
raise RuntimeError(f"{body['code']}: {body.get('detail')}")
print(body["template_key"], body["value"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]any{
"value": map[string]string{"value": "25.00", "currency": "EGP"},
})
req, _ := http.NewRequest(http.MethodPut, "https://maatstays.wtf/v1/listings/7c4b1e58-2a90-4f61-b3d2-5e8a1c0f7d24/quick-actions/pricing.cleaning_fee", 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()
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated {
var problem struct {
Code string `json:"code"`
Detail string `json:"detail"`
}
_ = json.NewDecoder(res.Body).Decode(&problem)
log.Fatalf("%d %s: %s", res.StatusCode, problem.Code, problem.Detail)
}
var body map[string]any
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
fmt.Println(body["template_key"], body["value"])
}
{
"template_key": "pricing.cleaning_fee",
"family": "pricing",
"result": "updated",
"value": {
"value": "200.00",
"currency": "EGP"
},
"active": true
}Authorizations
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.
Path Parameters
Maat's identifier for the listing.
The quick action's identifier. A closed set. Permanent: a withdrawn template
keeps its identifier and is refused with validation_failed, never removed.
The identifier of a quick action. A CLOSED set, and every member of it is
permanent: a template that is withdrawn keeps its identifier and is reported
available: false, unavailable_reason: "retired" rather than disappearing.
This list is checked in CI against the platform's seeded templates, so it cannot lag what we actually run.
booking.min_stay, booking.max_stay, booking.no_checkout_days, booking.weekend_min_stay, pricing.weekend_surcharge, pricing.long_stay_discount, pricing.large_group_surcharge, pricing.cleaning_fee "pricing.cleaning_fee"
Body
Omit to take the action's own default. Typed by the action's param.kind;
a currency kind takes a Money object whose currency equals the
listing's.
Response
The action is on at the value in the body. result says whether this
call switched it on or moved one that was already on.
What PUT answers, and what a listing currently has switched on.
booking.min_stay, booking.max_stay, booking.no_checkout_days, booking.weekend_min_stay, pricing.weekend_surcharge, pricing.long_stay_discount, pricing.large_group_surcharge, pricing.cleaning_fee "pricing.cleaning_fee"
Which engine evaluates the rule this action created — booking rules gate a stay, pricing rules move a rate.
See RuleFamily.
booking, pricing Whether this call created the quick action or re-pointed one that
already existed. The same field upsertProperty and
upsertCalendarBlock publish — one 200 and a field in the body,
rather than a status code a caller has to branch on.
See WriteResult.
created, updated The stored value, in the shape the action's param publishes — which is
what we actually stored, not what you sent: omit value on the PUT and
this is the action's own default, resolved by the service that owns it.
null only for a corrupt row — one whose stored value does not match the
kind its descriptor declares. We publish null rather than coerce it,
because inventing a number for a value that is not one would put it on a
guest's receipt. The read publishes null in the same case, so the two
surfaces cannot disagree. You should never see it; if you do, the rule
needs a human.
Whether the rule is live. A rule can be attached but paused in the Maat app; nothing on this API pauses one.