# Quickstart

Ten steps: register, wait for the site to be provisioned, take a token, create a block and
a page, read the collection back, change it safely, look at a refusal, and watch the page
appear on the site's own domain. Nothing here is a sandbox — these are the production
hosts, and what you build in the next ten minutes is a real site.

You need `curl` and [`jq`](https://jqlang.org/), and the whole thing is one file:
[quickstart.sh](/quickstart.sh). Every block below is a region of it, so what you read is
what runs.

```sh
curl -O https://dev.moover.app/quickstart.sh && chmod +x quickstart.sh

# with an account you already have:
EMAIL=you@example.com PASSWORD=… ./quickstart.sh
```

## 1. Is that address still free?

Sign-up is the platform's only unauthenticated surface, so every call on it is
bot-checked — including this one. From a terminal that means a CAPTCHA proof in
`recaptcha_token`; from the browser it means the sign-up form at
[moover.app](https://moover.app) does it for you. If you registered that way, skip to
step 4 with the address and password you chose.

```sh title="quickstart.sh"
curl_json -X POST "$SITE_API/site-api/v1/users:check_email_availability" \
  -d "$(jq -n --arg email "$EMAIL" --arg captcha "$RECAPTCHA_TOKEN" \
        '{email: $email, recaptcha_token: $captcha}')" | jq '{available}'
```

It is a question, not a reservation: the answer can go stale, and step 2 is what actually
decides.

## 2. An account, and a site being built

Creating the user creates the account and starts provisioning the first website, so the
response carries three things the user resource itself does not have: the `account_id`, a
one-time `signup_token`, and the name of the `operation` to poll.

```sh title="quickstart.sh"
signup=$(curl_json -X POST "$SITE_API/site-api/v1/users" \
  -d "$(jq -n \
    --arg email "$EMAIL" --arg password "$PASSWORD" --arg website "$WEBSITE_NAME" \
    --arg captcha "$RECAPTCHA_TOKEN" --arg request_id "quickstart-$(date +%s)-$$" \
    '{email: $email, password: $password, website_name: $website,
      default_language: "en", recaptcha_token: $captcha, request_id: $request_id}')")

signup_token=$(jq -r '.signup_token' <<<"$signup")
```

The call answers `201` with the resource, not a receipt — see
[the standard methods](/concepts/methods) — and `request_id` makes it replayable: a
timeout you retry with the same value gives back the first attempt's result instead of
provisioning a second site ([idempotency](/concepts/idempotency)).

## 3. Waiting for the site to be provisioned

The account exists the moment step 2 returns. The site does not: a database, buckets,
routing, a view and the seeded content are still being made, and that work outlives the
request that started it. So the call names an
[Operation](/concepts/operations) and hands the polling to you.

```sh title="quickstart.sh"
state='{"done": false}'
until [ "$(jq -r '.done' <<<"$state")" = true ]; do
  sleep 2
  state=$(curl_json "$SITE_API/site-api/v1/operations/$signup_token")
  jq -r '"  " + (.metadata.step // "?")' <<<"$state" >&2
done

# Done means finished, not succeeded: exactly one of `response` and `error` is there.
jq -e '.error | not' <<<"$state" >/dev/null || { jq '.error' <<<"$state" >&2; exit 1; }
SITE_URL=$(jq -r '.response.website_url' <<<"$state")
```

`metadata.step` is the hop the work is on. There is no percentage, because nothing in the
cascade counts anything — a progress bar here would be an invention.

## 4. A bearer token

Everything a site is made of lives behind `Authorization: Bearer <jwt>`. Two ways to get
one, and this is the ordinary way: the password the account signs in with.

```sh title="quickstart.sh"
token=$(curl_json -X POST "$TENANT_API/tenant-api/v1/users:authenticate" \
  -d "$(jq -n --arg email "$EMAIL" --arg password "$PASSWORD" \
        '{email: $email, password: $password}')" | jq -r '.token')

auth=(-H "Authorization: Bearer $token")
```

Straight after a signup there is a second way, which is what the platform's own dashboard
uses so nobody has to retype the password they just chose: trade the one-time
`signup_token` at `users:exchange_handoff`. See
[authentication](/concepts/authentication).

## 5. Finding the website

Three lists down the hierarchy the [resource names](/concepts/resources) spell out: an
account owns projects, a project owns websites, and everything after this hangs off the
website.

```sh title="quickstart.sh"
account=$(curl_json "${auth[@]}" "$TENANT_API/tenant-api/v1/accounts" | jq -r '.accounts[0].id')
project=$(curl_json "${auth[@]}" \
  "$TENANT_API/tenant-api/v1/accounts/$account/projects" | jq -r '.projects[0].id')
website=$(curl_json "${auth[@]}" \
  "$TENANT_API/tenant-api/v1/projects/$project/websites" | jq -r '.websites[0].id')

site="$TENANT_API/tenant-api/v1/websites/$website"
```

Every list on the platform answers in the same envelope, with the array named exactly
after the collection it came from — one reader handles all three, and there is no
per-endpoint map to keep.

## 6. A block, then a page

A block is a fragment with a handle: pages, themes and rich-text links pull it in by its
`identifier`, and editing it once changes it everywhere it appears. A page is what a URL
resolves to.

```sh title="quickstart.sh"
curl_json "${auth[@]}" -X POST "$site/cms_blocks" \
  -d '{"title": "Store hours", "identifier": "store-hours", "is_active": true,
       "content": "<p>Open Monday to Friday, 9:00–18:00.</p>"}' >/dev/null

url_key="hello-api"
page=$(curl_json "${auth[@]}" -X POST "$site/cms_pages" \
  -d "$(jq -n --arg url_key "$url_key" \
    '{title: "Hello, API", url_key: $url_key, is_active: true,
      content: "<p>This page was created with three lines of curl.</p>"}')")

jq '{name, id, create_time, etag}' <<<"$page"
```

Create answers `201` with the created resource and a `Location` header, so the client that
just wrote something never has to read it back to learn what the server made of it —
including the `etag` step 8 needs.

## 7. Reading the collection back

Four parameters, the same four on every list on the platform.

```sh title="quickstart.sh"
curl_json -G "${auth[@]}" "$site/cms_pages" \
  --data-urlencode 'page_size=5' \
  --data-urlencode 'order_by=create_time desc' \
  --data-urlencode 'filter=is_active = true AND title:"Hello"' |
  jq '{total_size, next_page_token, titles: [.cms_pages[].title]}'
```

A field is filterable or sortable only where the resource says so, and the reference page
for each resource lists which — an unknown field is an
[`INVALID_ARGUMENT`](/concepts/errors) naming it, never a silently ignored URL that
returns a full unfiltered page. [Lists, filters and ordering](/concepts/lists) has the
grammar.

## 8. Changing it without overwriting somebody else

`PATCH` merges: the fields in the body are written, everything else is left alone. The
`etag` a resource carries is its version — send it back in `If-Match` and the write is
refused if anybody has changed the page since you read it.

```sh title="quickstart.sh"
etag=$(jq -r '.etag' <<<"$page")

curl_json "${auth[@]}" -X PATCH "$site/cms_pages/$page_id" \
  -H "If-Match: $etag" \
  -d '{"meta_description": "Created from the quickstart."}' >/dev/null

# That write minted a new etag, so the one above is now stale. Sending it again is what a
# second editor's Save looks like: 409 ABORTED, with the current resource in the body.
stale=$(curl_json "${auth[@]}" -X PATCH "$site/cms_pages/$page_id" \
  -H "If-Match: $etag" -d '{"title": "Hello again"}' || true)

jq '.error | {code, status, message}' <<<"$stale"
```

The second write is what a second editor's Save looks like: `409`, `ABORTED`, and the
current resource in the body so the client can show what it lost to.
[Concurrency](/concepts/concurrency) is the whole rule.

## 9. What a refusal looks like

One shape for every failure on every surface, so a client needs one error reader.

```sh title="quickstart.sh"
refused=$(curl_json -G "${auth[@]}" "$site/cms_pages" \
  --data-urlencode 'order_by=content desc' || true)

jq '.error | {code, status, message, details}' <<<"$refused"
```

`status` is the canonical code to branch on, `message` is for the human reading the log,
and `details` is where a form finds which field to attach the message to.
[Errors](/concepts/errors) has the codes.

## 10. The page, on the site

Publishing is asynchronous. The write above returned as soon as the page was stored; the
rendered site follows a moment later, when the read model behind it has been rebuilt.
Polling for it is the whole of the difference.

```sh title="quickstart.sh"
deadline=$(( $(date +%s) + LIVE_TIMEOUT_SECONDS ))
until curl -sSf -o /dev/null $CURL_OPTS "$SITE_URL/$url_key"; do
  [ "$(date +%s)" -lt "$deadline" ] || { echo "$SITE_URL/$url_key never appeared" >&2; exit 1; }
  sleep 3
done

echo "live: $SITE_URL/$url_key"
```

## Where to go from here

- The [Tenant API](/tenant-api) reference — every resource a site is made of, with the
  fields, filters and examples generated from the service itself.
- [Concepts](/concepts/overview) — the rules every one of those endpoints obeys, so you
  can predict an endpoint you have not read yet.
- [For agents](/agents) — what the site you just made offers a program that reads it.
