Documentation

Raw REST

Headers, request shape, response envelope, and error format for any-language clients.

There is no language requirement for talking to AppEngine. If you can make HTTPS calls and parse JSON, you have an SDK. This page covers the conventions you need to know — headers, body shape, response envelope, errors.

Base URL

https://appengine.appmint.io

Self-hosted deployments use their own host. Every path in the docs is relative to the base URL.

Headers

FieldTypeDescription
orgid*string

Your tenant id. Required on almost every endpoint. Without it the request hits no tenant and returns 400 (or empty results on legacy endpoints).

Authorizationstring

Bearer <jwt>. Required for non-public endpoints. Get a JWT by signing in (/profile/signin for staff, /profile/customer/signin for customers) or refreshing.

x-api-keystring

Long-lived API key, alternative to a JWT. Pair with orgid. Use for server-to-server clients where a session-cookie won't fit.

Content-Typestring

application/json for JSON bodies. multipart/form-data for file uploads (the boundary is set by your HTTP library).

x-client-infostring

Optional JSON-encoded client metadata for audit. Common keys: host, protocol, timezone, userAgent, url.

x-client-hoststring

Hostname of the calling site. Used by magic-link redirects, OAuth callbacks, and embedded widgets to compute return URLs.

x-client-authorizationstring

When a server-side proxy forwards a request on behalf of an end user, the proxy uses its own service Authorization token and puts the user's JWT here so AppEngine can attribute the action.

Auth combinations

CombinationUse case
orgid onlyPublic endpoints — product browse, blog read, contact form submit
orgid + Authorization: Bearer <user-jwt>Most authenticated requests (browser app, mobile app)
orgid + x-api-keyServer-to-server, scheduled jobs, webhooks
orgid + Authorization: Bearer <service-jwt> + x-client-authorization: Bearer <user-jwt>Server-side proxy forwarding a user's request

The platform-level authentication model is documented in Authentication overview.

Request shape

Most reads use GET with query params. Most writes are POST with a JSON body. Some endpoints accept multipart for uploads. The dynamic-query controller is always POST.

Body envelope for repository writes

Repository creates expect a BaseModel\<T\> body:

{
  "datatype": "product",
  "data": {
    "sku": "MUG-XL",
    "title": "Extra-large mug",
    "price": 19.99
  },
  "isNew": true,
  "version": 0
}

For updates, send the modified data plus version:

{
  "data": { "price": 17.99 },
  "version": 7
}

Body envelope for queries

POST /repository/find/{datatype} and the /dynamic-query/* verbs expect:

{
  "query": { "data.status": "active" },
  "options": { "page": 1, "pageSize": 50, "sort": "modifydate", "sortType": "desc" }
}

/dynamic-query/find uses MongoDB-driver field names (filter, limit, skip) — see Dynamic queries.

Response shape

Single record

A BaseModel\<T\>:

{
  "pk": "my-org",
  "sk": "product-MUG-XL",
  "datatype": "product",
  "version": 3,
  "state": "published",
  "createdate": "2026-04-01T12:00:00Z",
  "modifydate": "2026-04-15T09:30:00Z",
  "data": { "sku": "MUG-XL", "title": "Extra-large mug", "price": 17.99 }
}

List

{
  "data": [ /* BaseModel\<T\>[] */ ],
  "total": 142,
  "hasNext": true,
  "page": 1,
  "pageSize": 50
}

total is exact. See Pagination and sorting.

Empty

Most list endpoints return { data: [], total: 0, hasNext: false } rather than 404 when there are no matches. 404 is reserved for "the requested datatype or id genuinely doesn't exist".

Error envelope

Errors use the NestJS exception filter shape:

{
  "statusCode": 400,
  "message": "Validation failed: data.email must be a valid email",
  "error": "Bad Request"
}

message may be a string or an array of validation error strings. error is the canonical short name.

Common codes:

CodeMeaning
400Bad payload, missing required fields, invalid orgid
401Missing/expired JWT, refresh required
403Authenticated but lacking the required role or permission
404Record not found (or datatype unknown)
409Optimistic-concurrency conflict — refetch and retry
413Upload exceeded size cap
429Rate-limited by the org's plan; check Retry-After header
500Server error — these are logged; if persistent, contact support

A complete example: list products, page 2

curl 'https://appengine.appmint.io/repository/find-page-data/product?p=2&ps=20&s=data.price&st=asc' \
  -H "orgid: my-org" \
  -H "x-api-key: ak_live_..."

Equivalent in plain Python:

import requests

url = "https://appengine.appmint.io/repository/find-page-data/product"
r = requests.get(url, params={"p": 2, "ps": 20, "s": "data.price", "st": "asc"},
                 headers={"orgid": "my-org", "x-api-key": "ak_live_..."})
r.raise_for_status()
result = r.json()
for record in result["data"]:
    print(record["data"]["title"], record["data"]["price"])

Equivalent in plain Go:

req, _ := http.NewRequest("GET",
    "https://appengine.appmint.io/repository/find-page-data/product?p=2&ps=20", nil)
req.Header.Set("orgid", "my-org")
req.Header.Set("x-api-key", "ak_live_...")
resp, err := http.DefaultClient.Do(req)
// handle err, decode resp.Body as JSON

A complete example: create a record

curl -X POST https://appengine.appmint.io/repository/create \
  -H "orgid: my-org" \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{
    "datatype": "lead",
    "data": {
      "email": "[email protected]",
      "firstName": "Alice",
      "source": "contact-form",
      "score": 0
    },
    "isNew": true,
    "version": 0
  }'

Response:

{
  "pk": "my-org",
  "sk": "lead-abc-123",
  "datatype": "lead",
  "version": 0,
  "state": "new",
  "data": { "email": "[email protected]", "firstName": "Alice", "source": "contact-form", "score": 0 }
}

A complete example: refresh a JWT

curl -X POST https://appengine.appmint.io/profile/customer/refresh \
  -H "orgid: my-org" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <refresh-jwt>" \
  -d '{}'

Response includes a new token and refreshToken. Replace your stored values.

Idempotency

Some write endpoints (banking transfers, checkout intents, payouts) accept an idempotencyKey field in the body. Generate a UUID and reuse it on retry — the server returns the original result rather than creating a duplicate. Check the per-endpoint reference for which paths support this.

Rate limits

Per-org, per-plan. The default is generous for normal app traffic. When you hit the cap, you get 429 with Retry-After (in seconds). Back off and retry. Server-to-server jobs that need higher throughput should request a custom limit through platform support.

What's missing from REST

Real-time chat and AI streaming aren't pure REST — they use WebSockets (/socket.io) and Server-Sent Events (/ai/stream/:streamId). Cover those in Webhooks and events once your REST integration is solid.