API reference
Every published app serves its own records API at its own address:
https://<app>.phiwebs.app/_host/recordsThis is the same API the app’s pages use. Your own code can call it too: a script that imports records, a service that reads new orders, a report that counts bookings. The app’s access rules apply to every call, exactly as they do for a visitor.
There is no PhiWebs API for building, changing or publishing an app. That happens in the studio.
Before you start
- The app must be published. Call the published app’s address. The preview keeps its own, separate records and is not meant for outside callers. See Preview, versions and rollback.
- Use the model’s name exactly as the app declares it in
app/definitions/entities.json, for exampleBooking. A name the app does not declare answers404 UNKNOWN_ENTITY_TYPE. - Closed by default. A data model that no access rule names cannot be read or written by anyone through this API. To open a model, ask the agent in the studio for a rule, for example “anyone may read Product”.
Who you are to the app
The API decides what a call may do from the access rules and from who is calling.
| Caller | How | What it may do |
|---|---|---|
| Anonymous | No credential | What the rules allow public, for example reading a menu or creating a contact request. |
| A visitor of a PhiWebs-sign-in app | The app’s own session, sent by the app’s pages | What the visitor’s roles allow. |
| A person or service of a company-sign-in app | Authorization: Bearer <token> from your company’s identity provider | What their roles allow. |
From your own server, the practical options are:
- Public rules. Give the model a rule for
publicwith just the verbs you need, for examplecreatefor a form that other systems submit to. - Company sign-in. If the app signs visitors in with your company’s account (for example Microsoft Entra ID), send a token your identity provider issued for the app. The token must be signed by the issuer the app names, and its audience must be the app’s audience or client ID. The caller then needs a place on the app’s people list, or sign-up must be open. Roles come from the people list, plus the roles claim when its values match roles the app declares.
An app that uses PhiWebs sign-in gives sessions only to people who sign in in a browser. A server cannot get one.
From a browser on another site: the API sends no cross-origin (CORS) headers, so a page on a different site cannot read its answers. Call it from your server, or from the app’s own pages.
Routes
All paths are on the app’s address, under /_host/records. <Model> is the
model name; <id> is a record id.
| Method | Path | What it does | Rule verb |
|---|---|---|---|
GET | /_host/records/<Model> | List records, with filter, sort and paging. | list |
POST | /_host/records/<Model> | Create a record. Answers 201. | create |
GET | /_host/records/<Model>/<id> | Read one record. | view |
PATCH | /_host/records/<Model>/<id> | Change the fields you send. | update |
DELETE | /_host/records/<Model>/<id> | Delete one record. | delete |
GET | /_host/records/<Model>/count | Count records. | count (plain count) or list (with a filter) |
GET | /_host/records/<Model>/aggregate | Sum, average or count of a field. | list |
In a rule, read means list and view, and write means create,
update and delete. A rule with own limits the caller to the records
they created.
Listing
GET /_host/records/Booking?filter=…&sort=date:desc&limit=20&offset=0| Parameter | Meaning |
|---|---|
filter | A JSON filter object, URL-encoded (below). |
sort | field:asc or field:desc. Separate several with commas. |
limit | Records per page, 1 to 500. Default 100. |
offset | Records to skip. Default 0. |
Without sort, the most recently changed records come first.
{
"data": [{ "id": "…", "entityType": "Booking", "name": "Ada", "date": "2026-11-20", "createdAt": "…", "updatedAt": "…" }],
"meta": { "total": 42, "scope": "project", "timestamp": "…" }
}meta.total counts every match, not only this page. meta.scope is owner
when the caller sees only their own records, and project when they see all.
Filters
A filter compares one field, and can combine others with and and or:
{ "field": "status", "operator": "eq", "value": "confirmed",
"and": [{ "field": "guests", "operator": "gte", "value": 2 }] }Operators: eq, neq, gt, gte, lt, lte, in (the value is a
list), contains and startsWith. contains and startsWith ignore case.
Text that looks like a number is compared as a number. Use a dot to reach
into nested fields (address.city).
Count and aggregate
GET /_host/records/Booking/count
GET /_host/records/Order/aggregate?fn=sum&field=total&filter=…fn is count, sum or avg. sum and avg need a field. The answer
is:
{ "data": { "fn": "sum", "field": "total", "value": 1280.5, "count": 37 }, "meta": { "scope": "project", "timestamp": "…" } }Creating and changing
Send the fields as a JSON object, either as they are or wrapped in
{ "data": { … } }. The body may be up to 1 MB.
curl -X POST "https://<app>.phiwebs.app/_host/records/ContactRequest" \
-H "content-type: application/json" \
-d '{ "name": "Ada", "email": "ada@example.com", "message": "Hello" }'{ "id": "3f0c…", "data": { "id": "3f0c…", "entityType": "ContactRequest", "name": "Ada", "…": "…" } }id,entityType,createdAtandupdatedAtare set by the app. Values you send for them are ignored.- A model’s required fields must be present on create and cannot be emptied on update.
- If the model has an owner field (for example
ownerorcreatedBy) and you leave it empty, it is filled with the signed-in caller. PATCHchanges only the fields you send.DELETEanswers{ "data": { "id": "…", "deleted": true } }.
Errors
Errors have one shape:
{ "error": { "code": "FORBIDDEN", "message": "You may not create Booking records." } }| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | A required field is missing or empty. |
| 400 | INVALID_QUERY | filter, fn or field is not valid. |
| 400 | INVALID_JSON | The body is not a JSON object. |
| 401 | SIGN_IN_REQUIRED | The rules need a signed-in caller, the app requires sign-in, or the session or token is no longer valid. |
| 403 | FORBIDDEN | The caller is known, but the rules do not allow this. |
| 403 | NOT_INVITED | The app lets in only invited people, and this caller is not on its people list. |
| 404 | UNKNOWN_ENTITY_TYPE | The app declares no such model. |
| 404 | NOT_FOUND | No such record, or one the caller may not see. |
| 404 | APP_NOT_FOUND | The app is not published right now. |
| 405 | METHOD_NOT_ALLOWED | The route does not take that method. |
| 413 | PAYLOAD_TOO_LARGE | The body is over 1 MB. |
The owner’s view
As the app’s owner, you also see and edit every record in the studio’s App → Records panel, whatever the rules say. That panel is for people. It is not an API for your code. See Working in the studio.
See also
- Data models and records: where the records come from and how starting records work.
- Sign-in, roles and people: writing the rules this API applies.
- Limits.