REST API (v1)

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 URL and endpoints

Base path:

https://example.com/PiranyaPlatform/Rest/v1

CRUD endpoints:

Action endpoints:

Custom workflow endpoints:

OpenAPI:

Quickstart

List 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

Authentication is identical to the Data API. Use the Authorization HTTP header with either:

Request elevation

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"

Type discovery

To explore available types and fields:

Actions are also exposed in OpenAPI if include_actions=true (default).

Common query parameters

Supported on list and single endpoints (same semantics as the Data API):

depth, expand, include, ignore — how they interact

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:

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:

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:

Response structure

Successful responses return JSON entities:

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:

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.

Create / Update / Delete payloads

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"
}

Batch (multiple entities)

Send a JSON array directly:

[
  { "username": "a@piranya.dk" },
  { "username": "b@piranya.dk" }
]

PATCH (partial update)

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:

Actions (lifecycle endpoints)

Actions are registered via Ninject and executed through the ActionService. URL format:

POST /resources/{type}/{id}/actions/{action}

Action keys are resolved as:

Example: POST /resources/Order/123/actions/Deliver

OpenAPI specification

The OpenAPI document is generated dynamically from runtime schemas:

Filtering and size reduction

The OpenAPI endpoint supports filtered, self-contained subspecs: /openapi/type/Order,OrderProduct.json

Referenced types are included transitively (unless you disable it):

Other options:

Pagination (cursor first)

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:

Notes:

Status codes and minimal responses

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.

Filtering

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.

Preset filters

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.

ID types

IDs can be integers, strings, or GUIDs depending on the entity. OpenAPI exposes the per-type ID schema where possible.

Limitations

Compatibility paths

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.