Skip to main content
    All articles

    Building Case Timeline Features from Orders Data

    4 August 202615 min readCourtMesh Team
    Cover card headed One Timeline, Two Clock Speeds, with the line: do not block the page

    Ask a litigator what they want from case software and a timeline is usually in the first three answers. Ask an engineer to build one and they will reach for a single endpoint that returns a list of dated events. The gap between those two instincts is where most case timeline features go wrong, because the events a litigator wants come from two very different places with very different latency.

    Some of a matter's history is already in the store: judgments and orders that have been collected, parsed and indexed, sitting behind identifiers you can query in milliseconds. The rest is upstream, in a registry that will hand it over at its own pace, over an interface built for a person and not for your render loop. A timeline feature that pretends these are the same thing will be fast and incomplete, or complete and hung.

    The CourtMesh API models the two cases as two separate surfaces, which is the correct design and also a design that requires you to know which one you are calling. This piece covers both: what GET /cases/:id/related returns and what it deliberately leaves out, how the POST /request-timeline and GET /get-timeline/:requestId job pair works, and the client side handling that makes an asynchronous fetch feel acceptable to a user who just wants to know what happened in their matter.

    Two Surfaces, Two Latency Profiles

    Before any code, get the mental model right, because everything else follows from it.

    The synchronous surface

    GET /cases/:id/related reads what is already stored. It finds every document sharing the same case number, sorts them, and returns both the documents and an assembled timeline. It is fast, it is cheap, it draws no AI credit, and it is safe to call while a page is rendering. Its limit is that it can only show you what has already been collected.

    The asynchronous surface

    POST /request-timeline starts a job that fetches orders for a case from upstream, and GET /get-timeline/:requestId is how you find out how it went. It is slow because the source is slow. It is the only way to reach orders that have not been collected yet, and it must never sit inside a request that a user is waiting on.

    In practice most products want both, layered. Render the stored timeline instantly so the page is useful in one round trip, then offer the user a way to go and fetch fresh orders from the registry, with the honest expectation that it will take a while.

    This endpoint does one thing precisely. It takes the case number of the record you asked for, finds every stored document that shares that case number, sorts them by decision date ascending, caps the set at 50, and returns two arrays.

    relatedDocuments

    Each entry carries an id, a title, the caseNumber, the court, a decisionDate formatted as YYYY-MM-DD, the caseType, and an isCurrent boolean marking which entry is the record you asked about. That last field is more useful than it looks: it is what lets you render the family of documents with the current one highlighted, without your client having to compare identifiers.

    timeline

    Each timeline entry carries four fields, and each of them repays a moment's attention.

    FieldWhat it holdsWhat to do with it
    dateThe date of the document, which is what the entries are sorted byRender it as the anchor of the entry. It is the only field guaranteed to be present, because an entry is not produced without one.
    statusOne of exactly two values: Final Judgment or Hearings / OrdersUse it for the visual distinction between a disposal and everything that came before it. Two values means you can style it exhaustively without a fallback branch that never runs.
    statusLabelA human readable label that prefers the disposal nature when one is presentShow this to the user rather than status. It is the field that says something specific about the event rather than which of two buckets it falls into.
    documentIdThe identifier of the underlying documentMake the entry clickable. It is what you pass to GET /cases/:id to open the record, or to GET /cases/:id/pdf for the document itself.

    The filter that explains most surprises

    Timeline entries are only produced for documents that have both a date and a stored PDF. That is a deliberate quality gate: a timeline entry that cannot be opened is worse than no entry, because it invites a user to click on nothing. The practical consequence is that timeline can be shorter than relatedDocuments for the same case, and both can be shorter than the matter's real history. Neither is a bug. Show the count you are displaying rather than implying completeness.

    One more behaviour worth handling explicitly. If the case has no case number at all, both arrays come back empty with an explanatory message. This happens on thin records, and it is a legitimate answer rather than an error. Your client should render an empty state that says so, not a spinner and not a failure.

    POST /request-timeline and GET /get-timeline: The Slow Path

    When you need orders that have not been collected yet, you are asking the platform to go out to an upstream court source on your behalf. That is a fundamentally different operation and it is modelled as a job.

    Starting the job

    A POST to /request-timeline with a body carrying case_id starts the fetch. It returns a requestId and a status. It returns 404 if the case does not exist, which is the one error worth distinguishing clearly in your client, because it means the identifier you hold is wrong rather than the fetch having failed.

    There is an important short circuit here. When the work has already been done and the result is cached, the response comes back with cached set to true, along with a message, an orderCount and possibly the orders themselves, immediately. Handle that branch first. A client that unconditionally starts polling after the request call will sit through a polling cycle for data it already has in hand, and will look slower than the API actually is.

    Polling for the result

    A GET to /get-timeline/:requestId returns requestId, status, createdAt and updatedAt on every call, and then, as they become available, startedAt, completedAt, error, orders, orderCount and totalOrderCount. An unknown identifier returns 404 with a message that the request was not found.

    Read that field list as a state machine rather than as a response shape. createdAt without startedAt means the job is queued and nothing has gone upstream yet. startedAt without completedAt means it is in flight. completedAt means it is over, and whether it succeeded is answered by whether error is populated. orderCount against totalOrderCount is your progress signal when the job is fetching in stages. Modelling those states in your client is what turns a spinner into an interface a user is willing to wait in front of.

    Client Side Handling That Does Not Annoy Anyone

    The endpoints are simple. The quality of a timeline feature is almost entirely decided by what your client does around them.

    1

    Render the stored timeline first, in one round trip

    Call GET /cases/:id/related as part of loading the case page. It is synchronous and cheap, and it gives the user something real immediately: the family of documents, the disposal if there is one, and dated entries they can open. A page that shows nothing until the slow path finishes has thrown away the fast path for no reason.

    2

    Kick off the job on user intent, never on page load

    Fetching orders from upstream is expensive in time and in load on a source that is not yours. Put it behind an explicit action, something like fetch latest orders, so it runs when someone actually wants fresh data. Firing it on every page view means every refresh, every duplicate tab and every crawler starts a registry fetch.

    3

    Handle the cached branch before you start polling

    If the response to /request-timeline carries cached true, you already have orderCount and possibly orders. Render them and stop. Only fall through to polling when the response is a genuine job in progress.

    4

    Poll with backoff, not in a tight loop

    A tight loop against a job endpoint is pure waste and will hit the rate ceiling of ten requests per minute per API key long before the job finishes. Start at a few seconds, grow the interval, and cap it. A sensible pattern is to poll at five seconds for the first minute, then at fifteen, then at thirty, with an overall deadline after which you stop and tell the user honestly.

    5

    Show a real pending state

    Use the timestamps you are given. Queued when there is a createdAt and no startedAt, fetching when there is a startedAt, and a progress hint from orderCount against totalOrderCount when both are present. A state that changes tells the user the system is working; an indeterminate spinner for ninety seconds tells them it is broken.

    6

    Handle the error field as a first class outcome

    A job can complete having failed. Check error on every poll response, surface what it says, and give the user a way to retry that does not require reloading the page. Silently rendering an empty timeline after a failed fetch is the worst possible outcome, because it looks like an answer.

    7

    Treat zero orders as a legitimate answer

    A completed job with an orderCount of zero means the upstream source published no orders for this matter. That is information, not failure. Say so plainly: no orders were published for this case at the source. Do not show an error, and do not leave the user looking at a blank panel wondering whether it worked.

    8

    Persist the result and do not re-fetch reflexively

    Store what came back with a timestamp of when you fetched it, and show that timestamp. A user who can see that orders were last fetched two hours ago will not press the button again, which is better for your credit consumption and better for the upstream source.

    The anti-pattern that produces the most support tickets

    Calling the upstream order fetch synchronously inside a page render, or inside an API of your own that a browser is waiting on. A synchronous call that waits on a registry is a call that times out. It will work in development against a warm cache, pass review, and then fail in production on exactly the matters your users care most about, because those are the ones with the most orders to fetch. Every layer between the user and the registry will impose its own timeout, and the failure will surface as a gateway error that tells nobody anything useful.

    Why the Job Pattern Is the Right Design Here

    It is tempting to read an asynchronous job pair as an inconvenience the API is passing on to you. It is worth arguing the opposite, because the alternative is genuinely worse.

    Upstream court sources were built for a person looking up one matter. They are slow in a way that is not a defect but a design point, they are variable, and they go down for maintenance without announcing it to your monitoring. Any architecture that puts a live registry fetch on the critical path of a user request inherits every one of those properties. Your median response time becomes the registry's median. Your availability becomes the registry's availability multiplied by your own. And your failure mode becomes a timeout, which is the least informative error in computing because it tells you only that something took too long.

    A job pattern breaks that coupling. The request to start work returns immediately and cheaply. The slow, unreliable operation happens where nothing is blocked on it. State is durable, so a browser refresh, a lost connection or a mobile app moving to the background does not destroy the work. Failure is a value in a field rather than an exception in a stack. And retries become a decision your product makes deliberately rather than something a user performs by mashing a button.

    A synchronous API is a promise about latency. When the work depends on a source you do not control, that is a promise you are not in a position to make, and the polite thing is to say so in the shape of the interface.

    This is also why the caching behaviour on /request-timeline matters more than it first appears. It means the second user asking for the same matter's orders does not trigger a second registry fetch. The job pattern plus a cache converts a per-user load on an upstream source into a per-matter one, which is the difference between a well behaved integration and one that becomes somebody else's incident.

    Assembling Something a Litigator Would Actually Use

    Having both surfaces, the product question is what to show. A raw list of dated rows is not a timeline; it is a table with a date column. A few decisions make the difference.

    • Lead with the disposal if there is one. The status field is either Final Judgment or Hearings / Orders. A user scanning a matter wants to know first whether it has been decided, so give the final judgment visual weight rather than burying it in date order.
    • Show statusLabel, not status. The label prefers the disposal nature when it is present, which is the difference between an entry that reads Hearings / Orders and one that says something specific about what the court did.
    • Make every entry openable. Each timeline entry carries a documentId. Wire it to the record through GET /cases/:id and to the document itself through GET /cases/:id/pdf, which returns a presigned pdfUrl with an expiresIn of 3600 seconds. Fetch that link at click time, never cache it as if it were permanent.
    • Distinguish stored from fetched. If your interface merges the stored timeline with freshly fetched orders, tell the user which is which and when the fetch happened. A merged list with no provenance is a list nobody can verify.
    • Say what is capped. The related endpoint caps at 50 documents. On a heavily litigated matter that is a ceiling the user should know about rather than discover by counting.
    • Handle the thin record honestly. A record with no case number, or with documents lacking dates or stored PDFs, yields a short timeline or none at all. Metadata completeness varies by court and by year, District Court records are thinner than Supreme Court records, and older records are thinner than recent ones. Empty is sometimes the truthful answer.
    A synchronous upstream fetch on the critical path, producing timeouts on exactly the busiest matters
    Tight polling loops that exhaust the ten requests per minute per key ceiling before the job completes
    Ignoring the cached true branch, so users wait through a poll cycle for data already returned
    Rendering an empty timeline after a failed job, so a failure is presented to the user as an answer
    Caching a presigned PDF link past its 3600 second expiry and shipping broken downloads
    Presenting an assembled timeline as the complete procedural history of the matter

    What a Timeline of Orders Is, and What It Is Not

    The interpretive point matters as much as the engineering, and it is the part software people most often get wrong when building for lawyers.

    A timeline assembled from orders is a record of what was published. It is not the procedural history of the matter. Hearings happen that produce no uploaded order. Orders are pronounced before they are uploaded. Registries publish at their own pace, and a stage can exist in the papers well before it appears online. Applications inside a matter carry their own numbers and may sit outside the family of documents sharing the main case number. So the correct description of what you have built is a view of what the registries published against this case number, ordered by date, and that is precisely how it should be labelled in the interface.

    The second half of the point is about significance, and it runs the other way. An entry that looks minor in a list can be the most important thing in the matter. An interim order under Order 39 of the CPC 1908 granting or refusing an injunction may govern how the parties behave for the next two years, while the main matter's status line reads, accurately and unhelpfully, as pending. An order on an application under section 9 of the Arbitration and Conciliation Act 1996, a stay in a section 138 proceeding under the Negotiable Instruments Act 1881, a direction on a moratorium question under the IBC 2016: these are single rows in your timeline and the entire commercial reality for the client.

    Design implication

    Do not build an interface that treats the final judgment as the only entry worth reading and everything else as noise before it. The Hearings / Orders bucket is where a great deal of what actually affects parties is decided. Rank by date, weight by significance, and let the user open anything. And keep the registry's primacy visible: what you display is a view of what was published, and it should be confirmed against the official record of the court concerned before anyone acts on it.

    Putting It Together

    The whole feature, end to end, is short to describe once the two surfaces are clear in your head. On page load, fetch the case with GET /cases/:id and its stored history with GET /cases/:id/related, and render both immediately, with the disposal distinguished and every entry openable through its documentId. Offer an explicit action to fetch fresh orders. When the user takes it, POST to /request-timeline with the case_id, check for cached true and render straight away if it is set, and otherwise poll GET /get-timeline/:requestId with backoff, driving a pending state from createdAt, startedAt, orderCount and totalOrderCount. On completion, check error before you check orders, treat an orderCount of zero as an answer, persist what you got with a fetch timestamp, and show that timestamp so nobody presses the button twice.

    That is a timeline feature that is fast when it can be, honest when it cannot, and does not fall over on the matters with the longest histories. The full field lists for both surfaces, including everything a case record carries, are in the endpoint reference.

    Build the timeline on the surface that fits the latency

    A case timeline is two features wearing one name. GET /cases/:id/related is synchronous, cheap and safe to render on, returning relatedDocuments and a timeline assembled from stored documents that carry both a date and a PDF. POST /request-timeline with GET /get-timeline/:requestId is the job pair for pulling orders from upstream, with a cached short circuit, durable state and an error field you must actually read. Use the fast path for the page and the job for freshness, and treat what you display as a view of what the registries published rather than as the procedural history of the matter. Field level detail for all twelve endpoints is at the API documentation, and there is an API overview.

    Explore CourtMesh
    TimelineOrdersAPIAsync JobsDevelopers
    X LinkedIn