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

# Quickstart

> Token, read, write, webhook — one runnable script per language, with the two loops every integration needs.

Four calls, end to end, in the language you are going to write this in. Each script below
does the same four things and handles the two failures every integration meets on day one: a
token that expired (`401`) and a ceiling you hit (`429`).

You need an API key for a **sandbox** workspace — [Get access](/guides/developer-access) —
and one environment variable:

```bash theme={null}
export MAAT_PARTNER_KEY="maat_partner_..."
```

> [!NOTE]
>
> **There is no SDK.** Every sample here is the standard HTTP client of its own language against
> the published contract — that is deliberate, and it is why the four scripts are the same length.

## Or import the collection

If you would rather press Send than run a script, import
[the Postman collection](https://developer.maatstays.wtf/openapi/postman/maat-partner-api.postman_collection.json)
— all 48 operations, foldered by tag, with the request bodies from this document already
filled in. Set the `apiKey` variable, run **Exchange your API key for a token** once, and
every other request is authorized from the token it stores.

It is generated from the OpenAPI document and checked against it on every build, so it
cannot describe a contract we no longer serve. The document itself is at
[`/openapi/openapi.yaml`](https://developer.maatstays.wtf/openapi/openapi.yaml) if
your tooling would rather have that.

## What the scripts do

**1. Exchange the key for a token**

`POST /v1/token`. The body is empty: the workspace, the environment and your scopes all come
from the key. Read `sandbox` in the response to confirm which tenancy you are in.

**2. Read your catalogue**

`GET /v1/properties`. Empty on a fresh sandbox — that is the correct answer, not a failure.

**3. Push a property**

`PUT /v1/properties/{provider_id}`, keyed by **your own** identifier, so a retry lands on
the same row.

**4. Register a webhook endpoint**

`PUT /v1/webhooks/endpoint`. We POST a signed `endpoint.challenge` to the URL during this
call and your handler must echo it back — until it does, the endpoint is
`pending_verification` and nothing is delivered. [Webhooks](/guides/webhooks) has the
verifier.

## The script

```bash cURL theme={null}
#!/usr/bin/env bash
set -euo pipefail
BASE="https://maatstays.wtf/v1"
TOKEN=""

mint_token() {
  TOKEN=$(curl --fail-with-body -sS -X POST "$BASE/token" \
    -H "Authorization: Bearer $MAAT_PARTNER_KEY" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
}

# One call, with the two retries: a fresh token on 401, a wait on 429.
call() { # call METHOD PATH [JSON]
  local method="$1" path="$2" body="${3-}" attempt=0
  while :; do
    local args=(-sS -o /tmp/maat.out -w '%{http_code}' -X "$method" "$BASE$path"
                -H "Authorization: Bearer $TOKEN" -D /tmp/maat.hdr)
    [ -n "$body" ] && args+=(-H 'Content-Type: application/json' -d "$body")
    local status
    status=$(curl "${args[@]}")
    if [ "$status" = 401 ] && [ "$attempt" -lt 1 ]; then attempt=1; mint_token; continue; fi
    if [ "$status" = 429 ]; then
      local wait
      wait=$(awk 'tolower($1)=="retry-after:"{print $2+0}' /tmp/maat.hdr)
      sleep "${wait:-5}"; continue
    fi
    if [ "$status" -ge 400 ]; then
      echo "$method $path -> $status $(python3 -c 'import json;print(json.load(open("/tmp/maat.out"))["code"])')" >&2
      return 1
    fi
    cat /tmp/maat.out
    return 0
  done
}

mint_token
call GET /properties
call PUT /properties/my-prop-9001 '{
  "name": "Courtyard flat with the blue door",
  "property_type": "apartment",
  "address": {"line1": "14 Example Street", "city": "Alexandria", "country": "EG",
              "latitude": 31.2001, "longitude": 29.9187},
  "occupancy": {"max_guests": 4, "bedrooms": 2, "beds": 3, "bathrooms": 1},
  "base_rate": {"value": "1250.00", "currency": "EGP"},
  "status": "listed"
}'
call PUT /webhooks/endpoint '{
  "url": "https://hooks.example-partner.invalid/maat",
  "events": ["booking.confirmed", "booking.cancelled"]
}'
```

```javascript Node.js theme={null}
const BASE = 'https://maatstays.wtf/v1';
let token = null;

async function mintToken() {
    const res = await fetch(`${BASE}/token`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${process.env.MAAT_PARTNER_KEY}` },
    });
    const body = await res.json();
    if (!res.ok) throw new Error(`token: ${res.status} ${body.code ?? body.message}`);
    token = body.access_token;
    return body; // `sandbox` and `kind` say which credential you hold.
}

