Resource endpoints
A resource offers its records to API clients when it lists the operations
in $api. Fields, rules, policies and actions come from the resource, so
the API and the user interface never differ.
final class ProjectResource extends Resource{ protected array $api = ['list', 'show', 'create', 'update', 'delete', 'actions'];}Possible operations: list, show, create, update, delete,
actions and transitions (Resource::API_OPERATIONS); the default is
none.
The resource must be in a panel of $apiPanels. A slug may be used by one
API resource only.
| Operation | Method and path | Permission | Uses |
|---|---|---|---|
list |
GET /api/v1/projects |
projects.view |
collection: filters, search, sort; policies |
show |
GET /api/v1/projects/{uuid} |
projects.view |
findRecord() (with scopeQuery()), policy |
create |
POST /api/v1/projects |
projects.create |
form rules (create), createRecord(), afterSave(); 201 with Location |
update |
PATCH /api/v1/projects/{uuid} |
projects.update |
form rules (edit) of the sent fields, updateRecord() |
delete |
DELETE /api/v1/projects/{uuid} |
projects.delete |
deleteRecord(), policy; 204 |
actions |
POST /api/v1/projects/{uuid}/actions/{action} |
permission of the action | visibility, policy and form of the action |
transitions |
GET /api/v1/projects/{uuid}/transitions, POST …/transitions/{name} |
actors of the workflow | the engine of the workflow |
Records the client may not see (other tenant, scopeQuery(), policy) are
404. Writes run in a transaction and are audited with the client as actor
(api:<name>), plus an entry api.request per call.
Representation
Section titled “Representation”apiRepresentation() returns the UUID, the fields of the form and the
timestamps:
{"uuid": "0199f6d3-…", "name": "Alpha", "status": "running", "budget": "1250.50", "starts_on": "2026-11-01", "is_active": true, "owner_id": "0199f6c1-…", "created_at": "2026-10-25T14:30:00+01:00", "updated_at": "2026-10-25T14:30:00+01:00"}Internal ids, tenant_id, columns that are not in the form, passwords and
files never appear. Override apiRepresentation() and
apiRepresentationSchema() (OpenAPI) for an own shape.
| Field | JSON value | Accepted input |
|---|---|---|
TextInput, Textarea, MarkdownEditor, Select, Radio |
string | string, numbers as string |
NumberInput (integer) |
number | number or digits |
NumberInput (decimals) |
string "1250.50" (no precision lost) |
number or string with a point; no grouping, at most the decimals of the field |
DateInput |
"2026-11-01" |
the same |
DateInput::withTime() |
"2026-11-01T14:30:00+01:00" |
ISO 8601, offset optional (converted into the time zone of the application); seconds are dropped |
Toggle |
true / false |
true, false, 1, 0 |
BelongsTo |
UUID of the related record | UUID |
CheckboxList, TagsInput |
list of strings | list |
Repeater |
list of objects | list of objects; unknown keys are dropped |
PasswordInput |
never shown | string |
FileUpload |
not part of the API | not accepted |
Each field converts the JSON value into the input of the form, then the
rules and the dehydrate() of the form apply unchanged. Own fields override
apiValue(), apiInput(), apiSchema(), isApiReadable() and
isApiWritable().
Writing
Section titled “Writing”- Unknown fields are dropped; fields that are disabled or hidden in the operation are not written.
- Create: missing fields get their default (
default()). - Change (PATCH): only the sent fields are validated and written. Virtual
fields that were not sent keep their values (
formValues()), as if the form had been sent unchanged. BelongsTofields need the view permission of the related resource, as in the panel.- A resource with a required
FileUploadcannot be created through the API (422).
| Parameter | Example | Notes |
|---|---|---|
filter[<name>] |
filter[status]=running |
filters of the collection; unknown filters are 422 |
filter[<name>][<part>] |
filter[created][from]=2026-01-01, filter[budget][min]=1000 |
ranges: from/until, min/max (numbers with a point) |
search |
search=Karl Heinz |
searchable columns; words are joined with OR |
sort |
sort=name, sort=-name |
sortable columns; without sort the newest records come first |
limit |
limit=50 |
1–100, default: page size of the collection |
cursor |
cursor=<next> |
the next value of the previous page |
The cursor is encrypted and bound to the client and the query: it cannot be
read, changed or used for another query. Without sort pages follow the
time ordered UUIDs (stable while records are added); sorted lists page by
position.
Actions
Section titled “Actions”Record actions with a handler are available under
/{uuid}/actions/{name}, the name in kebab case (markDone →
mark-done). The body holds the fields of the action form. The answer:
{"data": {"message": "Project finished.", "record": {"uuid": "…", "status": "done"}}}Checklist before switching a resource on
Section titled “Checklist before switching a resource on”- Form: every field in the form is readable by the client. Move fields
that must not leave the system out of the form, or override
apiRepresentation(). - Rules: the form rules are the only check of API input. Rules that only
the browser enforces (
requiredin HTML, hidden fields) do not exist for the API. scopeQuery()and policies must not rely on a logged in user: during API calls there is none (auth()->user()is null). Use the gate (service('gate')) and the record, or treat API calls explicitly.- Policies are registered in
Config\Anvil::$policies, so that they apply to panels, API and CLI alike. Policies registered in a filter of a panel do not reach the API. afterSave()of virtual fields must accept a change of other fields only (PATCH).- Actions: every action with a handler is available; check its
permission and
visible(). - Permissions: grant clients the narrowest set; a separate client per system.