Skip to content

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.

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().

  • 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.
  • BelongsTo fields need the view permission of the related resource, as in the panel.
  • A resource with a required FileUpload cannot 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.

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"}}}
  1. 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().
  2. Rules: the form rules are the only check of API input. Rules that only the browser enforces (required in HTML, hidden fields) do not exist for the API.
  3. 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.
  4. 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.
  5. afterSave() of virtual fields must accept a change of other fields only (PATCH).
  6. Actions: every action with a handler is available; check its permission and visible().
  7. Permissions: grant clients the narrowest set; a separate client per system.