Video chapters
- AppEngine Database & Repository APIs
- Built-in data, custom fields and custom collections
- Use the staff identity and organization
- Read the record and collection
- Extend a built-in customer definition
- Save the added field through the customer API
- Define and save a custom collection
- Give the request code a unique index
- Create once and keep the returned ID
- Open the same API record in Studio
- Filter, sort and paginate results
- Search text, then act on the exact record
- Change one field without replacing its neighbors
- Save a full record and handle an edit conflict
- Inspect history and restore a chosen version
- Delete and restore through Trash
- Separate a saved result from an old screen
- Continue from data to files and business actions
Written guide
Companion source code
Get the course source code on GitHub. The folder includes the supplied examples and setup instructions. Keep credentials in your own environment; use the maintained API reference when adapting recorded request examples.
AppEngine supplies business records, a generic data API and the staff interface for managing them. This course shows when to extend a built-in definition, when to create a custom collection, and how to find, edit and recover its records. The Design Requests example is a small way to see those platform features working; the subject is AppEngine, not running a design business.
The demonstrated requests were executed against a controlled training deployment. Their outcomes are recorded in the request evidence. Public addresses below are deployment examples: substitute the server and organization you actually use. No training response is represented as a request executed on the public production service.
1. What AppEngine already gives you
AppEngine already defines customers, leads, reservations, products, orders and other business records. A datatype is the technical type name, such as customer. A collection definition supplies the datatype's fields and configuration. A record is one saved item. Appmint Studio uses the definition to display and edit records.
Open Appmint Studio, sign in to your organization, and choose Database → All Collections. Use the Built-in filter to inspect supplied definitions. Custom identifies organization-created types. In the recorded interface, choosing a collection opens its management view with Edit Schema and Data actions.

