curl --fail-with-body -X POST https://maatstays.wtf/v1/webhooks/endpoint/test \
-H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "event_type": "booking.confirmed" }'const res = await fetch('https://maatstays.wtf/v1/webhooks/endpoint/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ event_type: 'booking.confirmed' }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);
// Queued. Your endpoint receives it exactly like a real event,
// with "test": true in the body.
console.log(body.delivery.delivery_id, body.test_events_remaining_today);
import os
import requests
res = requests.post(
"https://maatstays.wtf/v1/webhooks/endpoint/test",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
json={"event_type": "booking.confirmed"},
timeout=30,
)
body = res.json()
if res.status_code >= 400:
raise RuntimeError(f"{body['code']}: {body.get('detail')}")
# Queued. Your endpoint receives it exactly like a real event,
# with "test": true in the body.
print(body["delivery"]["delivery_id"], body["test_events_remaining_today"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type payload struct {
Delivery struct {
DeliveryID string `json:"delivery_id"`
} `json:"delivery"`
TestEventsRemainingToday int `json:"test_events_remaining_today"`
}
func main() {
reqBody, _ := json.Marshal(map[string]string{"event_type": "booking.confirmed"})
req, _ := http.NewRequest(http.MethodPost,
"https://maatstays.wtf/v1/webhooks/endpoint/test", bytes.NewReader(reqBody))
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 >= 400 {
log.Fatalf("test event refused: %d", res.StatusCode)
}
var body payload
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
// Queued. Your endpoint receives it exactly like a real event,
// with "test": true in the body.
fmt.Println(body.Delivery.DeliveryID, body.TestEventsRemainingToday)
}
{
"delivery": {
"delivery_id": "6f1a2b3c-4d5e-4f60-8718-9a0b1c2d3e4f",
"event_id": "9a1d2c37-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
"event_type": "booking.confirmed",
"status": "pending",
"attempts": 0,
"last_error_code": null,
"last_response_status": null,
"next_attempt_at": null,
"occurred_at": "2026-09-07T09:05:00.000Z",
"created_at": "2026-09-07T09:05:00.100Z",
"payload_present": true,
"test": true
},
"test_events_remaining_today": 19
}Send yourself a test event
Queues one real delivery of the named event to your registered endpoint, built from a fixed, obviously synthetic fixture — so you can exercise your handler end to end (signature, envelope, payload shape, your own 2xx) before a real event exists to fire.
It is a real delivery in every way that matters: the same queue, the same
dispatcher, the same signing with your current secret (and the previous one during
a rotation overlap), the same retry ladder, the same User-Agent, and the same
row in GET /webhooks/deliveries — badged "test": true there and in the body we
POST you. Verify it exactly as you verify everything else.
It is NOT a real event in two ways you can rely on. The body carries "test": true
at the top level, beside event_id, on every test delivery and on no real one —
branch on it if your handler must not act on the fixture. And the fixture’s ids
are visibly not yours: 00000000-0000-4000-8000-00000000000n, a provider_id of
test-property, a stay a week from now.
The data you receive is produced by the same projection as the real event.
There is no second serializer for test bodies to drift from the first.
event_type must be one your credential can receive, whether or not your endpoint
is subscribed to it: 400 validation_failed names the ones that are not. 409 conflict when there is no endpoint registered, or it is not active — register it
and answer the challenge first. Each accepted call spends one unit of a per-credential
allowance over a rolling 24 hours; 429 test_event_budget_exhausted carries
Retry-After. A refused call spends nothing and queues nothing.
Live in a sandbox workspace. Returns 501 against a production one.
curl --fail-with-body -X POST https://maatstays.wtf/v1/webhooks/endpoint/test \
-H "Authorization: Bearer $MAAT_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "event_type": "booking.confirmed" }'const res = await fetch('https://maatstays.wtf/v1/webhooks/endpoint/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MAAT_PARTNER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ event_type: 'booking.confirmed' }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.detail}`);
// Queued. Your endpoint receives it exactly like a real event,
// with "test": true in the body.
console.log(body.delivery.delivery_id, body.test_events_remaining_today);
import os
import requests
res = requests.post(
"https://maatstays.wtf/v1/webhooks/endpoint/test",
headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_TOKEN']}"},
json={"event_type": "booking.confirmed"},
timeout=30,
)
body = res.json()
if res.status_code >= 400:
raise RuntimeError(f"{body['code']}: {body.get('detail')}")
# Queued. Your endpoint receives it exactly like a real event,
# with "test": true in the body.
print(body["delivery"]["delivery_id"], body["test_events_remaining_today"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type payload struct {
Delivery struct {
DeliveryID string `json:"delivery_id"`
} `json:"delivery"`
TestEventsRemainingToday int `json:"test_events_remaining_today"`
}
func main() {
reqBody, _ := json.Marshal(map[string]string{"event_type": "booking.confirmed"})
req, _ := http.NewRequest(http.MethodPost,
"https://maatstays.wtf/v1/webhooks/endpoint/test", bytes.NewReader(reqBody))
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 >= 400 {
log.Fatalf("test event refused: %d", res.StatusCode)
}
var body payload
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
// Queued. Your endpoint receives it exactly like a real event,
// with "test": true in the body.
fmt.Println(body.Delivery.DeliveryID, body.TestEventsRemainingToday)
}
{
"delivery": {
"delivery_id": "6f1a2b3c-4d5e-4f60-8718-9a0b1c2d3e4f",
"event_id": "9a1d2c37-4e5f-4a6b-8c9d-0e1f2a3b4c5d",
"event_type": "booking.confirmed",
"status": "pending",
"attempts": 0,
"last_error_code": null,
"last_response_status": null,
"next_attempt_at": null,
"occurred_at": "2026-09-07T09:05:00.000Z",
"created_at": "2026-09-07T09:05:00.100Z",
"payload_present": true,
"test": true
},
"test_events_remaining_today": 19
}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.
Body
Which event to send yourself. One field; a test event is never bulk and takes no payload of yours — the body is the fixture, so what you verify is what we send.
Any event on the list, subscribed or not.
See WebhookEventType.
booking.confirmed, booking.cancelled, booking.requested, booking.rejected, booking.checked_in, booking.checked_out, booking.modified, review.created, listing.published, listing.unlisted, pricing.mode_changed, pricing.currency_changed, calendar.held, calendar.released Response
Queued. 202, not 200: nothing has been delivered yet — the dispatcher
picks it up on its next pass. Watch GET /webhooks/deliveries/{delivery_id}
for the outcome, or your own endpoint's log.
What POST /webhooks/endpoint/test answers: the delivery as queued — pending,
attempts at zero, test set — and how much of your allowance is left.
One delivery's ENVELOPE — what GET /webhooks/deliveries carries for every row.
No payload, and no field that could hold one: a delivery's body is the most
sensitive thing this platform sends anywhere, so the list carries only whether one
is still held.
Show child attributes
Show child attributes
How many test events are left in the trailing 24 hours, after this one. The allowance is per credential and rolls forward continuously — it does not reset at midnight.