Hypermedia and the Richardson maturity model

Hypermedia is the part of REST most APIs skip. It has a real cost, and a real payoff when clients must evolve alongside the server.

The four levels

LevelNameWhat it meansExample
0The swamp of POXOne URL, one verbPOST /api with an action field in the body
1ResourcesMany URLs, one per thingPOST /orders, GET /orders/42
2HTTP verbsMethods carry the semantics, plus status codesDELETE /orders/42 returns 204
3Hypermedia controlsResponses contain the links to what you can do nextAn _links.cancel that appears only when cancellable

Level 2 is where almost every successful API lives, and it is a good place to be. The missing piece at level 3 is not links in general — most APIs have some — but links that are the mechanism by which a client discovers valid transitions rather than a decoration.

{
  "id": 42,
  "status": "pending",
  "total": { "amount": 2500, "currency": "GBP" },
  "_links": {
    "self":    { "href": "/orders/42" },
    "items":   { "href": "/orders/42/items" },
    "cancel":  { "href": "/orders/42/cancel", "method": "POST" },
    "invoice": null
  },
  "_embedded": {
    "customer": { "id": 7, "name": "Ada", "_links": { "self": { "href": "/customers/7" } } }
  }
}

HAL, JSON:API and the honest trade-off

  • HAL is minimal: _links and _embedded, with a media type of application/hal+json.
  • JSON:API is far more prescriptive, covering links, relationships, pagination, sparse fields and error objects in one specification.
  • Use a registered media type so clients know which hypermedia dialect the payload uses; inventing your own links array gives you the cost without the tooling.
  • The real payoff is discoverability: a client that reads cancel from the response never needs to hardcode the rule “orders in state pending can be cancelled”.
  • The cost is payload size, more complex serialisation, and a client that is genuinely harder to write without strong typing.
def order_representation(order) -> dict:
    links = {"self": {"href": "/orders/" + str(order.id)}}
    # The transition exists only when it is valid right now
    if order.status == "pending":
        links["cancel"] = {"href": "/orders/" + str(order.id) + "/cancel",
                           "method": "POST"}
    if order.status == "fulfilled":
        links["invoice"] = {"href": "/invoices/" + str(order.invoice_id)}
    return {
        "id": order.id,
        "status": order.status,
        "_links": links,
    }
💡
Hypermedia pays off when many independent clients must track a changing workflow, and costs you when you control both ends of the wire. Deciding against it is a legitimate engineering choice, but make it a decision rather than an omission — and if you skip it, document the state machine somewhere clients can read.

FAQ

Does hypermedia replace API documentation?
No. It removes the client's need to hardcode valid transitions, which is one class of change. Field meanings, authentication and error codes still need documentation.
What is the smallest useful step towards level 3?
Return a link object with a self link on every resource, then add links only for transitions whose validity depends on state. That is most of the benefit for a fraction of the effort.

Resource modelling and methods Documenting APIs with OpenAPI

Last refreshed 2026-09-18.