Use these choices deliberately:
| Requirement | AppEngine route to take |
|---|---|
| Keep a customer's extra preference | Extend the built-in customer definition, then use the customer API to save its value. |
| Add an application-specific record | Create a custom collection and use Repository for its records. |
| Book a time, create an account or perform another business action | Use the owning business API. Saving a similarly shaped record does not perform the action. |
Repository is more than a raw database connection: it supplies organization context, queries, pagination, custom-record writes and recovery features. It still does not replace domain rules. Customer and user writes are protected by their dedicated account APIs; reservation operations own availability and booking rules. Discover the operation supported by your deployment in API Reference.
Read the record envelope
Find a known customer using its exact email:
/repository/find/customer{
"query": { "data.email": "YOUR_CUSTOMER_EMAIL" },
"options": { "page": 1, "pageSize": 20 }
}The response contains a list under data. Each item has its own data object containing business fields:
{
"total": 1,
"data": [
{
"sk": "CUSTOMER_ID_FROM_YOUR_RESPONSE",
"datatype": "customer",
"version": 1,
"data": { "email": "YOUR_CUSTOMER_EMAIL", "firstName": "Mira-Lin" }
}
]
}This is a shortened shape illustration. In your response, keep the real sk: it is the record identifier used by subsequent requests. The outer list's data and each record's data are different levels. A title or email can help a person choose a record; it is not interchangeable with its ID.
2. Set up what you need
Use a development organization with an authorized staff account. API Foundations: organization setup explains the organization and API addresses. Its primary practical session is a customer session; this course requires the separate staff sign-in below.
The account needs Database access in Studio and content Read, Create, Update and Delete permissions for these operations. Use Roles: create a role, record responsibilities and assign it through a group. Sign in again after an assignment changes. Existing permissions and record restrictions still apply; a token is not blanket access.
Sign in as staff
/profile/user/signin{ "email": "YOUR_STAFF_EMAIL", "password": "YOUR_STAFF_PASSWORD" }The recorded request returned HTTP201 with user, orgId, token and refreshToken, with no challenge flags for this account. Other accounts can require verification or password change. Inspect and complete those challenges before treating sign-in as a normal session; the Foundations challenge explanation explains the distinction. Use the returned access token after Bearer, not the refresh token, for these requests. Keep credentials out of screenshots, recordings and client code published to visitors.
Set your environment, without placing real secrets in saved course files:
export API='https://appengine.appmint.io'
export ORG='YOUR_ORG_ID'
export STAFF_TOKEN='YOUR_STAFF_ACCESS_TOKEN'Keep one existing customer whose account you control. The account signup path is covered in API Foundations. Find that account as above and retain its real sk as CUSTOMER_ID. The profile update demonstrated later can request a notification; the recorded organization's inactive mail integration was left inactive, and no delivered-email result is claimed.
The same request in six formats
Here is a record read in each supported example format. TYPE is your collection's technical name and RECORD_ID is its returned sk.
TypeScript
type RecordEnvelope = {
sk: string; datatype: string; version: number;
data: { requestCode?: string; [key: string]: unknown };
};
const response = await fetch(`${api}/repository/get/${type}/${recordId}`, {
headers: { orgid: org, Authorization: `Bearer ${staffToken}` }
});
const payload: unknown = await response.json();
if (!response.ok) throw new Error(JSON.stringify(payload));
const record = payload as RecordEnvelope;
console.log(record.sk, record.data);The type declaration describes expected application use; it does not validate a remote response at runtime.
JavaScript
const response = await fetch(`${api}/repository/get/${type}/${recordId}`, {
headers: { orgid: org, Authorization: `Bearer ${staffToken}` }
});
const payload = await response.json();
if (!response.ok) throw new Error(JSON.stringify(payload));
console.log(payload.sk, payload.data);Python
import os
import requests
response = requests.get(
f"{os.environ['API']}/repository/get/{os.environ['TYPE']}/{os.environ['RECORD_ID']}",
headers={"orgid": os.environ["ORG"],
"Authorization": f"Bearer {os.environ['STAFF_TOKEN']}"},
timeout=30,
)
print(response.status_code)
print(response.json())
response.raise_for_status()curl
curl -sS "$API/repository/get/$TYPE/$RECORD_ID" -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN"Postman
Create environment values API, ORG, STAFF_TOKEN, TYPE and RECORD_ID. Use method GET, URL {{API}}/repository/get/{{TYPE}}/{{RECORD_ID}}, header orgid: {{ORG}}, and Bearer Token {{STAFF_TOKEN}}. Keep secrets in private/local values. Send and inspect the status plus JSON response.
JSON
A GET has no JSON request body. Its returned JSON is the record envelope shown above. For writes below, the same supplied JSON body works with each client: set Content-Type: application/json, retain the organization and bearer headers, and use the exact demonstrated method/path.
3. Add your own field to a built-in collection
Read the collection definition:
/repository/collections/customer?enriched=falseInclude the same staff headers. This endpoint matches names by prefix, so select the returned definition whose data.name equals customer, not whichever item is first.
Retain the complete definition and add this property under data.schema.properties:
"preferredStyle": {
"type": "string",
"title": "Preferred style",
"enum": ["modern", "scandinavian", "industrial", "traditional"]
}The snippet is one property to add to the existing JSON object, not a complete request body. Send the complete modified collection to POST /repository/update/COLLECTION_SK. The first built-in definition can have customer as its sk; saving it creates your organization's own collection copy. Later reads return that copy's actual ID. Read again before the next change and use the current returned identifier/version.
AppEngine merges the built-in definition with organization additions. Keep the technical name customer so it remains an override. The recorded result grew from 52 to 53 properties: the original fields remain, and preferredStyle is present.
In Studio, navigate Database → All Collections → Built-in → search Customer → Customer → Edit Schema. Inspect the added field and existing schema; the recorded API save is visible here.

Save the customer's value
Defining a field does not populate it. Use its owning account API:
/user/customer/update{
"sk": "YOUR_CUSTOMER_ID",
"version": 1,
"data": { "preferredStyle": "scandinavian" }
}Replace the sample version with the version from your latest read. The recorded request returned HTTP201; a fresh customer read confirmed scandinavian. The dedicated update merges the supplied fields into the customer. A generic repository/update-partial/customer/ID was refused with HTTP400, directing the caller to the user API.
Supplying the current customer version helps detect stale changes. The customer endpoint's error contract is separate from Repository's full-record conflict response below. Notifications are also separate from storage: the recording proved the saved field, while its attempted profile notice failed locally because no sending integration was active.
4. Create a collection for what isn't built in

