Context
The REST API currently has no machine-readable contract. The request/response shapes exist in Go structs and in hand-written TypeScript Zod schemas in the SDK, and these are already starting to drift slightly. As the API surface grows (pagination cursor, GraphQL, admin endpoints), maintaining consistency manually becomes error-prone.
An OpenAPI 3.1 specification solves this by making the /v1/events API contract the single source of truth. From the spec, we can:
- Auto-generate the SDK TypeScript types (replacing the hand-written Zod schemas)
- Generate a Postman collection for developers
- Auto-generate the REST docs section (instead of hand-writing it)
- Run spec linting in CI to catch schema drift before it reaches users
- Enable third-party client generators for Python, Rust, etc.
What needs to be built
api/openapi.yaml
OpenAPI 3.1 document at the repo root api/ directory. The spec must cover:
GET /v1/events
/v1/events:
get:
summary: List Soroban events
operationId: listEvents
parameters:
- name: contract_id
in: query
schema: { type: string }
description: Filter by Soroban contract address (strkey format)
- name: topic_0
in: query
schema: { type: string }
- name: topic_1
in: query
schema: { type: string }
- name: from_ledger
in: query
schema: { type: integer, minimum: 0 }
- name: to_ledger
in: query
schema: { type: integer, minimum: 0 }
- name: network
in: query
schema: { type: string, enum: [testnet, mainnet] }
default: testnet
- name: first
in: query
schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
- name: after
in: query
schema: { type: string }
description: Opaque pagination cursor from previous response
responses:
'200':
content:
application/json:
schema: { : '#/components/schemas/EventListResponse' }
'400': { : '#/components/responses/BadRequest' }
'401': { : '#/components/responses/Unauthorized' }
'503': { : '#/components/responses/ServiceUnavailable' }
GET /v1/events/{id}, GET /v1/health, GET /v1/stats/contracts (#73), GET /v1/stats/indexer (#74), and the admin endpoints (#63) should all be covered.
Reusable schemas (components/schemas):
SorobanEvent — the core event object
EventListResponse — { events: SorobanEvent[], next_cursor: string | null, total: number }
ErrorResponse — { error: { code: string, message: string, request_id: string } }
HealthResponse — all fields from the health endpoint
Security scheme:
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
security:
- ApiKeyAuth: []
SDK type generation
Add a script to sdk/typescript/package.json:
"generate:types": "openapi-typescript ../../api/openapi.yaml -o src/api-types.gen.ts"
The generated file is committed to the repo. The existing hand-written Zod schemas in the SDK are refactored to z.infer<typeof generatedType> where the generated types replace manual schema writing.
CI linting
Add make lint-openapi using Spectral (@stoplight/spectral-cli):
spectral lint api/openapi.yaml --ruleset @stoplight/spectral-oas
This catches: missing operationId, missing description on parameters, response schemas with no content, etc.
Validation in Go API
Use kin-openapi or oapi-codegen to optionally validate requests against the spec at runtime. At minimum, add a CI check that the Go API's actual behavior matches the spec (run the spec through a request validator against the integration test suite).
Acceptance criteria
Dependencies
Context
The REST API currently has no machine-readable contract. The request/response shapes exist in Go structs and in hand-written TypeScript Zod schemas in the SDK, and these are already starting to drift slightly. As the API surface grows (pagination cursor, GraphQL, admin endpoints), maintaining consistency manually becomes error-prone.
An OpenAPI 3.1 specification solves this by making the
/v1/eventsAPI contract the single source of truth. From the spec, we can:What needs to be built
api/openapi.yamlOpenAPI 3.1 document at the repo root
api/directory. The spec must cover:GET /v1/eventsGET /v1/events/{id},GET /v1/health,GET /v1/stats/contracts(#73),GET /v1/stats/indexer(#74), and the admin endpoints (#63) should all be covered.Reusable schemas (
components/schemas):SorobanEvent— the core event objectEventListResponse—{ events: SorobanEvent[], next_cursor: string | null, total: number }ErrorResponse—{ error: { code: string, message: string, request_id: string } }HealthResponse— all fields from the health endpointSecurity scheme:
SDK type generation
Add a script to
sdk/typescript/package.json:The generated file is committed to the repo. The existing hand-written Zod schemas in the SDK are refactored to
z.infer<typeof generatedType>where the generated types replace manual schema writing.CI linting
Add
make lint-openapiusing Spectral (@stoplight/spectral-cli):This catches: missing
operationId, missingdescriptionon parameters, response schemas with no content, etc.Validation in Go API
Use
kin-openapioroapi-codegento optionally validate requests against the spec at runtime. At minimum, add a CI check that the Go API's actual behavior matches the spec (run the spec through a request validator against the integration test suite).Acceptance criteria
api/openapi.yamlvalidates withspectral lint(zero errors)npm run generate:typesin the SDK produces types that compile withtsc --noEmitspectral lintand fails the build on spec errorsapi-types.gen.tsis up-to-date (CI fails if it's stale — run generate + git diff check)Dependencies
networkquery param)