This document describes the REST API introduced alongside the existing Data API. The REST API is designed for standard CRUD workflows with predictable URLs and an automatically generated OpenAPI specification.
The REST API is dynamic and reflects the modules and features enabled on a site. Types and fields are discovered at runtime.
For API selection guidance and shared semantics, see API Overview.
Base path:
https://example.com/PiranyaPlatform/Rest/v1
CRUD endpoints:
GET /resources/{type} (list)GET /resources/{type}/{id} (single)POST /resources/{type} (create)PUT /resources/{type}/{id} (update)DELETE /resources/{type}/{id} (delete)PATCH /resources/{type}/{id} (partial update)Action endpoints:
POST /resources/{type}/{id}/actions/{action}Custom workflow endpoints:
/Rest/v1/... when a use case is better represented as a workflow or aggregate operation than as a single resource type.OpenAPI:
GET /openapi.json (full spec)GET /openapi/type/{types}.json (filtered spec, comma-separated types)
/openapi/type/Order,OrderProduct.jsonList orders:
curl -H "Authorization: Bearer <token>" \
"https://example.com/PiranyaPlatform/Rest/v1/resources/Order?limit=50"
Get one order:
curl -H "Authorization: Bearer <token>" \
"https://example.com/PiranyaPlatform/Rest/v1/resources/Order/123"
Create a user (single entity):
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"username":"example@piranya.dk"}' \
"https://example.com/PiranyaPlatform/Rest/v1/resources/User"
Batch create:
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '[{"username":"a@piranya.dk"},{"username":"b@piranya.dk"}]' \
"https://example.com/PiranyaPlatform/Rest/v1/resources/User"
Patch a field:
curl -X PATCH -H "Authorization: Bearer <token>" -H "Content-Type: application/merge-patch+json" \
-d '{"username":"new@piranya.dk"}' \
"https://example.com/PiranyaPlatform/Rest/v1/resources/User/123"
Trigger an action:
curl -X POST -H "Authorization: Bearer <token>" \
"https://example.com/PiranyaPlatform/Rest/v1/resources/Order/123/actions/Deliver"
Authentication is identical to the Data API. Use the Authorization HTTP header with either:
Basic (username/password)Bearer (access token)Request elevation uses the same mode (or elevation) parameter as the Data API, with the same values and semantics.
See API Overview for the shared model and Data API for detailed examples.
Example with elevation and department scoping:
curl -H "Authorization: Bearer <token>" \
-H "Authorization-Department: 42" \
"https://example.com/PiranyaPlatform/Rest/v1/resources/Order?mode=provider_department"
To explore available types and fields:
/openapi.json/openapi/type/Order,OrderProduct.jsoninclude_dynamic_types=falseActions are also exposed in OpenAPI if include_actions=true (default).
Supported on list and single endpoints (same semantics as the Data API):
contract_version (shared contract selector used by both Data API and REST API)depthexpandmode / elevationinclude / ignorecache
depth controls how many levels of relations are auto-resolved. At depth=1 only scalar fields are returned; at depth=2 direct relations are included; and so on. Relations with IncludeByDefault=false are never auto-included regardless of depth (use include or expand for those).
expand lets you name specific relation paths that should be resolved and serialized beyond the depth baseline. Paths use dot-notation and are comma-separated:
expand=customer.invoicing_address.country
expand=customer,items.product.prices
Expand is orthogonal to include/ignore:
expand forces a path to be loaded and serialized even if depth would cut it off.IncludeByDefault=false are expanded when explicitly listed in expand.ignore still wins — if a field appears in both expand and ignore, it is suppressed.
include forces individual fields to be included at the current depth level (useful for IncludeByDefault=false fields at the existing depth). It does not extend depth.
ignore suppresses fields from the response regardless of depth or expand.
Precedence:depth baseline → expand adds branches → include/ignore shape output (ignore wins).
Error cases:
depth=0 (count mode) combined with expand returns 400 Bad Request.expand path segment that does not exist or is not a relation returns 400 Bad Request.Examples:
Get an order with its customer's address resolved, even at depth=1:
curl "https://example.com/PiranyaPlatform/Rest/v1/resources/Order/123?depth=1&expand=customer.invoicing_address"
Get a list of orders expanding a non-default-included relation:
curl "https://example.com/PiranyaPlatform/Rest/v1/resources/Order?expand=audit_log"
List endpoints also accept:
limitoffsetcursororder_byorder_direction (asc or desc)searchSuccessful responses return JSON entities:
GET /resources/{type} -> JSON object with items and metaGET /resources/{type}/{id} -> JSON object (404 Not Found if no matching resource exists)POST /resources/{type} -> JSON object for single-entity create, JSON array for batch createPUT /resources/{type}/{id} -> JSON object (without Prefer: return=minimal)List responses use an envelope:
{
"items": [
{ "id": 123, "name": "Example" }
],
"meta": {
"next_cursor": "eyJvZmZzZXQiOjUwfQ==",
"limit": 50,
"prev_cursor": null,
"has_more": true,
"total_count": 123
}
}
Standard metadata may be returned via headers:
Contract-Version (effective contract version used for the response)Content-Language (effective locale used for the response)Impersonated-By-User-Id (when impersonation is active)Scopes (when scopes are present)Request-Id (echoed when provided by client/gateway)Error responses use RFC7807 Problem Details (application/problem+json). Validation errors return 422 with structured errors:
{
"type": "about:blank",
"title": "Error",
"status": 422,
"detail": "Friendly error message",
"instance": "/PiranyaPlatform/Rest/v1/resources/Order/123",
"request_id": "abc123",
"code": "validation_failed",
"errors": [
{ "path": "/", "code": "validation_failed", "message": "Title is required" }
]
}
Validation errors always include message. path and code can be generic depending on the underlying provider/serializer.
The REST API accepts both single-entity and batch payloads using root objects or arrays.
DELETE /resources/{type}/{id} does not require a request body.
Send a JSON object directly:
{
"username": "example@piranya.dk"
}
Send a JSON array directly:
[
{ "username": "a@piranya.dk" },
{ "username": "b@piranya.dk" }
]
Preferred PATCH media type:
Content-Type: application/merge-patch+json
Example:
{
"title": "New Title"
}
Legacy compatibility media type:
Content-Type: application/json
Legacy application/json uses a restricted JSON Patch-like format and currently supports onlyreplace operations:
[
{ "op": "replace", "path": "/title", "value": "New Title" }
]
Notes:
application/merge-patch+json is the preferred format for new clients.id, key, temporary_id), and otherwise by array index.application/json remains supported for backward compatibility.application/json is not full JSON Patch (application/json-patch+json).application/jsonpath must be a single-segment property path (e.g. /title){type} and {id}Actions are registered via Ninject and executed through the ActionService. URL format:
POST /resources/{type}/{id}/actions/{action}
Action keys are resolved as:
{type}.{action} (e.g. Device.PowerCycle)Example:
POST /resources/Order/123/actions/Deliver
The OpenAPI document is generated dynamically from runtime schemas:
DiscoveryService#/components/parameters to reduce sizeIdUuid for IEntityWithGuidIdString for IEntityWithStringKeyIdInt32 for IEntityWithIntIdIdInt64 for IEntityWithLongIdIdFlexible fallback (oneOf)The OpenAPI endpoint supports filtered, self-contained subspecs:
/openapi/type/Order,OrderProduct.json
Referenced types are included transitively (unless you disable it):
include_references=true (default)include_references=false to include only the requested typesOther options:
include_actions=true|falseinclude_dynamic_types=true|falsetype=TypeName (query param fallback)The REST API supports cursor-based pagination. The cursor currently encodes an offset internally, but clients should treat it as opaque.
Request:
GET /resources/Order?limit=100&cursor=eyJvZmZzZXQiOjIwMH0=
The cursor is currently a base64-encoded JSON payload (currently {"offset": N}), but clients should treat it as opaque.
Pagination metadata is returned in the meta object on list responses:
next_cursorlimitprev_cursorhas_moretotal_count (when available)Notes:
cursor takes precedence over offset if both are provided.total_count may be omitted when not available from the underlying query providers.POST returns 201 Created.POST /resources/{type}/{id} is not allowed and returns 405 Method Not Allowed.DELETE returns 204 No Content.PATCH returns 204 No Content.PUT returns 200 OK with a response body, or 204 No Content if the client requests a minimal response.POST /resources/{type}/{id}/actions/{action} returns 204 No Content.To request a minimal response body, send:
Prefer: return=minimal
On POST, the API may return a Location header with the canonical URL of the created resource.
Most entity fields can be used as query parameters for filtering, similar to the Data API. Because types are dynamic, rely on OpenAPI or Discovery to see which fields are exposed for a given type.
Some types expose named presets as preset_{groupKey}=optionKey parameters. These are shorthand
for a predefined combination of filter parameters and are useful when you want to filter by a
well-known category without knowing the specific field values involved.
Example:
GET /resources/Account?preset_main=client
Available preset groups and their option keys are declared in the Discovery schema under
preset_groups. Each option carries filter_parameters (used when filtering) and optionally
creation_defaults (used when creating a new entity from that preset). Only options with
filter_parameters set are valid as filter values.
Presets are also exposed as preset_* parameters in the OpenAPI specification with an enum of
valid option keys, and as standard schema.filters entries (with key preset_{groupKey}) so
generic filter tooling can use them transparently.
IDs can be integers, strings, or GUIDs depending on the entity. OpenAPI exposes the per-type ID schema where possible.
application/json PATCH only supports replace and single-segment paths.data/item wrappers in request bodies.For compatibility, some deployments may also continue to accept legacy root-level resource paths such as /{type} and /{type}/{id} under /PiranyaPlatform/Rest/v1.
New integrations should use the /resources/{type} route family and rely on the OpenAPI document for canonical URLs.