Skip to main content
This work was completed during my time at the Stellar Development Foundation in 2022–2023.
Stellar’s API documentation kept falling out of sync with their code. I was brought in to read the codebase, work with the engineering team, and create the OpenAPI spec files that now feed directly into their doc generator — so when the spec changes, the website updates automatically.

50+ parameters

Shared parameter definitions, defined once and referenced across every endpoint.

30+ schemas

Reusable schema objects covering accounts, transactions, ledgers, and more.

40+ endpoints

Full coverage across both the Horizon API and the Anchor Platform.

30+ examples

Concrete request and response examples wired directly into endpoint definitions.

Context

Stellar is a decentralized payment network built for fast, low-cost cross-border transactions. Horizon is Stellar’s REST API — the primary interface for querying accounts, submitting transactions, and streaming ledger data. The Anchor Platform handles the bridge between Stellar and traditional finance, managing KYC, deposits, withdrawals, and compliance. Both APIs serve developers building on Stellar. Both needed documentation that could scale.

The approach

There was no existing spec to work from — everything had to be derived.
1

Read the codebase

The starting point was understanding what the APIs actually did, not what the old docs claimed. That meant reading source code, tracing request handlers, and mapping response shapes directly.
2

Review existing documentation

The existing docs were a useful signal — not for accuracy, but for identifying gaps, inconsistencies, and areas where engineers had clearly stopped maintaining them.
3

Interview senior engineers

Codebase reading can only go so far. I sat down with engineers to clarify intent, surface edge cases, and document behavior that had never been written down.
4

Design a modular file structure

Rather than a single monolithic spec file, I designed a structure where components are defined once and referenced everywhere using $ref. Parameters, schemas, and examples all live in dedicated files.
5

Write and validate the spec

With structure in place, I wrote the spec incrementally — parameters first, then schemas, then endpoints — validating against the OpenAPI 3.0 standard at each stage.

File structure

The spec is split across purpose-specific files. Nothing is defined more than once.
openapi/
├── horizon/
│   ├── components/
│   │   ├── endpoints/
│   │   │   ├── accounts.yml
│   │   │   ├── transactions.yml
│   │   │   ├── operations.yml
│   │   │   └── ...
│   │   ├── parameters.yml
│   │   ├── schemas/
│   │   │   ├── accountSchema.yml
│   │   │   ├── transactionSchema.yml
│   │   │   └── ...
│   │   └── examples/
│   │       └── responses/
│   └── main.yml
└── anchor-platform/
    ├── schemas.yaml
    └── bundled-platform.yaml
The key insight is reusability through $ref. Instead of repeating parameter definitions across dozens of endpoints, define them once in parameters.yml and reference them everywhere. Change one definition, update every endpoint that uses it.

How it works

Shared parameters

Pagination and ordering parameters are defined once in parameters.yml and shared across all list endpoints.
parameters.yml
components:
  parameters:
    AccountIDParam:
      name: account_id
      in: path
      required: true
      description: This account's public key encoded in a base32 string representation.
      schema:
        type: string
        example: GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ

    CursorParam:
      name: cursor
      in: query
      required: false
      description: A number that points to a specific location in a collection of responses.
      schema:
        type: integer
        example: 6606617478959105

    LimitParam:
      name: limit
      in: query
      required: false
      description: The maximum number of records returned. Defaults to 10, max 200.
      schema:
        type: integer
        example: 10

    OrderParam:
      name: order
      in: query
      required: false
      description: Sort order. Options include asc (ascending) or desc (descending).
      schema:
        type: string
        enum:
          - asc
          - desc

Endpoint definitions

Endpoints compose shared components via $ref rather than repeating definitions inline.
endpoints/accounts.yml
paths:
  /accounts/{account_id}:
    get:
      tags:
        - Accounts
      summary: Retrieve an Account
      description: The single account endpoint provides information on a specific account.
      operationId: RetrieveAnAccount
      parameters:
        - $ref: '../parameters.yml#/components/parameters/AccountIDParam'
      responses:
        '200':
          description: Returns details about an account.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "../schemas/linksSchema.yml#/components/schemas/Links"
                  - $ref: "../schemas/accountSchema.yml#/components/schemas/Account"
              examples:
                RetrieveAnAccount:
                  $ref: "../examples/responses/Accounts/RetrieveAnAccount.yml"
For list endpoints that support pagination, the parameters are wired in via $ref:
endpoints/accounts.yml (list endpoint)
  /accounts/{account_id}/transactions:
    get:
      tags:
        - Accounts
      summary: Retrieve an Account's Transactions
      operationId: GetTransactionsByAccountId
      parameters:
        - $ref: '../parameters.yml#/components/parameters/AccountIDParam'
        - $ref: '../parameters.yml#/components/parameters/CursorParam'
        - $ref: '../parameters.yml#/components/parameters/OrderParam'
        - $ref: '../parameters.yml#/components/parameters/LimitParam'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "../schemas/linksSchema.yml#/components/schemas/Links"
                  - $ref: "../schemas/transactionSchema.yml#/components/schemas/Transaction"

The outcome

The specifications now power the Stellar API documentation, auto-generated through Docusaurus. The full spec lives in the stellar-docs repo on GitHub.
PropertyResult
Single source of truthThe spec is the contract — docs and implementation stay in sync
Update once, propagate everywhereChange CursorParam and every endpoint using it reflects the change
Docs that can’t driftGenerated from the spec, so always in sync with what the API actually does
Reduced maintenance burdenAdding a new endpoint means composing existing components, not rewriting definitions

Beyond documentation

A formal OpenAPI definition isn’t just documentation — it’s infrastructure.

SDK generation

Auto-generate client libraries in Python, JavaScript, Go, and other languages directly from the spec.

Mock servers

Spin up fake APIs for frontend development before the backend is ready.

Contract testing

Validate that the live implementation matches what the spec promises.

Postman collections

Auto-import all endpoints for manual testing without writing collection entries by hand.

Changelog diffing

Compare spec versions to see exactly what changed between releases.

Multi-team alignment

A shared spec gives frontend, backend, and docs teams a single reference to work from.

Build docs developers (and LLMs) love