Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: AICD Agenda hits API
version: 1.0.0
contact:
name: AICD API
url: https://aicdapi.com
email: hello@aicdapi.com
x-publisher:
'@id': https://cairn-labs.com/#organization
description: 'Structured Dallas-Fort Worth civic records. Public endpoints retain their documented cache and IP limits. Customer endpoints under /api/v1/data require a server bearer credential and always return Cache-Control: no-store. Customer grants, request/burst/export limits and response size caps are enforced. v1 allows additive response fields; breaking changes use a new version with a minimum 90-day migration notice during the first-customer pilot. Source check time and processing state describe observed coverage; no uptime or completeness guarantee is implied.'
servers:
- url: /
description: Same origin as this document
security: []
tags:
- name: Agenda hits
description: Classified local-government agenda items.
paths:
/api/v1/public/agenda-hits:
get:
operationId: listRecentAgendaHits
summary: List recent agenda hits
description: 'Returns up to three recent classified agenda hits. Normalized `*Slug`
parameters take precedence over their legacy aliases when both are sent.'
tags:
- Agenda hits
parameters:
- $ref: '#/components/parameters/JurisdictionSlugQuery'
- $ref: '#/components/parameters/JurisdictionLegacyQuery'
- $ref: '#/components/parameters/TopicSlugQuery'
- $ref: '#/components/parameters/TopicLegacyQuery'
- $ref: '#/components/parameters/ExcludeTopicSlugQuery'
- $ref: '#/components/parameters/ExcludeTopicLegacyQuery'
- name: limit
in: query
description: Positive requested result limit. The effective limit is capped at 3.
schema:
type: integer
minimum: 1
responses:
'200':
description: Recent agenda hits.
headers:
Cache-Control:
$ref: '#/components/headers/PublicCacheControl'
content:
application/json:
schema:
$ref: '#/components/schemas/AgendaHitListEnvelope'
'400':
$ref: '#/components/responses/InvalidParameter'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
security: []
x-auth-class: public
x-required-scope: []
x-release-status: available
x-markdown-url: /api-reference/listRecentAgendaHits.md
x-curl-example: curl -X GET 'https://aicdapi.com/api/v1/public/agenda-hits'
x-rate-limit: '120 requests per 60 seconds per client IP, counted separately for each public endpoint and keyed on the first X-Forwarded-For entry. The allowance is not shared between public endpoints: exhausting one leaves the others available. If the limiter''s own store fails, the request is allowed through rather than refused.'
x-caching: public, s-maxage=60, stale-while-revalidate=300 on success. Error responses carry no cache directive.
x-notes:
- A backend query failure is reported as an empty list with HTTP 200, not as an error.
- Both a canonical and a short alias are accepted for each filter; the canonical name wins when both are sent.
x-pagination: None. The result set is capped at three hits.
x-retry-after: Sent on 429 only, as whole seconds.
x-errors:
- status: 400
code: invalid_parameter
- status: 429
code: rate_limited
- status: 500
code: internal_error
/api/v1/public/agenda-hits/{matchId}:
get:
operationId: getAgendaHit
summary: Get an agenda hit
tags:
- Agenda hits
parameters:
- name: matchId
in: path
required: true
description: Agenda topic-match UUID.
schema:
type: string
format: uuid
responses:
'200':
description: Agenda hit.
headers:
Cache-Control:
$ref: '#/components/headers/PublicCacheControl'
content:
application/json:
schema:
$ref: '#/components/schemas/AgendaHitDetailEnvelope'
'400':
$ref: '#/components/responses/InvalidParameter'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
description: Returns one public agenda hit by its id, including the source link to the original public record. Ids come from `GET /api/v1/public/upcoming-agenda-hits`, whose `data.hits[].matchId` is this path parameter; the recent-hits list does not return ids. Ids outside the active public coverage set are treated as not found. If the underlying query fails, this endpoint answers 404 rather than 500.
security: []
x-auth-class: public
x-required-scope: []
x-release-status: available
x-markdown-url: /api-reference/getAgendaHit.md
x-curl-example: curl -X GET 'https://aicdapi.com/api/v1/public/agenda-hits/{matchId}'
x-rate-limit: '120 requests per 60 seconds per client IP, counted separately for each public endpoint and keyed on the first X-Forwarded-For entry. The allowance is not shared between public endpoints: exhausting one leaves the others available. If the limiter''s own store fails, the request is allowed through rather than refused.'
x-caching: public, s-maxage=60, stale-while-revalidate=300 on success. Error responses carry no cache directive.
x-notes:
- A backend query failure is reported as 404, not 500. Do not read 404 as proof that the record never existed.
x-retry-after: Sent on 429 only, as whole seconds.
x-errors:
- status: 400
code: invalid_parameter
- status: 404
code: not_found
- status: 429
code: rate_limited
- status: 500
code: internal_error
/api/v1/public/upcoming-agenda-hits:
get:
operationId: listUpcomingAgendaHits
summary: List upcoming agenda hits
description: 'Returns upcoming hits for one topic in meeting-time order. Send `topicSlug`
or its legacy alias `topic`: the request must carry one of them, and `topicSlug` wins
when both are present. A missing or uncovered topic is a 400, so the example request
below sends a topic.
Cursor pagination is the default. The legacy `page` parameter remains
available through page 100. Do not send `cursor` and `page` together.
Cursor responses contain `hits` and `nextCursor`, plus `totalCount` when
`count=true`. Legacy page responses retain `hits`, `totalCount`, `page`,
and `pageSize`. Every successful response includes `meta.pagination`.
Each hit carries `matchId`: that is the id `GET /api/v1/public/agenda-hits/{matchId}`
takes, and the recent-hits list does not return ids. An empty `hits` array is a
normal answer — no upcoming classified hit for that topic in the public coverage
set right now — and a backend failure returns the same empty page with HTTP 200.
Neither is proof that no body meets.'
tags:
- Agenda hits
parameters:
- name: topicSlug
in: query
description: Normalized topic slug. A request needs this or its legacy alias `topic`; `topicSlug` takes precedence when both are sent.
example: zoning
schema:
type: string
minLength: 1
- name: topic
in: query
deprecated: true
description: Legacy alias for `topicSlug`. Accepted on its own; `topicSlug` takes precedence when both are sent.
schema:
type: string
minLength: 1
- name: cursor
in: query
description: Opaque cursor returned by the previous response. Do not parse or modify it.
schema:
type: string
minLength: 1
maxLength: 200
- name: page
in: query
deprecated: true
description: Legacy one-based page number. Omit it to use cursor pagination.
schema:
type: integer
minimum: 1
maximum: 100
- name: count
in: query
description: Set to true to include `totalCount` in a cursor response. Omit otherwise.
schema:
type: boolean
const: true
responses:
'200':
description: Upcoming agenda hits.
headers:
Cache-Control:
$ref: '#/components/headers/PublicCacheControl'
content:
application/json:
schema:
$ref: '#/components/schemas/UpcomingAgendaHitsEnvelope'
'400':
$ref: '#/components/responses/InvalidParameter'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
security: []
x-auth-class: public
x-required-scope: []
x-release-status: available
x-markdown-url: /api-reference/listUpcomingAgendaHits.md
x-curl-example: curl -X GET 'https://aicdapi.com/api/v1/public/upcoming-agenda-hits?topicSlug=zoning'
x-rate-limit: '120 requests per 60 seconds per client IP, counted separately for each public endpoint and keyed on the first X-Forwarded-For entry. The allowance is not shared between public endpoints: exhausting one leaves the others available. If the limiter''s own store fails, the request is allowed through rather than refused.'
x-caching: public, s-maxage=60, stale-while-revalidate=300 on success. Error responses carry no cache directive.
x-notes:
- A backend query failure is reported as an empty page with HTTP 200, not as an error.
- Unlike the other public endpoints, this one validates its input before applying this endpoint's IP limit.
x-pagination: Cursor pagination by default, using the meeting time, classification time, and hit id as a keyset seek. A deprecated page parameter switches to offset pagination and is limited to page 100 with a fixed page size of 10. The cursor is not bound to the topic filter.
x-retry-after: Sent on 429 only, as whole seconds.
x-errors:
- status: 400
code: invalid_parameter
- status: 429
code: rate_limited
- status: 500
code: internal_error
components:
schemas:
LegacyAgendaHitsPage:
description: A legacy `page` response. `data.page`, `data.pageSize`, and `data.totalCount`.
type: object
additionalProperties: false
required:
- hits
- totalCount
- page
- pageSize
properties:
hits:
type: array
maxItems: 10
items:
$ref: '#/components/schemas/UpcomingAgendaHit'
totalCount:
type: integer
minimum: 0
page:
type: integer
minimum: 1
maximum: 100
pageSize:
type: integer
const: 10
UpcomingAgendaHitsEnvelope:
type: object
additionalProperties: false
required:
- data
- meta
properties:
data:
description: One page of upcoming hits. A cursor request returns `hits` and `nextCursor`, plus `totalCount` when `count=true`; a legacy `page` request returns `hits`, `totalCount`, `page`, and `pageSize`.
type: object
additionalProperties: false
required:
- hits
properties:
hits:
description: Upcoming classified hits for the requested topic in meeting-time order. An empty array means no upcoming classified hit for that topic right now; a backend failure returns the same empty page, so it is not proof that a body has no meetings.
type: array
maxItems: 10
items:
$ref: '#/components/schemas/UpcomingAgendaHit'
nextCursor:
description: Cursor responses only. Null when the last page was returned.
type:
- string
- 'null'
totalCount:
description: Legacy `page` responses, and cursor responses that ask for `count=true`.
type: integer
minimum: 0
page:
description: Legacy `page` responses only.
type: integer
minimum: 1
maximum: 100
pageSize:
description: Legacy `page` responses only.
type: integer
const: 10
oneOf:
- $ref: '#/components/schemas/CursorAgendaHitsPage'
- $ref: '#/components/schemas/LegacyAgendaHitsPage'
meta:
type: object
additionalProperties: true
required:
- pagination
properties:
pagination:
$ref: '#/components/schemas/PaginationMeta'
PublicAgendaHit:
type: object
required:
- jurisdictionSlug
- jurisdictionName
- bodyName
- topicSlug
- meetingAt
- title
- summary
- businessImpact
- evidenceQuote
- sourceUrl
- confidence
- affectedLocation
- proceduralContext
properties:
jurisdictionSlug:
type: string
jurisdictionName:
type: string
bodyName:
type: string
topicSlug:
type: string
meetingAt:
type: string
format: date-time
meetingTimePrecision:
type:
- string
- 'null'
enum:
- date
- time
- unknown
- null
description: Optional precision. date has no supplied clock; time is a verified source clock; unknown is unverified. Absent or null preserves pre-contract legacy precision and does not certify a clock.
title:
type: string
summary:
type: string
businessImpact:
type: string
evidenceQuote:
type: string
sourceUrl:
type: string
format: uri
confidence:
type: integer
minimum: 0
maximum: 100
affectedLocation:
$ref: '#/components/schemas/AffectedLocationValue'
proceduralContext:
$ref: '#/components/schemas/AgendaProceduralContext'
ProblemEnvelope:
type: object
additionalProperties: false
required:
- error
properties:
error:
type: object
additionalProperties: false
required:
- code
- message
properties:
code:
$ref: '#/components/schemas/ErrorCode'
message:
type: string
details: {}
CursorAgendaHitsPage:
description: A cursor page. `data.nextCursor`, and `data.totalCount` when `count=true`.
type: object
additionalProperties: false
required:
- hits
- nextCursor
properties:
hits:
type: array
maxItems: 10
items:
$ref: '#/components/schemas/UpcomingAgendaHit'
nextCursor:
type:
- string
- 'null'
totalCount:
type: integer
minimum: 0
AgendaHitListEnvelope:
type: object
additionalProperties: false
required:
- data
properties:
data:
type: array
maxItems: 3
items:
$ref: '#/components/schemas/PublicAgendaHit'
meta:
$ref: '#/components/schemas/ResponseMeta'
ResponseMeta:
type: object
additionalProperties: true
properties:
pagination:
$ref: '#/components/schemas/PaginationMeta'
AgendaProceduralContext:
type: object
additionalProperties: false
required:
- proceduralStage
- actingBodyRole
- nextBodyName
- nextMeetingDate
- actionKind
- actionUrl
properties:
proceduralStage:
type: string
enum:
- proposed
- scheduled
- recommended
- continued
- approved
- denied
- adopted
- unknown
actingBodyRole:
type: string
enum:
- recommending
- final_decision_maker
- unknown
nextBodyName:
type:
- string
- 'null'
nextMeetingDate:
type:
- string
- 'null'
format: date
actionKind:
type: string
enum:
- public_comment
- registration
- meeting
- staff_contact
- unknown
actionUrl:
type:
- string
- 'null'
format: uri
ErrorCode:
type: string
enum:
- invalid_parameter
- not_found
- unauthorized
- forbidden
- rate_limited
- internal_error
AffectedLocationValue:
type: object
additionalProperties: false
required:
- status
- locations
properties:
status:
type: string
enum:
- stated
- not_stated
locations:
type: array
maxItems: 20
items:
$ref: '#/components/schemas/AffectedLocation'
UpcomingAgendaHit:
description: One classified hit with the id the detail route takes. The fields are the recent-hits fields plus `matchId`, published flat so a reader sees every field without resolving a composed schema.
type: object
additionalProperties: false
required:
- matchId
- jurisdictionSlug
- jurisdictionName
- bodyName
- topicSlug
- meetingAt
- title
- summary
- businessImpact
- evidenceQuote
- sourceUrl
- confidence
- affectedLocation
- proceduralContext
properties:
matchId:
type: string
format: uuid
description: Id for `GET /api/v1/public/agenda-hits/{matchId}`.
jurisdictionSlug:
type: string
jurisdictionName:
type: string
bodyName:
type: string
topicSlug:
type: string
meetingAt:
type: string
format: date-time
meetingTimePrecision:
type:
- string
- 'null'
enum:
- date
- time
- unknown
- null
description: Optional precision. date has no supplied clock; time is a verified source clock; unknown is unverified. Absent or null preserves pre-contract legacy precision and does not certify a clock.
title:
type: string
summary:
type: string
businessImpact:
type: string
evidenceQuote:
type: string
sourceUrl:
type: string
format: uri
confidence:
type: integer
minimum: 0
maximum: 100
affectedLocation:
$ref: '#/components/schemas/AffectedLocationValue'
proceduralContext:
$ref: '#/components/schemas/AgendaProceduralContext'
AffectedLocation:
type: object
additionalProperties: false
required:
- kind
- sourceText
properties:
kind:
type: string
enum:
- exact_address
- parcel_or_case
- zip_code
- council_district
- corridor
- named_development
- neighborhood
- citywide
sourceText:
type: string
maxLength: 160
AgendaHitDetailEnvelope:
type: object
additionalProperties: false
required:
- data
properties:
data:
$ref: '#/components/schemas/UpcomingAgendaHit'
meta:
$ref: '#/components/schemas/ResponseMeta'
PaginationMeta:
type: object
additionalProperties: false
required:
- limit
- returned
- nextCursor
properties:
limit:
type: integer
const: 10
returned:
type: integer
minimum: 0
maximum: 10
nextCursor:
type:
- string
- 'null'
description: Opaque cursor for the next page, or null when traversal is complete.
parameters:
JurisdictionSlugQuery:
name: jurisdictionSlug
in: query
description: Normalized jurisdiction slug. Takes precedence over `jurisdiction`.
schema:
type: string
minLength: 1
ExcludeTopicLegacyQuery:
name: excludeTopic
in: query
deprecated: true
description: Legacy alias for `excludeTopicSlug`.
schema:
type: string
minLength: 1
TopicLegacyQuery:
name: topic
in: query
deprecated: true
description: Legacy alias for `topicSlug`.
schema:
type: string
minLength: 1
TopicSlugQuery:
name: topicSlug
in: query
description: Normalized topic slug. Takes precedence over `topic`.
schema:
type: string
minLength: 1
ExcludeTopicSlugQuery:
name: excludeTopicSlug
in: query
description: Normalized topic slug to omit. Takes precedence over `excludeTopic`.
schema:
type: string
minLength: 1
JurisdictionLegacyQuery:
name: jurisdiction
in: query
deprecated: true
description: Legacy alias for `jurisdictionSlug`.
schema:
type: string
minLength: 1
responses:
RateLimited:
description: More than 120 requests were made by this client IP in 60 seconds.
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/ProblemEnvelope'
examples:
rateLimited:
value:
error:
code: rate_limited
message: Rate limit exceeded
InvalidParameter:
description: A path or query parameter is invalid or unsupported.
content:
application/json:
schema:
$ref: '#/components/schemas/ProblemEnvelope'
examples:
invalidParameter:
value:
error:
code: invalid_parameter
message: Invalid parameter
NotFound:
description: The requested record was not found.
content:
application/json:
schema:
$ref: '#/components/schemas/ProblemEnvelope'
examples:
notFound:
value:
error:
code: not_found
message: Resource not found
InternalError:
description: The request could not be completed because of an internal error.
content:
application/json:
schema:
$ref: '#/components/schemas/ProblemEnvelope'
examples:
internalError:
value:
error:
code: internal_error
message: Internal server error
headers:
PublicCacheControl:
description: Public CDN cache policy for successful public GET responses.
schema:
type: string
const: public, s-maxage=60, stale-while-revalidate=300
RetryAfter:
description: Seconds until this client IP may retry.
required: true
schema:
type: integer
minimum: 1
securitySchemes:
ApiCustomerBearer:
type: http
scheme: bearer
bearerFormat: Opaque service credential
description: Server-only customer key. civic:read covers changes, batch and health. civic:export covers snapshot creation and pages. Keep test and production keys separate.
x-aicd-reference:
human: /api-reference
markdown: /api-reference.md
operationMarkdownTemplate: /api-reference/{operationId}.md
openApi: /api/openapi.json
llmsIndex: /llms.txt
llmsFull: /llms-full.txt
x-aicd-mcp:
transport:
paths:
- /api/v1/mcp
methods:
- POST
protocol: 'Streamable HTTP carrying JSON-RPC 2.0 over HTTPS. The same path serves two protocol eras. A current-era request identifies itself in the body with a params._meta envelope claim (io.modelcontextprotocol/protocolVersion) together with protocolVersion and clientCapabilities, and is answered with one JSON response. A POST without that envelope is treated as a 2025-era request: it is answered per request with no session, and the reply is an SSE stream whose data frames carry the JSON-RPC result. JSON-RPC batch arrays are accepted only on the 2025-era path; a batch containing a current-era element is refused. The server never issues an Mcp-Session-Id, so nothing has to be kept between calls. Only tools are exposed: there are no resources and no prompts. Clients use initialize, tools/list and tools/call; server/discover reports the supported revision.'
headers:
- 'Authorization: Bearer <aicd_... API key> - required on every request. The key must grant mcp:read. A browser session is not accepted here.'
- 'Content-Type: application/json - required. Any other media type is refused with HTTP 415.'
- 'Accept: application/json and text/event-stream - required on a 2025-era request, and both media types must be listed; otherwise HTTP 406. Not checked on a 2026-07-28 request.'
- MCP-Protocol-Version - optional. On a 2025-era request it must be one of 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 or 2024-10-07. On a request carrying the 2026-07-28 envelope claim it must be 2026-07-28 and must equal the body claim.
- Mcp-Method - required on a 2026-07-28 request and must agree with the JSON-RPC method in the body; a disagreement is refused.
- Mcp-Name - required on a 2026-07-28 tools/call and must equal params.name.
- Origin - optional, but when sent its hostname must match the hostname of the request URL, otherwise HTTP 403. A cross-site browser write is refused before the route runs.
- Content-Length - optional, but when sent it must parse as a non-negative integer and must not exceed 65,536; otherwise HTTP 400 or HTTP 413.
- Mcp-Session-Id - never issued and never required. The transport is stateless, so send none.
sizeLimit: 65,536 bytes for the whole request body, enforced both on the declared Content-Length and on the streamed body (HTTP 413 request_too_large). Civic tool results are separately capped at 16,384 bytes; a result that cannot fit is trimmed, and a result that still cannot fit fails with the tool error response_too_large rather than being returned partially. The REST data-report routes have their own 8 KiB body limit, which does not apply to a submission sent inside an MCP request.
auth:
class: bearer_api_key
scopes:
- mcp:read
note: 'Send an opaque AICD API key as `Authorization: Bearer aicd_...`. The same valid key serves the records API and MCP, and every stored key carries the MCP read scope, including tools/list. The two report tools ask for no report permission and never use the paid civic executor or take an extra payment, and the account still needs current paid access. The tool list itself is static: all five tools are always listed, and the difference between accounts is per call, not per list - a civic tool call is billed or refused according to the account''s billing channel (pay-as-you-go settlement, a live subscription period, or an operator channel that rejects payment metadata). Authentication happens before the MCP handler runs and can fail with HTTP 401 unauthorized, HTTP 403 forbidden or a billing code, or HTTP 503 authentication_unavailable; a handler failure returns HTTP 503 mcp_unavailable. Every route response carries Cache-Control: no-store. The separate route /api/mcp is a closed legacy stub that always returns HTTP 403 mcp_prelaunch with no credential check and is not an MCP surface; it is not an alias of /api/v1/mcp.'
rateLimit: 'On the metered path, a message from an account bound to a subscription that is not a `tools/call` consumes two per-account transport buckets: a minute bucket (60-second window, default 120 requests) and a burst bucket (1-second window, default 30 requests), at the account''s own allowances, which the account console and the public usage limits help show. On exhaustion the request fails with HTTP 429 rate_limited and a Retry-After header carrying whole seconds until the current fixed window ends, minimum 1. A civic `tools/call` is not counted here: the paid quota path charges it. A message from an account on another billing channel is not counted here either. An allowlisted metadata message (`initialize`, `tools/list`, `ping`, `server/discover`, `notifications/initialized`, `notifications/cancelled`) and one of the two report tools take neither bucket: they share one admission instead, a valid key with mcp:read and then a fail-closed per-client-IP report edge limit that refuses with 429 rate_limited and a Retry-After, or with 503 when it cannot be checked. For a report that admission is abuse control only, because the report still needs the account''s current paid access. Report counters surface as tool errors with retryAfterSeconds inside the payload instead of HTTP 429: 5 submissions per account-minute, 30 status reads per account-minute, 20 new reports per account-day, and 20 unresolved reports.'
releaseStatus: limited
tools:
# --- truncated at 32 KB (81 KB total) ---
# Full source: https://artifacts.apis.io/aicdapi-com/openapi/aicdapi-com-agenda-hits-api-openapi.yml