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
| Path | Returns |
|---|---|
/odata | The service document — every entity set, as JSON |
/odata/$metadata | The CSDL schema — types, properties, relationships |
/odata/$batch | The batch endpoint (POST only) |
Collections and entities
GET /odata/Flights → the collection
GET /odata/Flights(1) → one entity, by keyA 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{ "@odata.context": "…", "value": "lhr" }Append /$value for the bare value, with no JSON envelope:
GET /odata/Flights(1)/origin/$valuelhr$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.
Navigating relationships
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 onThis is the addressing alternative to $expand, and the choice between them is about the shape you want back:
| Returns | Use when | |
|---|---|---|
/Flights(1)/passengers | the passengers, alone | the client is showing a related list on its own |
/Flights(1)?$expand=passengers | the flight, with its passengers nested | the 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=flightDeeper 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 bookingsA 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=originWhat a bad path answers
| Situation | Status | Code |
|---|---|---|
| Unknown entity set | 400 | unknown_entity_set |
| No entity matches the key | 404 | entity_not_found |
| Unknown property or navigation property | 400 | unknown_navigation_property |
| Composite key sent positionally | 400 | invalid_key |
$value not following a structural property | 400 | invalid_path |
| Key literal that doesn't fit the property's type | 400 | invalid_key |
Single-entity read on a resolver that has no resolveOne() | 501 | entity_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/Productsis rejected asmethod_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 tomiddleware; if the host mounts the routes itself, its own group already supplies one.
// 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?.