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
| Field | Type | Description |
|---|---|---|
| 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). |
| Authorization | string |
|
| x-api-key | string | Long-lived API key, alternative to a JWT. Pair with |
| Content-Type | string |
|
| x-client-info | string | Optional JSON-encoded client metadata for audit. Common keys: |
| x-client-host | string | Hostname of the calling site. Used by magic-link redirects, OAuth callbacks, and embedded widgets to compute return URLs. |
| x-client-authorization | string | When a server-side proxy forwards a request on behalf of an end user, the proxy uses its own service |
Auth combinations
| Combination | Use case |
|---|---|
orgid only | Public endpoints — product browse, blog read, contact form submit |
orgid + Authorization: Bearer <user-jwt> | Most authenticated requests (browser app, mobile app) |
orgid + x-api-key | Server-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:
| Code | Meaning |
|---|---|
| 400 | Bad payload, missing required fields, invalid orgid |
| 401 | Missing/expired JWT, refresh required |
| 403 | Authenticated but lacking the required role or permission |
| 404 | Record not found (or datatype unknown) |
| 409 | Optimistic-concurrency conflict — refetch and retry |
| 413 | Upload exceeded size cap |
| 429 | Rate-limited by the org's plan; check Retry-After header |
| 500 | Server 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.