Concept illustration using the course's schema and saved records; the following requests and Studio captures show the actual implementation.
A custom collection suits this application's design request. A sales lead would use lead; a booked time would use the reservation feature. Do not create parallel replacements for built-in business concepts merely to obtain different field labels.
The recording uses technical name design_video_1009 and title Design Requests — API course. Use a different valid name when that example already exists in your organization. Carry your selected name into every request. A collection is itself a record with datatype collection:
/repository/create{
"datatype": "collection",
"isNew": true,
"data": {
"name": "design_video_1009",
"title": "Design Requests — API course",
"enableVersioning": true,
"enableIndexing": true,
"schema": {
"type": "object",
"properties": {
"requestCode": {
"type": "string",
"title": "Request code",
"unique": true
},
"title": {
"type": "string",
"title": "Title"
},
"customer": {
"type": "string",
"title": "Customer",
"x-control": "selectMany",
"dataSource": {
"source": "collection",
"collection": "customer",
"value": "sk",
"label": [
"email"
]
}
},
"roomCount": {
"type": "number",
"title": "Rooms"
},
"projectNotes": {
"type": "string",
"title": "Project notes"
},
"status": {
"type": "string",
"title": "Status",
"enum": [
"new",
"reviewing",
"closed"
]
}
},
"required": [
"requestCode",
"title",
"status"
]
}
}
}This is the complete verified collection body. The properties have distinct purposes:
| Property/configuration | Meaning |
|---|---|
data.name | Technical datatype used in routes. |
data.title | Human-readable collection title. |
isNew: true | Explicit new-record create. |
enableVersioning: true | Enables the custom collection's saved history and Trash tracking. |
requestCode.unique | Declares a uniqueness rule; build its index next. |
customer | Stores a customer sk; dataSource describes the Studio picker. |
required | Describes mandatory form fields. This proof does not establish universal validation of every schema rule on every generic API write. |
Save this JSON to collection.json, then send it:
curl -sS -X PUT "$API/repository/create" -H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" -H 'Content-Type: application/json' --data-binary @collection.jsonThe recorded create returned HTTP200 with the saved collection ID. Read the collection back, then open it in Studio. Edit Schema → JSON Schema shows the same definition; Form Preview shows the generated editing shape. The Database module course covers creating the definition in the UI, and its field chapter covers individual controls.

Build and verify the unique index
/repository/collections/fix[
{
"action": "index",
"datatype": "design_video_1009"
}
]The recorded response reports fixed. Repeat index maintenance after relevant schema/index changes. If existing records contain duplicates, resolve them first; declaring a unique field cannot make conflicting stored values unique.
A controlled duplicate create with code DR-101 returned HTTP409, reason: duplicate, field: requestCode. Its title differed from the original, so this checked uniqueness rather than identical-request suppression. The original three records remained intact.
5. Save a design request
Set TYPE to your collection name. If continuing a previous exercise, first find the code and reuse the saved record instead of creating it again:
/repository/find/design_video_1009{
"query": { "data.requestCode": "DR-101" },
"options": { "page": 1, "pageSize": 20 }
}For a new record, use PUT /repository/create. Replace the customer value with the actual customer ID from your organization:
{
"datatype": "design_video_1009",
"isNew": true,
"data": {
"requestCode": "DR-101",
"title": "Living room refresh",
"customer": "6ac82b5a58e293f2e4d6635a",
"roomCount": 2,
"projectNotes": "Lighting and storage",
"status": "new"
}
}The displayed customer ID belongs to the controlled fixture. It is evidence, not a value to copy into another organization. Save the adjusted body to record.json and send it with the same headers. The recorded create returned HTTP200.
Keep the returned identity
The saved record has sk: 6ac988def5d6b011f6e10ddc, datatype design_video_1009, version0 and the supplied fields inside data. Your response will have its own ID. Save the entire JSON response, retain its sk, then request GET /repository/get/YOUR_TYPE/YOUR_RECORD_ID.
Do not treat the readable request code as an internal ID. If a connection drops before the response arrives, the create may already have succeeded. Find the unique code before retrying. Neither a network failure nor an empty response establishes that nothing was saved. The unique-code conflict gives you a reason to inspect the existing item rather than repeatedly creating.
The recording also created DR-102 (one room, Home office layout) and DR-103 (four rooms, Whole home concept). Those three rows make pagination and search visible; they are not extra business setup requirements.
6. See and check the result
Open Database → Design Requests — API course → Data in Studio, then open DR-101. Compare the request code and values with a fresh API read of that same ID. Later captures show the final restored state, so use the matching read rather than expecting the original pre-edit response forever.

