docs
/
Walkthroughs

Authenticate and make your first admin call

Sign in as an operator, read a record, write a record — and the one header that makes reads fail in a way the error message does not explain.

Everything in the tutorials that follow starts here. An operator token is what a deploy script, a migration or an admin tool carries, and it is different from the customer token a shopper gets.

Two kinds of sign-in

EndpointWho it signs inWhat it can do
POST /profile/user/signinAn operator — the people who run the orgRead and write org data, deploy pages, manage records
POST /profile/customer/signinA customer — someone who buys, books or subscribesTheir own cart, orders, bookings, profile

These are separate identity stores. An operator is not a customer with more permissions; the records live in different collections and the tokens resolve differently. Signing in as one and calling an endpoint meant for the other is the most common way to get a confusing 404.

Sign in

curl -s "$APPMINT_HOST/profile/user/signin" \
  -H "orgid: $APPMINT_ORG" \
  -H "Content-Type: application/json" \
  -d '{"email":"'"$APPMINT_EMAIL"'","password":"…"}'
{
  "token": "eyJhbGciOiJIUzI1NiIs…",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs…",
  "orgId": "your-org",
  "rootOrg": "…",
  "sharedOrg": "…",
  "user": {
    "sk": "…",
    "datatype": "user",
    "name": "[email protected]",
    "data": { }
  }
}

Keep token. It goes on every subsequent call as Authorization: Bearer <token>.

This token is an operator credential. It belongs in a deploy script, a server process or an environment variable — never in a browser bundle, a page, or a repository. Anything that reaches the browser has been published.

The header that quietly breaks reads

This one is worth more than its length suggests, because the error it produces points somewhere else entirely.

You will see x-client-authorization used throughout the client-integration docs. It carries a visitor's identity alongside your application's. It is correct there and wrong here.

Send an operator token in both headers and repository reads start failing like this:

{
  "statusCode": 404,
  "error": "customer {\"email\":\"[email protected]\",\"username\":\"[email protected]\"} not found ",
  "path": "/",
  "method": "GET"
}

Nothing in that message mentions headers. It looks like a missing record, and the obvious next move — checking whether the record exists — wastes the next twenty minutes. What actually happened is that x-client-authorization told AppEngine to resolve the caller as a customer, the operator is not one, and the lookup failed before your request was ever considered.

For operator calls, send Authorization: Bearer <token> and nothing else. Add x-client-authorization only when you are relaying a signed-in customer's identity from your server, and then it sits alongside your application credential, never instead of it.

A reusable client

You will make hundreds of these calls. Write the client once:

import json, os, urllib.request, urllib.error

API = os.environ["APPMINT_HOST"]
ORG = os.environ["APPMINT_ORG"]
UA  = "Mozilla/5.0 (compatible; my-deploy-script)"

def req(method, path, body=None, token=None):
    headers = {"orgid": ORG, "User-Agent": UA, "Accept": "application/json"}
    if body is not None:
        headers["Content-Type"] = "application/json"
    if token:
        headers["Authorization"] = "Bearer " + token   # and nothing else
    r = urllib.request.Request(
        API + path, method=method, headers=headers,
        data=json.dumps(body).encode() if body is not None else None)
    try:
        with urllib.request.urlopen(r, timeout=60) as x:
            return x.status, json.loads(x.read() or b"{}")
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode("utf-8", "replace")[:400]

Send a User-Agent. The platform sits behind a CDN that rejects requests with no user agent — Python's urllib sends one that gets blocked. The symptom is a 403 with the body error code: 1010, which looks like a permissions problem and is not one.

Cache the token rather than signing in on every run:

def token():
    cache = f"/tmp/.appmint_token_{ORG}"
    if os.path.exists(cache):
        t = open(cache).read().strip()
        s, _ = req("GET", "/repository/find/site?perPage=1", token=t)
        if s == 200:
            return t                      # still good
    s, d = req("POST", "/profile/user/signin",
               {"email": os.environ["APPMINT_EMAIL"], "password": os.environ["APPMINT_PASSWORD"]})
    assert s in (200, 201), (s, d)
    t = d["token"]
    open(cache, "w").write(t)
    return t

The probe matters. Tokens expire, and a cached one that has gone stale fails on the next call instead of at sign-in, so the error surfaces in the middle of whatever you were doing rather than at the start.

Read something

Records live in collections addressed by datatype. find returns a page of them:

curl -s "$APPMINT_HOST/repository/find/site?perPage=10" \
  -H "orgid: $APPMINT_ORG" -H "Authorization: Bearer $TOKEN"
{
  "total": 1,
  "datatype": "site",
  "pageSize": 10,
  "page": 1,
  "hasNext": false,
  "data": [
    {
      "datatype": "site",
      "sk": "67dbad05b7820b761a55d7e5",
      "name": "your-site",
      "version": 11,
      "data": { "name": "your-site", "title": "…", "domain": "…" }
    }
  ]
}

Two fields decide everything you do next:

  • sk is the record id. It is what you pass to get, update-partial and delete.
  • version is the optimistic-concurrency counter. Writes that supply a stale one are rejected.

find returns a partial projection — enough to list records, not necessarily every field. Before editing a record, fetch it whole with GET /repository/get/<datatype>/<sk>. Writing back a record you only ever saw through find is how fields get silently dropped.

Write something

update-partial changes named fields and leaves the rest alone:

curl -s "$APPMINT_HOST/repository/update-partial/site/$SK" \
  -H "orgid: $APPMINT_ORG" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "sk": "'"$SK"'", "version": 11, "data.title": "New title" }'

A successful write returns 201.

Use dot-paths, not a nested object. {"data": {"title": "New title"}} replaces the entire data field — every other key in it is gone. {"data.title": "New title"} changes one key. This has destroyed catalogue prices and image lists in real migrations. If you are writing a script that touches records you did not create, snapshot them with find first so you can put them back.

Always pass the version you just read. If someone else wrote in between, the call is rejected rather than silently overwriting them.

Creating records

PUT /repository/create — note the verb; POST is for updates.

curl -s -X PUT "$APPMINT_HOST/repository/create" \
  -H "orgid: $APPMINT_ORG" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "datatype": "crm_form",
    "name": "contact-us",
    "data": { "name": "contact-us", "title": "Contact us" }
  }'

The response is the created record, including its new sk.

Checklist before moving on

  • You can sign in and get a token
  • find returns your site record, and you have its sk
  • You wrote a field with update-partial and got 201
  • Your client sends a User-Agent and only Authorization

Next: Set a logo and favicon — a small, complete task that uses uploads, a generator endpoint and a record patch together.