Skip to content

Resource Paths ​

Every OData URL has the same two halves: a path that names what you are reading, and system query options that shape how it comes back. This page covers the path; the pages that follow cover the options.

/odata/Flights(1)/passengers?$select=name&$top=10
       └──────── path ──────┘└──── query options ────┘

The path is not a route your application declares. It is derived from the schema, so every entity set, every key, every property and every navigation property is addressable the moment it appears in $metadata.

Service root and schema ​

PathReturns
/odataThe service document — every entity set, as JSON
/odata/$metadataThe CSDL schema — types, properties, relationships
/odata/$batchThe batch endpoint (POST only)

Collections and entities ​

GET /odata/Flights          → the collection
GET /odata/Flights(1)       → one entity, by key

A key in parentheses selects a single entity. String keys are quoted, as in /odata/Airports('LHR'); the literal is parsed according to the key property's declared Edm type, so a value that doesn't fit answers 400.

Composite keys ​

An entity type whose key spans several properties uses named-key syntax — the positional form is only valid for a single-property key:

GET /odata/Assignments(tenant_id=7,project_id=34)

Sending Assignments(7,34) answers 400 Composite key requires named-key syntax.

Properties ​

A structural property can be addressed directly on an entity:

GET /odata/Flights(1)/origin
json
{ "@odata.context": "…", "value": "lhr" }

Append /$value for the bare value, with no JSON envelope:

GET /odata/Flights(1)/origin/$value
lhr

$value responses are served as text/plain, which makes them convenient for a client that wants a single field — a download link, a status string, a counter — without parsing a document. $value must follow a structural property; anywhere else it answers 400.

Every navigation property is a path segment. Traversal to a collection returns a collection; to a single entity, an entity:

GET /odata/Flights(1)/passengers   → the passengers on flight 1
GET /odata/Passengers(1)/flight    → the flight passenger 1 is on

This is the addressing alternative to $expand, and the choice between them is about the shape you want back:

ReturnsUse when
/Flights(1)/passengersthe passengers, alonethe client is showing a related list on its own
/Flights(1)?$expand=passengersthe flight, with its passengers nestedthe client needs parent and children in one payload

A traversed collection takes the same query options as any other collection:

GET /odata/Flights(1)/passengers?$select=name&$orderby=name desc&$top=10&$count=true
GET /odata/Flights(1)/passengers?$expand=flight

Deeper paths ​

Segments chain, and a segment may carry its own key:

GET /odata/Flights(1)/passengers(5)          → one passenger of flight 1
GET /odata/Flights(1)/passengers(5)/name     → that passenger's name
GET /odata/Flights(1)/passengers(5)/bookings → that passenger's bookings

A keyed segment becomes the new anchor for whatever follows it; unkeyed single-valued segments are walked at execution time to find the parent entity.

Traversal follows what discovery found. If a navigation property is absent from $metadata — a polymorphic relation, or one whose other side was never discovered — the segment answers 400 Unknown property or navigation. See Model Discovery.

Functions and singletons ​

GET /odata/GetFlightCount()               → a function import
GET /odata/GetFlightsByOrigin(origin='lhr')
GET /odata/DefaultFlight                  → a singleton
GET /odata/DefaultFlight?$select=origin

See Functions & Singletons.

What a bad path answers ​

SituationStatusCode
Unknown entity set400unknown_entity_set
No entity matches the key404entity_not_found
Unknown property or navigation property400unknown_navigation_property
Composite key sent positionally400invalid_key
$value not following a structural property400invalid_path
Key literal that doesn't fit the property's type400invalid_key
Single-entity read on a resolver that has no resolveOne()501entity_resolver_not_supported

A path is authorized like any other read: the plan it produces passes through the read authorizer before anything is fetched.

Verbs ​

The engine is read-only, so paths answer GET. Two exceptions exist, and neither is a write:

  • POST /odata/$batch — the batch endpoint, whose inner requests are themselves reads.
  • HEAD /odata — the CSRF handshake, described below.

Any other verb answers 400 with code method_not_allowed. (405 Method Not Allowed would be the more conventional answer; the current status is 400.)

The CSRF handshake ​

A SAP UI5 model fetches a CSRF token before it considers a service usable, and refuses to proceed if the handshake fails. This engine answers it:

HEAD /odata

200 OK
X-CSRF-Token: 7mGqK…

The token comes from Laravel's csrf_token(), so it is the same token the rest of your application issues. The engine never validates it — there is nothing to protect on a read-only service — but returning it lets a UI5 model complete a step it will not skip.

Two details matter in practice:

  • Only the service root answers HEAD. HEAD /odata/Products is rejected as method_not_allowed, like any other non-GET.
  • It needs a session. csrf_token() reads the session store, so the OData routes must run through session middleware for the header to carry a real token. If your routes are registered by the package, add it to middleware; if the host mounts the routes itself, its own group already supplies one.
php
// config/odata.php — the package's own route group
'middleware' => [\Illuminate\Session\Middleware\StartSession::class],

Mutations belong in endpoints of your own — see Why OData?.