GET /repository/link/YOUR_TYPE/YOUR_RECORD_ID returns a deployment-specific Studio link with the record title and datatype. The address follows that deployment's configuration. It does not grant access: the staff member following it still needs the right session and permissions. Do not paste a development-only link into a public production workflow.
Filter, sort and paginate
Send POST /repository/find/YOUR_TYPE with:
{
"query": {
"data.status": "new"
},
"options": {
"page": 1,
"pageSize": 2,
"sort": "data.roomCount",
"sortType": 1
}
}Business fields are stored under data, so the path is data.status. Keep filters inside query; a similarly named top-level field is not this filter. page starts at1; sortType: 1 is ascending and -1 is descending.
Before the edits, all three requests had status new. Page1 returned DR-102 (one room) and DR-101 (two rooms). Page2 returned DR-103 (four rooms). Retain the same filter and sort while changing page.
Read total alongside returned rows and hasNext. In the current implementation hasNext reports a full page, so an exactly full final page can be followed by an empty page. Do not interpret it as an independent count of remaining records. Paging over changing data is not a frozen snapshot.
Search text, with an optional filter
Use POST /repository/search/YOUR_TYPE:
{
"keyword": "Lighting",
"query": {
"data.status": "new"
},
"options": {
"page": 1,
"pageSize": 20
}
}The recorded keyword Lighting returned DR-101 and DR-103 before subsequent edits. keyword finds text; query adds exact conditions. After selecting a result, use its sk for an exact read or edit. Search labels and ordering help discovery; they are not stable record identities.
7. Change it, undo it, delete it
Change one field with a dotted path
/repository/update-partial/design_video_1009/YOUR_RECORD_ID{
"data.status": "reviewing"
}The key is literally data.status. Read the record again after the response. The controlled result changed new to reviewing; other fields remained, version stayed0 and the custom record had no history entries.
Do not substitute {"data":{"status":"reviewing"}}: that sets the whole data object and can discard its other fields. The partial route does not provide the full-save version check and history pipeline. Some audited built-in types have additional tracking, so the observed custom-record behavior is not a promise about every datatype.
Full save and conflict detection
Read the latest complete record, change only the intended value, and send the full envelope to:
/repository/update/YOUR_RECORD_IDThere is no datatype segment in this path. The body retains datatype, sk, version and the other current fields. For the demonstrated save, data.projectNotes became Lighting and storage; revised after review while the already changed status stayed reviewing.
The successful full save returned version1. Resending the previous version0 body returned HTTP400 Inconsistent state mutation. A fresh read showed the newer notes remained. Read the latest record, review the competing change and reapply only the intended edit. Do not copy a stale full body over it or borrow a different endpoint's 409 STALE_RECORD contract.
A JavaScript example preserves the full current object without shell JSON transformations:
const headers = {
orgid: org, Authorization: `Bearer ${staffToken}`,
'Content-Type': 'application/json'
};
const read = await fetch(`${api}/repository/get/${type}/${recordId}`, {headers});
const current = await read.json();
if (!read.ok) throw new Error(JSON.stringify(current));
const updated = {...current, data: {...current.data,
projectNotes: 'Lighting and storage; revised after review'}};
const saved = await fetch(`${api}/repository/update/${recordId}`, {
method: 'POST', headers, body: JSON.stringify(updated)
});
const result = await saved.json();
if (!saved.ok) throw new Error(JSON.stringify(result));
console.log(result);In Python, retain the decoded record, change record["data"]["projectNotes"], and send requests.post(..., json=record, ...). In Postman or curl, start from the latest full response and edit the JSON body/file. Preserve the version you actually read.
Inspect and restore a history version
After the first full save, another full save changed roomCount from2 to3 and produced version2. Request:
/repository/history/design_video_1009/YOUR_RECORD_IDTwo recorded entries contain the saved full-save results. The latest has three rooms; the earlier has two. A history entry's _id identifies the snapshot. Its sk remains the original record ID. Do not pass the original sk as historyId.
/repository/history/restore{
"datatype": "design_video_1009",
"id": "YOUR_RECORD_ID",
"historyId": "THE_CHOSEN_SNAPSHOT_ID"
}The recorded fresh read after restore had two rooms and version3. Restore replaces the saved data and associated saved fields such as workflow, post and style; it is broader than undoing one text field. Inspect the snapshot before choosing it. The original create and the partial edit did not automatically create these custom-record snapshots. Use the retained history shown by your deployment rather than assuming an unlimited version archive.
Delete and recover from Trash
Confirm the exact record first, then call:
/repository/delete/design_video_1009/YOUR_RECORD_IDFor this versioned custom collection, a copy was retained in Trash. The subsequent direct read returned404. To restore, send the original record ID in an array:
/repository/trash-restore["YOUR_RECORD_ID"]A fresh read returned the same ID and saved fields; the matching Trash entry was removed. Restoration was also checked through search. That proves recovery of the record, not restoration of every related task, comment or schedule removed during a deletion. Customer and user accounts have dedicated deletion APIs and should not follow this custom-record procedure.
Choose recovery from the actual error
| Observed condition | Appropriate response |
|---|---|
| Duplicate code:409, reason duplicate, field requestCode | Find that code or intentionally choose a different unique value. |
| Full Repository save:400 Inconsistent state mutation | Read the latest envelope and reconcile the intended edit. |
| Generic customer write refused400 | Use the customer API and its own contract. |
| Deleted exact read404 | Confirm the ID; inspect and restore its eligible Trash entry if intended. |
| Access refused | Inspect actor, organization and required permissions; repeated retries do not grant access. |
8. When a screen shows old data
Read the same record directly through Repository. Check organization, datatype and sk before comparing values. Current generic Repository reads query the provider rather than a stored client list.
If the fresh API value is correct, reload/refetch the client view. If it is still old, inspect the write response and target. Your application should refresh the relevant record or list after a save and clear organization-specific state when switching businesses or signing out. Do not assume API writes automatically refresh every open UI.
Analytics and reporting can have separate caching rules; Analytics and administration APIs covers those surfaces. Clearing an unrelated cache will not correct a write sent to the wrong ID. The video distinguishes this client-state explanation from an actually recorded application defect.
9. Find the rest in the API reference
Open the hosted API reference, or /documentation on the deployment you use. Search for Repository, collections, history or the owning business feature. Check the method, path, caller, required headers and body on each operation. The API Reference course covers discovery, endpoint details, section Markdown and OpenAPI.
The completed controlled sequence proves staff sign-in, collection definition and indexing, three record creates, filtered paging, search, exact reads, partial and full edits, stale-save refusal, history restore, delete/Trash restore, built-in schema extension and customer-field readback. It does not claim external email delivery, universal schema validation, cross-organization isolation or every possible business-module side effect.
Where to go next
- File Storage: upload a file, handle its access and attach the returned reference to a record.
- CRM APIs: customer, lead and reservation actions whose behavior goes beyond generic record storage.
- Database in Appmint Studio: field controls, collection forms, layouts, permissions and public submissions.
- Workflow actions: approvals and automation attached to records.
- Sites and rendering: use business records in a served application or site.
These are linked lessons in the same catalogue; their written-content and video statuses remain separate. Return to the AppEngine developer catalogue to choose the next course.