// One call, with the two retries: a fresh token on 401, a wait on 429.
async function call(method, path, payload) {
    for (let attempt = 0; ; attempt += 1) {
        const res = await fetch(`${BASE}${path}`, {
            method,
            headers: {
                Authorization: `Bearer ${token}`,
                ...(payload ? { 'Content-Type': 'application/json' } : {}),
            },
            body: payload ? JSON.stringify(payload) : undefined,
        });

        if (res.status === 401 && attempt === 0) {
            await mintToken();
            continue;
        }
        if (res.status === 429) {
            const wait = Number(res.headers.get('Retry-After') ?? 5);
            await new Promise((r) => setTimeout(r, wait * 1000));
            continue;
        }
        if (res.status === 204) return null;

        const body = await res.json();
        if (!res.ok)
            throw new Error(`${method} ${path}: ${res.status} ${body.code} — ${body.detail}`);
        return body;
    }
}

const { sandbox } = await mintToken();
console.log('sandbox workspace:', sandbox);

console.log(await call('GET', '/properties'));

console.log(
    await call('PUT', '/properties/my-prop-9001', {
        name: 'Courtyard flat with the blue door',
        property_type: 'apartment',
        address: {
            line1: '14 Example Street',
            city: 'Alexandria',
            country: 'EG',
            latitude: 31.2001,
            longitude: 29.9187,
        },
        occupancy: { max_guests: 4, bedrooms: 2, beds: 3, bathrooms: 1 },
        base_rate: { value: '1250.00', currency: 'EGP' },
        status: 'listed',
    }),
);

console.log(
    await call('PUT', '/webhooks/endpoint', {
        url: 'https://hooks.example-partner.invalid/maat',
        events: ['booking.confirmed', 'booking.cancelled'],
    }),
);
```

```python Python theme={null}
import os
import time

import requests

BASE = "https://maatstays.wtf/v1"
token = None


def mint_token():
    global token
    res = requests.post(
        f"{BASE}/token",
        headers={"Authorization": f"Bearer {os.environ['MAAT_PARTNER_KEY']}"},
        timeout=30,
    )
    body = res.json()
    if not res.ok:
        raise SystemExit(f"token: {res.status_code} {body.get('code') or body.get('message')}")
    token = body["access_token"]
    return body  # `sandbox` and `kind` say which credential you hold.


# One call, with the two retries: a fresh token on 401, a wait on 429.
def call(method, path, payload=None):
    attempt = 0
    while True:
        res = requests.request(
            method,
            f"{BASE}{path}",
            headers={"Authorization": f"Bearer {token}"},
            json=payload,
            timeout=30,
        )
        if res.status_code == 401 and attempt == 0:
            attempt = 1
            mint_token()
            continue
        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", "5")))
            continue
        if res.status_code == 204:
            return None

        body = res.json()
        if not res.ok:
            raise SystemExit(f"{method} {path}: {res.status_code} {body['code']} — {body['detail']}")
        return body


print("sandbox workspace:", mint_token()["sandbox"])
print(call("GET", "/properties"))
print(
    call(
        "PUT",
        "/properties/my-prop-9001",
        {
            "name": "Courtyard flat with the blue door",
            "property_type": "apartment",
            "address": {
                "line1": "14 Example Street",
                "city": "Alexandria",
                "country": "EG",
                "latitude": 31.2001,
                "longitude": 29.9187,
            },
            "occupancy": {"max_guests": 4, "bedrooms": 2, "beds": 3, "bathrooms": 1},
            "base_rate": {"value": "1250.00", "currency": "EGP"},
            "status": "listed",
        },
    )
)
print(
    call(
        "PUT",
        "/webhooks/endpoint",
        {
            "url": "https://hooks.example-partner.invalid/maat",
            "events": ["booking.confirmed", "booking.cancelled"],
        },
    )
)
```

```go Go theme={null}
package main

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

