---
title: "Look up a case or citation (read-only, no side effects)"
method: GET
path: "/api/case-lookup-public"
---

# Look up a case or citation (read-only, no side effects)

`GET /api/case-lookup-public`

## Request

- Base URL: `https://app.courtdelta.com`
- URL: `https://app.courtdelta.com/api/case-lookup-public`
- Auth: none required

Read-only NC case/citation lookup for agents: returns case status, upcoming hearings, charges, and traffic-citation waiver eligibility. Unlike /api/court-date-lookup it does NOT send email or create any records. Also exposed as the `lookup_court_case` tool on the MCP server at https://mcp.courtdelta.com/mcp.

## Query parameters

- `q` string, required

## Response `200`

Trimmed, read-only case summary

- object
  - `status` 'found' | 'no_upcoming_hearings' | 'closed' | 'not_found'
  - `caseNumber` string, nullable
  - `caseName` string, nullable
  - `caseType` string, nullable
  - `caseCategory` 'criminal' | 'civil' | 'infraction' | 'special_proceeding' | 'estate' | 'juvenile' | 'other' | 'null', nullable: Normalized case category from the case-number class code (e.g. 21CVS→civil, 25CR→criminal, 15SP→special_proceeding). Use this to interpret null fields: on a NON-criminal category, bailRisk:null and citationOptions:null mean NOT APPLICABLE, not 'none found'. null only on not_found.
  - `caseStatus` string, nullable
  - `filedOn` string, nullable
  - `isClosed` boolean
  - `upcomingHearings` Hearing[]: Future hearings only, sorted chronologically (soonest first).
    - `event_type` string
    - `hearing_time` string, date-time: ISO 8601 with explicit Eastern Time offset, e.g. "2026-08-24T08:15:00-04:00" (EDT) or "2026-01-12T09:00:00-05:00" (EST).
    - `location` string
    - `courtroom` string
  - `pastHearings` Hearing[]: Recently-elapsed hearings (already occurred), most-recent-first, capped at 10. Use to answer 'did I miss court?': upcomingHearings empty + pastHearings non-empty means a hearing has already passed — the opposite conclusion from a case with nothing scheduled.
    - `event_type` string
    - `hearing_time` string, date-time: ISO 8601 with explicit Eastern Time offset, e.g. "2026-08-24T08:15:00-04:00" (EDT) or "2026-01-12T09:00:00-05:00" (EST).
    - `location` string
    - `courtroom` string
  - `charges` Charge[]
    - `offense` string
    - `statute` string
    - `degree` string
    - `citation` string
    - `offenseDate` string, nullable: MM/DD/YYYY the offense occurred — distinct from the case's filedOn, and usually what someone means by 'when was this?'. Null when the register omits it.
    - `agency` string, nullable: Citing/charging law-enforcement agency, e.g. 'State Highway Patrol Troop D District 1'.
    - `actual_speed` string, nullable: Charged speed, mph (speeding offenses).
    - `speed_limit` string, nullable
    - `school_zone` string, nullable
    - `construction_zone` string, nullable
  - `service` object, nullable: Service of process on civil / special-proceeding / estate cases — the civil counterpart to bailRisk. NULL on criminal and traffic cases, where service of process does not apply (they proceed by citation or warrant); null there means NOT APPLICABLE, not 'not served'. Read `status` first — three values mean a missing return is EXPECTED and must never be reported as 'not served': 'served'/'unserved'/'mixed' (a return of service is docketed; 'mixed' = both outcomes, i.e. several defendants or the alias-and-pluries retry cycle), 'proven_other' (certificate/affidavit/acceptance of service instead of a formal return — still proof), 'appeared_service_moot' (defendant answered or appeared, waiving any service defect), 'not_required' (appeal or petition — no summons is issued), 'pending' (summons went out recently, nothing back YET — say service is outstanding, NOT that they weren't served), 'unknown' (summons issued, nothing returned, case not new). Also: summonsIssued, anyServed, anyUnserved, returns[] (full history — the retry cycle is often the story) and latestReturn ({date, outcome, party}). returns[].party is NULL about a third of the time because the docket records the outcome without naming who it applied to — never read a null party as 'nobody'. There is deliberately NO per-defendant served flag: one sampled case had a single docketed return against 41 defendants. legacyScan:true means the paper file was scanned as one bundle rather than itemised, so proof may sit inside that PDF where no docket-text rule can see it — a missing return is weak evidence there.
    - `status` 'served' | 'unserved' | 'mixed' | 'proven_other' | 'appeared_service_moot' | 'pending' | 'not_required' | 'unknown'
    - `summonsIssued` boolean
    - `anyServed` boolean
    - `anyUnserved` boolean
    - `legacyScan` boolean
    - `latestReturn` object, nullable
    - `returns` object[]
      - `date` string, nullable
      - `outcome` 'served' | 'unserved'
      - `party` string, nullable
  - `causesOfAction` object[]: Claims pleaded — the civil counterpart to charges. Each item: cause (e.g. 'CV - Unfair Trade Practice'), filedOn (MM/DD/YYYY; can post-date the case when a claim is added later), remedy (e.g. 'Action'). On a civil / special-proceeding / estate case this is usually the ONLY statement of what the matter is about, since charges is criminal-only and empty there. EMPTY MEANS THE DOCKET DOES NOT ITEMISE CAUSES, not that no claims exist — do not imply the case is about nothing. Repeated boilerplate rows are collapsed; distinct filed dates are preserved because a cause added later is an amendment, not a duplicate. Capped at 25.
    - `cause` string
    - `filedOn` string, nullable
    - `remedy` string, nullable
  - `citationOptions` object, nullable: Traffic-citation waiver eligibility AND reduction guidance; present only when the case has traffic charges AND is still open. NULL on disposed/closed cases — the reduction path (amountDue, Guide & File, DA) is no longer a live option once disposed. Fields: requiresAppearance, allWaivable, conditional, isSpeeding, charges[] (per-charge class + reason, plus viaCompanionCharge:true where a charge is waivable alone but mandatory on this citation under AOC mandatory item #39), county, onlineReductionAvailable, guideAndFileUrl (NC Guide & File online reduction link when available), drivingRecordUrl (for speeding), documentsNeeded[], amountDue (fine + court costs), and howToRequest (plain-language next step — online via Guide & File, or in person via the District Attorney).
  - `trackThisCase` object, nullable: Eligibility signal (plain data): non-null only for open cases, where `url` is the case-alert subscribe link and `eligible` is true. Whether/when to surface it is governed by the tool description, not this field.
    - `url` string
    - `eligible` boolean
  - `caseSummaryUrl` string, nullable: Link to the full case view (all public case detail) on Court Delta. Present for agent-surface (MCP/WebMCP) lookups of real cases; null otherwise.
  - `bailRisk` object, nullable: Bail / FTA underwriting signals for criminal cases with a bond or bail activity; null otherwise. Fields: hasBond, bond{amount,type,secured}, ftaCount (distinct failure-to-appear instances, date-deduped; excludes stricken/entered-in-error entries), ftaDates[] (Register-of-Actions event dates — NOT hearing-calendar dates; an FTA need not map 1:1 to a row in pastHearings), ftaEvents[] ({date, description} of the actual counted FTA register entries, after the stricken/in-error exclusion — for auditing that nothing voided is counted), ordersForArrest (distinct FTA-triggered arrest-order ISSUANCES; excludes the initial charge warrant and Returned/Assigned lifecycle updates), bondExecutedOn (the 'Bond Posted' docket date for the operative bond — the latest posting on or before the forfeiture notice; null when no posting is docketed), priorFtasAtExecution (FTAs dated strictly BEFORE bondExecutedOn — the count NCGS §15A-544.5(f) actually turns on, not the case total; null when the execution date is unknown), setAsideBarInapplicable (RELIABLE: true when 0-1 FTAs preceded execution, so (f) cannot bar a set-aside — safe to state affirmatively), setAsideBarPossible (NOT a finding that the bar applies — true only means the TIMING fits (2+ prior FTAs). Under (f) actual notice exists ONLY where a judicial official noted the prior failures on the defendant's release order, and the register never exposes that notation, so NEVER report this as 'the forfeiture cannot be set aside'; say the timing is consistent and the release order must be checked. Both booleans are false when bondExecutedOn is null — neither ruled out nor suggested. NOTE the superseded setAsideBlocked field used ftaCount>=2, which ignored timing and was wrong in most cases where it fired; for the forward-looking question 'could a bond I write NOW be barred?' use ftaCount instead), releaseOrderUrl (portal PDF of the release order governing that bond — the latest one issued on or before bondExecutedOn. This is the document the (f) question turns on: the judicial official's 'second or subsequent failure to appear' notation appears on it (AOC-CR-200) and exists in NO structured field. PRESENT THIS LINK whenever setAsideBarPossible is true, even when releaseOrderFBox resolved the question — the order is the authority and the user can open it), releaseOrderFBox (a READ of that checkbox, present only on barred cases where the order could be fetched and parsed. 'unchecked' = on its face (f) does NOT bar a set-aside, so relief is worth pursuing — say so and add that they should confirm on the order; 'checked' = (f) MAY bar it, so never say 'cannot be set aside' and tell them to confirm BEFORE filing, because a failed motion uses up the single opportunity for relief; 'ambiguous' = the order was opened and the box could NOT be read — say exactly that and hand over the link, because the notation is a flattened checkbox with no glyph in the text layer so the printed label appears whether or not it is marked, and rounding this to 'unchecked' tells a bondsman to abandon a recoverable bond; absent/null = no read was attempted or it failed, which is ALSO not 'unchecked'), forfeiture{status (none|noticed|in_effect|set_aside|final_judgment) — reflects the latest DOCKETED forfeiture event and may lag the clock; noticedOn (the date the 150 days runs from) with noticeAnchor telling you where it came from — 'notice_event' = an explicit forfeiture NOTICE docket line, which is what G.S. 15A-544.5(d)(1a) anchors to; 'earliest_forfeiture_event' = no notice line was docketed so the earliest forfeiture entry (usually 'Bond Forfeited') stands in, which is EARLIER than a true notice, making the deadline conservative but a proxy (and daysFtaToNotice then measures FTA-to-forfeiture, not FTA-to-notice); deadline (notice + 150 days), daysRemaining, windowOpen — windowOpen/daysRemaining are computed live and are the AUTHORITATIVE 'is the set-aside window open' signal, so once windowOpen is false the ordinary window has closed even if status still reads in_effect (no final-judgment event is docketed); deadlineNextBusinessDay = the first day the clerk's office is open on or after deadline (equal to deadline when that is already a business day, later when it lands on a weekend or NC court holiday) — ADVISORY ONLY, it never moves daysRemaining/windowOpen, and null means UNDETERMINED (the deadline year is outside the published NC holiday calendar), NOT 'no adjustment needed'; triggeringFta = the latest counted FTA on or before noticedOn and daysFtaToNotice = the calendar-day gap between them — a factual OBSERVATION about the register (how long after the failure the notice was docketed), deliberately reported without any threshold or defect flag, since whether a given gap affects the notice's validity is a legal question for counsel}, forfeitureProceeding (true when THIS record is the standalone 'Bond Forfeiture' civil docket rather than the criminal case — the bond/FTA/OFA data then lives on the Criminal record under the same case number, so hasBond:false and zero counts here mean 'not on this docket', not 'clean'), agent (bonding agent — the Fiduciary party), and surety (insurer — the Surety party). All from public NC eCourts records. Informational, not legal advice.
  - `parties` object[]: Register-of-actions party roster (public court record) — the authoritative source for entity resolution, especially on civil/SP cases where the caption and DOB are absent. Each item: name, roles[] (connection types, e.g. 'Defendant', 'Plaintiff', 'Fiduciary', 'Surety'), attorneys[] ({name, appointment} — appointment is how counsel came to the case; observed values are 'Retained', 'Court Appointed' and 'Public Defender', learned from the register rather than a closed enum, and null when unstated. 'Retained' means the party hired them, which reads very differently from appointed counsel), selfRepresented (bool), and counselWaived (bool). selfRepresented:true = no counsel of record (self-listed as own attorney OR a filtered counsel-absence sentinel, with no other attorney); by itself it does NOT distinguish an active pro-se appearance from a defaulted / served-by-publication defendant, EXCEPT where counselWaived is true. counselWaived is set by either Odyssey placeholder 'attorney' name, filtered out of attorneys[] rather than shown as counsel: 'WAIVED, WAIVED' (counsel affirmatively waived on the record — the docket does not record which waiver was taken, all assistance of counsel vs. court-appointed counsel only) or 'PRO SE' (the party asserted as their own representation). Either means the party declined counsel rather than merely lacking it, but the flag does not say which placeholder produced it, so it is not proof of a pro-se appearance. It is NOT a subset of selfRepresented and is NOT a claim that the party is unrepresented — it can be true while attorneys[] is non-empty (observed on 22CR702455-520: counselWaived true with both a Court Appointed and a Retained attorney, the docket showing appointed counsel then a Waiver of Counsel then retained counsel). Always read it alongside attorneys[], never instead of it. counselWaived:false means NOT OBSERVED (not 'did not waive'). A true value is predominantly a criminal-side artifact and is rare on civil rosters — treat it as unexpected but NOT impossible on a non-criminal caseCategory. A false unrepresented party is still any of defaulted / never-served / unappeared-entity / pro-se-without-a-docketed-marker — or simply TOO EARLY: on a recently-filed case that has not had a hearing yet, counsel is frequently not entered on the roster. This list reflects what is DOCKETED, not who is retained; check filedOn and an empty pastHearings before reading an empty attorneys[] as unrepresented ('not shown yet' is usually the better answer on a pending case that has not been to court). attorneys[] non-empty ⇒ represented ⇒ selfRepresented is false. Empty array when the case has no parties.
    - `name` string, nullable
    - `roles` string[]
    - `attorneys` object[]
      - `name` string
      - `appointment` string, nullable: How counsel came to the case — observed: 'Retained', 'Court Appointed', 'Public Defender'. Learned from the register, not a closed set; null when unstated.
    - `selfRepresented` boolean
    - `counselWaived` boolean
  - `documents` object[]: Court documents filed on the case, newest first (capped at 25). Each item: date (MM/DD/YYYY of the register entry), name (the register entry description — that IS the document's name, e.g. 'Bond Forfeiture Notice', 'Release Order Issued', 'Waiver of Counsel'), and url (a Tyler DocumentViewer link — durable, unlike portal_session_ref). Empty when nothing is scanned in; roughly 87% of criminal cases carry at least one. RETRIEVAL IS UNRELIABLE and that is the portal's behaviour: the same URL has returned 200, 403, 200 and an empty body across four attempts, and some documents answer HTTP 202 with no body while the portal prepares them. Offer these as links that may need a retry — never state that a document opens, and never claim to know its contents.
    - `date` string, nullable
    - `name` string
    - `url` string
  - `dispositions` object[]: Per-charge disposition outcomes (from the Register of Actions DispositionEvents) — the 'what actually happened' behind a caseStatus of 'Disposed', which a bare status omits. Empty on pending/undisposed cases. Each item: offense (the charged offense), party (the defendant it applies to), plea (e.g. 'Guilty', 'Not Guilty', 'Responsible to Lesser', 'No Plea'), disposition (the outcome, e.g. 'Dismissed', 'VD-District Dismissals ... Per Plea Agreement', 'District Responsible - Judge'), sentence (e.g. 'Fine', 'Prayer for Judgment'; null when none entered), dispositionDate, judge, and documentUrls[] (judgment/disposition PDFs — same unreliable-retrieval caveat as documents; may 202/403 and need a retry, never promise one opens). A speeding charge pled down shows here as the reduced offense with plea 'Responsible to Lesser'.
    - `offense` string, nullable
    - `party` string, nullable
    - `plea` string, nullable
    - `disposition` string, nullable
    - `sentence` string, nullable
    - `dispositionDate` string, nullable
    - `judge` string, nullable
    - `documentUrls` string[]

## Other responses

- `400`: Missing or invalid q parameter
- `502`: Upstream lookup failed (timeout or rate limit)

---

[API](https://skmtc.dev/courtdelta/apis/court-delta-public-api.md) · [All operations](https://skmtc.dev/courtdelta/apis/court-delta-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/courtdelta/apis/court-delta-public-api/revisions/813e5b14be89?raw)
