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
| Endpoint | Who it signs in | What it can do |
|---|---|---|
POST /profile/user/signin | An operator — the people who run the org | Read and write org data, deploy pages, manage records |
POST /profile/customer/signin | A customer — someone who buys, books or subscribes | Their 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 tThe 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:
skis the record id. It is what you pass toget,update-partialanddelete.versionis 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
-
findreturns your site record, and you have itssk - You wrote a field with
update-partialand got201 - Your client sends a
User-Agentand onlyAuthorization
Next: Set a logo and favicon — a small, complete task that uses uploads, a generator endpoint and a record patch together.