const base = "https://maatstays.wtf/v1"

var token string

func mintToken() map[string]any {
	req, _ := http.NewRequest(http.MethodPost, base+"/token", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("MAAT_PARTNER_KEY"))

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

	var body map[string]any
	if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
		log.Fatal(err)
	}
	if res.StatusCode >= 400 {
		log.Fatalf("token: %d %v", res.StatusCode, body["code"])
	}
	token, _ = body["access_token"].(string)
	return body // `sandbox` and `kind` say which credential you hold.
}

// One call, with the two retries: a fresh token on 401, a wait on 429.
func call(method, path string, payload any) map[string]any {
	for attempt := 0; ; attempt++ {
		var reader io.Reader
		if payload != nil {
			encoded, _ := json.Marshal(payload)
			reader = bytes.NewReader(encoded)
		}
		req, _ := http.NewRequest(method, base+path, reader)
		req.Header.Set("Authorization", "Bearer "+token)
		if payload != nil {
			req.Header.Set("Content-Type", "application/json")
		}

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

		if res.StatusCode == http.StatusUnauthorized && attempt == 0 {
			res.Body.Close()
			mintToken()
			continue
		}
		if res.StatusCode == http.StatusTooManyRequests {
			wait, _ := strconv.Atoi(res.Header.Get("Retry-After"))
			res.Body.Close()
			if wait == 0 {
				wait = 5
			}
			time.Sleep(time.Duration(wait) * time.Second)
			continue
		}
		if res.StatusCode == http.StatusNoContent {
			res.Body.Close()
			return nil
		}

		var body map[string]any
		err = json.NewDecoder(res.Body).Decode(&body)
		res.Body.Close()
		if err != nil {
			log.Fatal(err)
		}
		if res.StatusCode >= 400 {
			log.Fatalf("%s %s: %d %v — %v", method, path, res.StatusCode, body["code"], body["detail"])
		}
		return body
	}
}

func main() {
	fmt.Println("sandbox workspace:", mintToken()["sandbox"])
	fmt.Println(call(http.MethodGet, "/properties", nil))

	fmt.Println(call(http.MethodPut, "/properties/my-prop-9001", map[string]any{
		"name":          "Courtyard flat with the blue door",
		"property_type": "apartment",
		"address": map[string]any{
			"line1": "14 Example Street", "city": "Alexandria", "country": "EG",
			"latitude": 31.2001, "longitude": 29.9187,
		},
		"occupancy": map[string]any{"max_guests": 4, "bedrooms": 2, "beds": 3, "bathrooms": 1},
		"base_rate": map[string]any{"value": "1250.00", "currency": "EGP"},
		"status":    "listed",
	}))

	fmt.Println(call(http.MethodPut, "/webhooks/endpoint", map[string]any{
		"url":    "https://hooks.example-partner.invalid/maat",
		"events": []string{"booking.confirmed", "booking.cancelled"},
	}))
}
```

## What you should see

* **Step 1** returns `sandbox: true` and a `scope` array. If `sandbox` is `false` you hold a
  production key, and every contract call below will answer `501 not_enabled` after
  authenticating and validating in full — see [Environments](/#environments).
* **Step 2** returns `{"items": [], "has_more": false}` on a fresh sandbox.
* **Step 3** returns `result: "created"` the first time and `result: "updated"` every time
  after, because the URL carries your own identifier. `publishable` and `missing` say whether
  the property can go on sale — a first push usually comes back
  `missing: ["cover_photo", "photos"]`.
* **Step 4** leaves the endpoint `pending_verification` unless your handler echoed the
  challenge. That is the one step you cannot complete without writing a receiver first.

## When it does not work

| You get | It means |
| :- | :- |
| `401 unauthorized` on step 1 | The key is unknown, revoked, or its developer access or workspace is suspended. One answer for all. |
| `403 insufficient_scope` | Your key was not issued with that operation's scope, and `detail` names it. See [Scopes](/guides/scopes). |
| `400 validation_failed` | Read `errors[]` — it names each rejected field and the rule it broke. |
| `501 not_enabled` | You are on a production workspace. Nothing was written; your request was otherwise correct. |

Every one of those is a problem document with a `code` to branch on —
[Errors](/guides/errors).


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