Video chapters
- Welcome: what you'll learn
- Why AppEngine
- What you can build
- How it fits together
- What's built in
- A built-in action, step by step
- The rule: built-in first
- Find an action in the live API reference
- Get started: signup, organization and live URLs
- Who is calling
- Get a token and make your first authenticated request
- More built in: files, chat, automation and AI
- Hosting
- Where to go next
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.
1. Why AppEngine
Say you are building an app for a fitness studio. Members need to book classes, pay for a membership, ask questions in a chat window and open a support ticket when something goes wrong. Behind each of those is a backend: checking whether a class still has spaces, saving the booking, emailing a confirmation with a calendar invite, sending reminders the day before, pricing a cart, confirming a card payment, routing a ticket to the right person.
You could build all of that yourself. Or you could call AppEngine, which already does it. AppEngine is the backend behind Appmint and BusinessMade, and it is open to your own website, mobile app or server. You design the experience; AppEngine runs the business underneath it.
This course is the entry point to the AppEngine series. By the end you will know what AppEngine already does for you, when to use it and when to build your own, how to find the exact endpoint you need, and how to make your first authenticated request.
2. What you can build
| You want to build | What AppEngine already provides |
|---|---|
| A booking site or app | Booking types, open times, booking, rescheduling, cancelling, confirmations and reminders |
| An online shop or shopping app | Products, prices, a saved cart, discounts, checkout with a payment gateway, orders and refunds |
| A customer portal | Customer sign-in, profile, their bookings, orders, invoices, tickets and files |
| A support experience | Live chat with history, hand-off between AI and staff, tickets with replies and a support workflow |
| A sales tool | Leads, pipelines, qualification, assignment, follow-ups and conversion to customers |
| A staff app | Staff sign-in, schedules, time clock and self-service |
| An app with its own data | Your own collections, with the same sign-in, permissions, files and search as everything else |
| An integration or AI tool | Scoped API keys, workflows, scheduled jobs, AI employees and an MCP server |
You can mix these freely. The studio app in this course uses bookings, customers, tickets and one collection of its own.
3. How your app, AppEngine and the business fit together
Three parties share one set of records:
- Your app — the website, mobile app or service you write. It shows screens and calls AppEngine over HTTPS.
- AppEngine — the backend. It signs people in, stores records, runs the business rules and talks to outside providers (email, SMS, payment gateways, file storage).
- The business — the studio's staff, working in Appmint Studio Manager or BusinessMade. When a member books through your app, the front desk sees the booking in their own screen, because it is the same record.
Everything belongs to an organization: one business, with its own records, staff, customers and settings. Its Organization ID is a short identifier you send on every request in the orgid header, so AppEngine knows which business you mean.
4. What is built in
"Built in" means AppEngine already has it, ready to use, with no setup in code. There are two kinds.
Built-in data types
A record is one saved thing — one booking, one customer, one ticket. A data type (or datatype) is the kind of record it is. AppEngine ships about 270 built-in data types, each with its fields already defined. A few you will meet often:
| Data type | What it holds | Some of its real fields |
|---|---|---|
customer | A person who buys or books | firstName, lastName, email, phone, company, timezone |
reservation_definition | A bookable thing: a class, an appointment type | name, services (each with name, duration, price), workDays, officeHours, hosts, spots |
reservation | One booking | reservationDefinitionId, service, startTime, endTime, timezone, customer, hosts, status |
lead | A potential customer in a sales pipeline | firstName, email, stageId, status, score, assignedTo |
ticket | A support request | title, description, priority, status, reportedByEmail |
sf_cart / sf_order | A shopping cart and a placed order | cart lines with sku, quantity, unitPrice; order totals and status |
file | An uploaded file's details | path, size, type, visibility |
chat / message | A conversation and its messages | participants, content, timestamps |
Every record arrives in the same envelope. The record's ID is sk. Its kind is datatype. The business fields live inside data. AppEngine adds the rest: version, author, owner, createdate, modifydate.
{
"sk": "66f1c2a9e4b0c81a2d3f4a51",
"datatype": "reservation",
"name": "K7Q2M9XA",
"data": { "service": "Morning Yoga", "startTime": "2026-10-20T13:00:00.000Z", "status": "new" },
"version": 1,
"author": "[email protected]"
}Built-in actions
A built-in action is an endpoint that runs a piece of business for you. You send one request; AppEngine does several things that would otherwise be your code.
| You call | AppEngine does for you |
|---|---|
POST crm/reservations/slots | Works out the open times for a booking type, service and day, in the business's timezone, leaving out full and blocked times and times the chosen staff member is busy |
POST crm/reservations/book | Locks the slot, checks it is still free, assigns a host, finds or creates the customer, saves the booking with a reservation number, emails the confirmation with a calendar invite, texts the customer when a phone number is given, copies the owner and schedules reminders |
POST crm/reservations/reschedule/:id | Re-checks the new time (answers 409 slot_unavailable if it is taken), moves the booking, re-schedules reminders and tells the customer |
POST crm/reservations/cancel/:id | Marks the booking cancelled with who and why, removes its reminders and sends the notice |
POST storefront/cart/update/:cartid | Saves the member's cart on the server so it survives devices and sessions |
POST storefront/checkout-cart | Prices the cart on the server, applies discounts, confirms the payment with the gateway, then writes the order |
POST crm/leads/qualify/:id | Moves the lead to qualified, records the activity on its history and notifies the lead's owner |
POST crm/tickets/create | Saves the ticket, emails the person who raised it and puts it on the support workflow |
POST crm/tickets/reply/:ticketId | Saves the reply on the ticket's thread, sends it by email (with any attached files) and tracks the first-response time for staff replies |
POST client-data/files/upload | Stores a signed-in customer's file in their own space |
Socket.IO namespace /chat | Live chat between a visitor and staff or an AI assistant, with the history saved (GET chat/history/:chatId) |
See one work: book a class
Before running requests, complete Getting started below so that your organization and API address are defined. The hosted contract may differ from the source example; check the deployment note in section 7.
The short examples in sections 4–7 use API for your AppEngine origin, ORG for your Organization ID, and STAFF_TOKEN for a completed staff sign-in's access token. These are placeholders, not predefined shell variables. The complete downloadable example in section 8 prepares its own inputs, signs in and reads your profile; its variables and token do not remain in your terminal after it exits. The booking IDs, dates and sample responses below explain the contract: substitute records from your own configured reservation definition before attempting a booking. Follow the linked specialist course for that setup.
The studio has a booking type called Classes with a service Morning Yoga. Your app first asks for the open times on a day. This route is public — a visitor does not need to sign in — so it needs only the organization:
curl -sS -X POST "$API/crm/reservations/slots" \
-H "orgid: $ORG" -H 'Content-Type: application/json' \
-d '{"reservationDefinitionId":"66f1b8d2e4b0c81a2d3f4a10","serviceName":"Morning Yoga","serviceDate":"2026-10-20"}'{
"reservationDefinition": { "sk": "66f1b8d2e4b0c81a2d3f4a10", "data": { "name": "Classes" } },
"service": { "name": "Morning Yoga", "duration": 60, "price": 15 },
"officeDays": [],
"slots": [
{ "startTime": "2026-10-20T13:00:00.000Z", "endTime": "2026-10-20T14:00:00.000Z", "spotsAvailable": 8, "businessTimezone": "America/Chicago" },
{ "startTime": "2026-10-20T15:00:00.000Z", "endTime": "2026-10-20T16:00:00.000Z", "spotsAvailable": 3, "businessTimezone": "America/Chicago" }
]
}(The responses here are shortened.) Times are exact instants; businessTimezone lets your app show them on the studio's clock. If the studio is closed that day, you get an empty slots list with closed: true and a message you can show as it is.
The member picks 8:00 and enters their name and email. Your app books it:
curl -sS -X POST "$API/crm/reservations/book" \
-H "orgid: $ORG" -H 'Content-Type: application/json' \
-d '{
"reservationDefinitionId": "66f1b8d2e4b0c81a2d3f4a10",
"service": "Morning Yoga",
"startTime": "2026-10-20T13:00:00.000Z",
"customer": { "name": "Maya Ortiz", "email": "[email protected]" }
}'{
"sk": "66f1c2a9e4b0c81a2d3f4a51",
"datatype": "reservation",
"name": "K7Q2M9XA",
"data": {
"name": "K7Q2M9XA",
"status": "new",
"service": "Morning Yoga",
"startTime": "2026-10-20T13:00:00.000Z",
"endTime": "2026-10-20T14:00:00.000Z",
"timezone": "America/Chicago",
"reservationDefinitionId": "66f1b8d2e4b0c81a2d3f4a10",
"customer": { "id": "66f1c2a8e4b0c81a2d3f4a4e", "name": "Maya Ortiz", "email": "[email protected]" }
},
"notices": [
{ "audience": "customer", "channel": "email", "to": ["[email protected]"], "templates": ["reservation-confirmation"], "status": "queued" },
{ "audience": "staff", "channel": "email", "to": ["[email protected]"], "templates": ["reservation-confirmation"], "status": "queued" }
]
}Look at what came back. endTime was filled in from the service's 60-minute duration. K7Q2M9XA is the reservation number Maya will quote. customer.id points to a customer record AppEngine found or created from her email. notices lists the messages it sent on your behalf. Reminders are scheduled in the background. If someone else took the last space a second earlier, the request fails instead of overbooking the class. And the front desk already sees the booking in their reservations screen.
Your app made two calls. Everything else was AppEngine.
5. The rule: built-in action, then built-in data type, then your own
When you need something, check in this order.
- Use the built-in action when one exists — booking, rescheduling, checkout, payment, qualifying a lead, raising a ticket, checking in. If you write the record yourself instead, you get the record and none of the work in the right-hand column above: no availability check, no confirmation, no reminders, no activity history.
- Use the built-in data type for the thing itself, and add your own fields to it. If the studio wants to store each member's emergency contact, add an
emergencyContactfield to the built-incustomercollection in Studio Manager → Database. The organization's copy of a built-in collection is merged over the built-in one, so you keep every built-in field and action and gain yours. Do not create a separatememberscollection: nothing built in would know about it. - Create your own collection only for something AppEngine does not have. That is next.
6. Your own collections with Repository
A collection is the definition of a data type: its name and the fields its records have. The built-in ones come with AppEngine; you can add your own.
The studio inspects its equipment every week — bikes, rowers, mats — and wants a record of each check. AppEngine has no "equipment inspection" type, so this is a case for rule 3. In Studio Manager → Database create a collection named equipment_inspection with fields equipment, condition, inspectedOn and notes. From then on, records of that type can be created in Studio Manager and from your app.
Your app reads and writes them through Repository, AppEngine's general set of routes that work for any data type the caller is allowed to use:
| Route | What it does |
|---|---|
PUT repository/create (or POST) | Creates a record from { datatype, data } and returns it with its new sk |
GET repository/get/:datatype/:id | Reads one record |
POST repository/find/:datatype | Finds records; body { query, options } where query filters on fields under data. and options sets page, pageSize, sort |
POST repository/update-partial/:datatype/:id | Changes only the fields you name, as flat dotted keys such as {"data.condition":"worn"} |
DELETE repository/delete/:datatype/:id | Deletes a record |
Create one inspection as a signed-in staff member (you will get the token in section 8):
curl -sS -X PUT "$API/repository/create" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" -H 'Content-Type: application/json' \
-d '{"datatype":"equipment_inspection","data":{"equipment":"Bike 4","condition":"good","inspectedOn":"2026-10-08"}}'Then find every worn item:
curl -sS -X POST "$API/repository/find/equipment_inspection" \
-H "orgid: $ORG" -H "Authorization: Bearer $STAFF_TOKEN" -H 'Content-Type: application/json' \
-d '{"query":{"data.condition":"worn"},"options":{"page":1,"pageSize":20}}'A find returns { "data": [ ...records ], "total": 3, ... } — the records are in data, each in the envelope you saw in section 4.
Repository works on built-in types too, which is handy for reading. For changing business records, go back to rule 1: a booking moved with update-partial is a booking whose customer was never told. The Collections and Records course covers schemas, queries, paging and history in full.
7. Finding the endpoint you need
You now know what to look for: a built-in action first, a data type second. To find the exact route, open the API reference that AppEngine serves itself at /documentation on your AppEngine address — for the hosted platform, https://appengine.appmint.io/documentation.
Search for the thing you want, in plain words. Type reservations. The recorded hosted reference exposes POST /crm/reservations/slots; its body, access requirements and response are shown in the video. Deployment check, 8 October 2026: /crm/reservations/book from the current source example was not listed in the hosted reference during recording. Do not copy that path into the hosted system without checking its available booking operation and contract. The source examples above describe the newer documented workflow, not a completed hosted booking. Routes in that source include: POST /crm/reservations/slots, POST /crm/reservations/book, POST /crm/reservations/reschedule/{id}, and others such as POST /crm/reservations/availability (open and full times across up to 31 days). Open one to see its headers, body, who may call it and what it returns. Search tickets to find the support routes, cart or checkout for the shop.
The same search works from the command line, which is useful in scripts and for AI tools:
curl -sS --get "$API/documentation/search" --data-urlencode 'q=reschedule reservation' --data-urlencode 'limit=5'The complete specification downloads from /openapi.json. The API Reference course shows every way to search and read it.
8. Getting started: account, address, token and first request
You can read the documentation before opening an account. These are the actual hosted entry points:
| Purpose | Open this address |
|---|---|
| Create an account | Appmint signup — or choose Start free at appmint.io |
| Sign in to the management interface | studio.appmint.io |
| Read the platform guides | docs.appmint.io → AppEngine API |
| Find the production API origin | AppEngine overview |
| Search the deployed operation contracts | Live API reference |
| Follow sign-in and token examples | Developer Quickstart |
The management website is not the API endpoint. Your application calls https://appengine.appmint.io, not studio.appmint.io. Use the separately assigned API origin if your organization has its own deployment.
Create or reuse your organization
If you already have an Appmint organization, use that account; you do not need a second signup merely to read this course. For a new account, open the signup link above. Choose email, Google or GitHub and complete that route. Keep the Organization ID from the welcome screen's Your sign-in details section. Use your own ID in orgid; the recorded welcome example and the training demonstration use example organizations. A business display name is not automatically its organization identifier. Create the account covers the complete account-creation workflow.
Use a separate development organization for exercises. An organization owns the records being read or changed. Signing in to the management website does not automatically supply a token to a separate terminal or server script.
The same JSON and HTTP exchange in every client
Use an existing staff account, its password and its exact organization ID. First create the account through Appmint signup if needed; these examples sign in, then read a profile. An existing Google/GitHub account may need its supported password setup or another documented authentication route before using this password example. Use your own development organization. The video uses a separate training deployment; the examples default to the hosted API.
Request 1 — sign in. Send JSON to the API host, with the organization header:
https://appengine.appmint.io/profile/signinResponse 1 — find token. This is an abbreviated JSON shape with placeholder values, not a credential:
{
"token": "ACCESS_TOKEN_RETURNED_BY_SIGNIN",
"refreshToken": "REFRESH_TOKEN_IF_RETURNED",
"orgId": "YOUR_ORGANIZATION_ID",
"user": {"datatype": "user", "data": {"email": "YOUR_STAFF_EMAIL"}}
}The access token is the top-level token, not refreshToken. A successful status alone does not establish a session. If requiresTwoFactor or requiresPasswordChange is returned without a usable token, stop and complete that flow. See User authentication and Two-factor and device security. Do not send a challenge token as the bearer token for this profile call.
Request 2 — use that token. URL-encode the same staff email in the path and keep the same orgid:
https://appengine.appmint.io/profile/user/profile/YOUR_URL_ENCODED_STAFF_EMAILResponse 2 — verify the returned person. The profile is a record with a top-level datatype and nested data:
{
"datatype": "user",
"data": {
"email": "YOUR_STAFF_EMAIL",
"firstName": "YOUR_FIRST_NAME",
"lastName": "YOUR_LAST_NAME"
}
}Expect HTTP 200, datatype: "user", and data.email matching the account you signed in with. Read names from data.firstName and data.lastName. If sign-in fails, check the host, organization and credentials. If the protected request fails, check that the access token is current and belongs to that same organization. These requests do not create an account or change a business record.
JavaScript — native fetch
Requires Node.js 20+; save as first-profile.mjs and run node first-profile.mjs. Set ORG_ID, STAFF_EMAIL, STAFF_PASSWORD privately in the process environment. APPENGINE_ENDPOINT is optional. Keep this on your server or private terminal; do not ship the password or privileged token in browser code.
const base = (process.env.APPENGINE_ENDPOINT || "https://appengine.appmint.io").replace(/\/$/, "");
const orgid = process.env.ORG_ID;
const email = process.env.STAFF_EMAIL;
const password = process.env.STAFF_PASSWORD;
if (!orgid || !email || !password) throw new Error("Set ORG_ID, STAFF_EMAIL and STAFF_PASSWORD");
async function json(response) {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
const session = await json(await fetch(`${base}/profile/signin`, {
method: "POST",
headers: { "Content-Type": "application/json", orgid },
body: JSON.stringify({ email, password })
}));
if (typeof session.token !== "string" || !session.token) {
throw new Error("No access token: complete any MFA or password-change challenge first");
}
const profile = await json(await fetch(`${base}/profile/user/profile/${encodeURIComponent(email)}`, {
headers: { orgid, Authorization: `Bearer ${session.token}` }
}));
if (profile.datatype !== "user" || profile.data?.email !== email) {
throw new Error("Returned profile does not match the signed-in staff account");
}
console.log(profile.datatype, profile.data.firstName, profile.data.lastName);TypeScript — the same HTTP requests, with response types
Requires Node.js 20+, TypeScript and Node type definitions. Save as first-profile.mts; compile with tsc first-profile.mts --target ES2022 --module NodeNext --moduleResolution NodeNext, then run node first-profile.mjs. Use the same private environment variables as JavaScript. Types describe the expected JSON; the explicit token and identity checks still run.
type Session = { token?: string; requiresTwoFactor?: boolean; requiresPasswordChange?: boolean };
type Profile = { datatype: string; data: { email: string; firstName?: string; lastName?: string } };
const base = (process.env.APPENGINE_ENDPOINT || "https://appengine.appmint.io").replace(/\/$/, "");
const orgid = process.env.ORG_ID;
const email = process.env.STAFF_EMAIL;
const password = process.env.STAFF_PASSWORD;
if (!orgid || !email || !password) throw new Error("Set ORG_ID, STAFF_EMAIL and STAFF_PASSWORD");
async function json<T>(response: Response): Promise<T> {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
const session = await json<Session>(await fetch(`${base}/profile/signin`, {
method: "POST",
headers: { "Content-Type": "application/json", orgid },
body: JSON.stringify({ email, password })
}));
if (typeof session.token !== "string" || !session.token) {
throw new Error("No access token: complete any MFA or password-change challenge first");
}
const profile = await json<Profile>(await fetch(`${base}/profile/user/profile/${encodeURIComponent(email)}`, {
headers: { orgid, Authorization: `Bearer ${session.token}` }
}));
if (profile.datatype !== "user" || profile.data?.email !== email) {
throw new Error("Returned profile does not match the signed-in staff account");
}
console.log(profile.datatype, profile.data.firstName, profile.data.lastName);Python — standard library
Requires Python 3, with no extra packages. Save as first_profile.py and run python3 first_profile.py. This version prompts for the organization, email and hidden password.
import getpass
import json
import os
from urllib.parse import quote
from urllib.request import Request, urlopen
base = os.environ.get("APPENGINE_ENDPOINT", "https://appengine.appmint.io").rstrip("/")
orgid = input("Organization ID: ").strip()
email = input("Staff email: ").strip()
password = getpass.getpass("Password: ")
if not all((orgid, email, password)):
raise SystemExit("Organization ID, staff email and password are required")
def request(path, headers, body=None):
data = None if body is None else json.dumps(body).encode("utf-8")
with urlopen(Request(base + path, data=data, headers=headers), timeout=30) as response:
return json.load(response) # HTTP failures raise HTTPError
session = request("/profile/signin", {"Content-Type": "application/json", "orgid": orgid},
{"email": email, "password": password})
token = session.get("token")
if not isinstance(token, str) or not token:
raise SystemExit("No access token: complete any MFA or password-change challenge first")
profile = request("/profile/user/profile/" + quote(email, safe=""),
{"orgid": orgid, "Authorization": "Bearer " + token})
if profile.get("datatype") != "user" or profile.get("data", {}).get("email") != email:
raise SystemExit("Returned profile does not match the signed-in staff account")
print(profile["datatype"], profile["data"].get("firstName"), profile["data"].get("lastName"))curl — inspect the JSON directly
Requires curl and a private working directory. Set umask 077 in your shell first. Create signin.json containing the Request 1 JSON above, replacing its placeholders with your own values. Set APPENGINE_ENDPOINT=https://appengine.appmint.io and ORG_ID to your organization ID. Keep the JSON files private and out of version control.
curl --fail-with-body -sS -X POST "$APPENGINE_ENDPOINT/profile/signin" \
-H 'Content-Type: application/json' -H "orgid: $ORG_ID" \
--data-binary @signin.json -o signin-response.jsonInspect signin-response.json privately. Continue only if it contains a nonempty access token, after completing any challenge. Create headers.txt with these two lines, replacing the placeholders; use the actual access token from that response:
Authorization: Bearer ACCESS_TOKEN_RETURNED_BY_SIGNIN
orgid: YOUR_ORGANIZATION_IDSet EMAIL_PATH to your URL-encoded staff email (for example, your.name%40example.com), then send the protected read:
curl --fail-with-body -sS \
"$APPENGINE_ENDPOINT/profile/user/profile/$EMAIL_PATH" \
-H @headers.txt -o profile.jsonOpen profile.json and check datatype is user, data.email is your account, and the nested name fields are correct. Remove signin.json, signin-response.json, headers.txt and profile.json after the exercise. Neither request requires jq. --fail-with-body requires curl 7.76+.
Postman — two saved requests
Create private environment values baseUrl (https://appengine.appmint.io), orgid, email, and password. Keep credentials and returned tokens out of shared/synced values. Use the same environment for both requests.
- Sign in:
POST {{baseUrl}}/profile/signin. Set headerorgid: {{orgid}}, Authorization No Auth, and Body → raw → JSON:
{"email":"{{email}}","password":"{{password}}"}Confirm Content-Type: application/json. Inspect the response body for token and challenge flags. In the request's post-response script, save only a completed session:
pm.environment.unset("accessToken");
const session = pm.response.json();
if (pm.response.code < 200 || pm.response.code >= 300 ||
typeof session.token !== "string" || !session.token) {
throw new Error("No session: check the response and complete any challenge first");
}
pm.environment.set("accessToken", session.token);
pm.environment.set("emailPath", encodeURIComponent(pm.environment.get("email")));- Own profile:
GET {{baseUrl}}/profile/user/profile/{{emailPath}}. Set headerorgid: {{orgid}}. In Authorization select Bearer Token and enter{{accessToken}}(Postman adds theBearerprefix). Send this request only after the first returns a usable token. In its post-response script:
pm.test("The signed-in staff profile was returned", () => {
pm.response.to.have.status(200);
const profile = pm.response.json();
pm.expect(profile.datatype).to.eql("user");
pm.expect(profile.data.email).to.eql(pm.environment.get("email"));
});Read data.firstName and data.lastName in the JSON response. Clear private password and token values when finished. A failed sign-in clears the old token; do not reuse a token from a different organization.
Optional Bash helper and downloadable examples
Download these JavaScript, TypeScript, Python, curl, Postman and JSON examples. The existing Bash helper remains optional. It requires Bash, curl and jq and can be run with bash first-authenticated-request.sh; it performs the same sign-in, token check and own-profile request with private prompts and temporary-file cleanup. You do not need that helper to use any client shown above.
Choose the identity for subsequent work
| Who is acting | Appropriate next lesson |
|---|---|
| Staff member | The staff token obtained above, limited by that user's permissions |
| Customer | POST /profile/customer/signin; customer authentication |
| Your backend application | App exchange through POST /profile/app/key, with its secrets retained server-side; Client Integration |
| Integration without an interactive sign-in | A scoped API key; API keys |
Some client flows use both an application token in Authorization and a person's token in x-client-authorization. They are different identities; do not use the application identity as the buyer. Follow the linked authentication lesson for that complete setup. Public operations have their own requirements: read the exact deployed operation's access and organization sections rather than assuming that every business endpoint behaves like the profile request.
9. The rest of the platform
The studio app will grow. These parts of AppEngine are ready when it does:
- Files. Upload with
POST repository/file/upload(staff) orPOST client-data/files/upload(a signed-in customer), as multipart form data with the file in thefilefield. Choose private or public, and hand out time-limited links for private files. → File Storage - Realtime. The
/chatsocket carries live support chat; other namespaces carry workspace updates, community messages and live calls. → Realtime - Automation, workflows and AI employees. A workflow moves a record through stages (a new ticket lands on the support workflow automatically). Automations start actions on events or schedules. AI employees are staff members that are AI: they take work, use the same built-in actions and follow the same permissions. → Workflow Actions, Scheduled Jobs, AI Employees, MCP
- Hosting. AppEngine can also host your app: create an environment, build it, read its logs and attach a domain through the
dev-envroutes. → Platform as a Service
10. Where to go next
Pick the course for what you are building.
| You are building | Course |
|---|---|
| Your first full client, web or mobile | Custom Web and Mobile Client |
| Bookings, leads, customers and tickets | CRM APIs |
| A shop: catalog, cart, checkout and orders | Headless Storefront, then Build Headless Commerce |
| Payments and refunds | Finance APIs |
| A customer portal | Customer Portal |
| Live chat with AI and staff | Realtime Chat and AI Support |
| Your own data types | Collections and Records |
| Email, SMS, templates and broadcasts | Messaging APIs |
| Phone calls and IVR | Phone APIs |
| Campaigns and social publishing | Marketing APIs |
| Locations, POS, queues and work orders | Business Operations APIs |
| Staff records and self-service | Workforce APIs |
| Books and payroll | Accounting and Payroll APIs |
| Content, community and courses | Content and Community APIs |
| Events, tickets and devices | Events and Devices |
| Delivery and rentals | Logistics and Rental APIs |
| Dashboards and reports | Analytics and Administration APIs |
| Sign-in for another app (OIDC/SAML) | Identity Provider |
| AppEngine as your whole backend | Backend as a Service |
| Connecting outside systems | Provider Integrations, Custom API Gateway |
Whatever you pick, the habit from this course carries over: find the built-in action first, use the built-in data type second, and build your own only for what is truly yours.