SIGNALS Documentation
API Reference

History & Point-in-Time

Read a record's audit history, and reconstruct what it looked like at an instant in the past.

Overview

Two read-only endpoints expose the audit trail (action_logs) as a first-class part of a record's API surface:

Endpoint Answers
GET /api/v1/{resource}/{id}/history What has happened to this record, and who did it?
GET /api/v1/{resource}/{id}/as_at?at=… What did this record look like at a given instant?

Both are available for four resources: rentals, invoices, accounts, and catalogue_items.

Neither endpoint is a projection engine. as_at reconstructs state by folding audit diffs backwards over the current row — it is the supported public approximation of a full event-sourced projection, and it tells you, on every response, exactly how faithful the answer was. When a real projection engine lands, these endpoints remain the stable public contract.

Authorisation

Both endpoints require two grants:

  • the resource's own read permission and ability (rentals.view / rentals:read, and so on), and
  • the audit-trail grants action-log.view / action-log:read.

A token scoped only to the resource cannot use these endpoints as a side door into the audit trail.

History

GET /api/v1/rentals/42/history?page=1&per_page=20
{
  "history": [
    {
      "id": 9001,
      "action": "rental.updated",
      "actor_type": "user",
      "actor_id": "7",
      "actor_label": "Jo Bloggs",
      "user_id": 7,
      "user_name": "Jo Bloggs",
      "old_values": { "status": "quotation" },
      "new_values": { "status": "order" },
      "via": "web",
      "created_at": "2026-01-15T14:30:00+00:00"
    }
  ],
  "meta": { "total": 12, "per_page": 20, "page": 1 }
}
  • Entries are newest first, offset-paginated like every other collection.
  • via is the surface the change came through (web, api, mcp, cli), lifted from the entry's metadata.
  • Soft-deleted records still answer. History outlives the record it describes. A 404 means the id never existed, not that the record was deleted.

Point-in-time (as_at)

GET /api/v1/rentals/42/as_at?at=2026-01-01T00:00:00Z

at is required, must be a valid ISO 8601 timestamp, and must not be in the future (422 otherwise). If the record did not yet exist at that instant, the response is a 404 naming the earliest instant it can be evidenced at.

The not-created response uses the standard API error envelope:

HTTP/1.1 404 Not Found
{
  "message": "No rental state exists for identifier 42 at 2026-01-01T00:00:00Z; it was first recorded at 2026-02-01T00:00:00Z.",
  "code": "aggregate_not_created"
}

The response is the resource in its normal shape — the same DTO GET /api/v1/rentals/42 returns — plus a meta.reconstruction block:

{
  "rental": { "id": 42, "status": "quotation", "…": "…" },
  "meta": {
    "reconstruction": {
      "as_at": "2026-01-01T00:00:00+00:00",
      "complete": true,
      "uncovered_fields": [],
      "unaudited_fields": ["internal_notes"],
      "events_folded": 6,
      "relations": "Only the record's own columns are reconstructed. …"
    }
  }
}

How the fold works

Starting from the record as it stands today, every audit entry newer than at is undone, newest first. For each key in an entry's new_values that also appears in its old_values, the attribute is reset to the old value. Order matters: walking newest→oldest is what stops an intermediate value from surviving when the same field moved twice.

Read meta.reconstruction before trusting the answer

Field Meaning
complete true only when every folded diff was fully reversible.
uncovered_fields Attributes a folded diff changed but could not reverse — the audit entry recorded a new value with no matching old value. These fields still hold their current value.
unaudited_fields Fillable attributes that appear in no audit diff for this record at all. Presented as-is; the audit trail cannot prove they never moved.
events_folded How many audit entries were applied in reverse.
relations A fixed caveat, repeated on every response: only the record's own columns are reconstructed.

Known limits

  • Relationships are current, not historical. Included or lazily loaded relations reflect today's state. Only columns move.
  • Type fidelity is the audit trail's fidelity. Values come back as the writer serialised them into JSONB — money stays an integer in minor units, datetimes come back as stored strings, JSON columns as decoded structures. They are re-hydrated through the model's casts, so scalar and JSON columns round-trip faithfully; anything the writer serialised lossily stays lossy.
  • Nothing is persisted. The reconstructed record is built in memory and never saved.

MCP

The same two surfaces are exposed as first-class MCP tools, both read-only:

Tool Arguments
get-entity-history resource, id, page, per_page
get-entity-as-at resource, id, at

resource is one of rentals, invoices, accounts, catalogue_items.