Authoring

# OpenAPI bridge

If you already maintain an OpenAPI spec, don't restate it. Point c.openapi at the file and one line renders the whole endpoint — method, path, fields, errors, request tabs, and response — derived from the spec.

## Zero hand-restatement

One operation call → a full endpoint reference, composed from the kit.

An API-reference page normally restates every method, path, field, and response that your `openapi.yaml` already describes — and a field rename means editing both. The **OpenAPI bridge** closes that gap: a single `operation "operationId"` reads the spec and renders the whole endpoint through the same kit the [API reference](https://docs-kit.zoolutions.llc/docs/api) page composes by hand — a `DocsUI::Endpoint` badge, `FieldTable`s, an `ErrorTable`, a `RequestExample` (or your `x-codeSamples`), and a `JsonResponse`.

Because it's composed from the kit, the `.md` twin, `llms.txt`, search, and MCP surfaces all derive from it for free.

## Point c.openapi at your spec

A path (.json ⇒ JSON, else YAML) or an already-parsed Hash. nil by default.

```ruby
DocsKit.configure do |c|
  c.openapi = Rails.root.join("openapi.yaml")
end
```

Then, in any page's `#content`, render an operation by its `operationId`:

```ruby
def content
  operation "createInvoice"
end
```

> **Tip:** The document is memoized and reloads when the file's mtime changes, so editing `openapi.yaml` in development shows up without a server restart.

## What one operation expands to

Each part of the operation maps to one kit component.

| Spec source | Renders as |
| --- | --- |
| operationId + summary + method/path | the Section title + a `DocsUI::Endpoint` badge |
| description | Markdown prose |
| parameters (query/path) | a `DocsUI::FieldTable` |
| requestBody schema ($ref, allOf, nested) | a `FieldTable` (nested names dotted: `customer.id`) |
| 4xx / 5xx responses | a `DocsUI::ErrorTable` (error `type` from a response example) |
| x-codeSamples | `DocsUI::Example` tabs (a lone sample → a plain `Code`) |
| no code samples | a generated `DocsUI::RequestExample` |
| first 2xx example | a `DocsUI::JsonResponse` |

## Live: createInvoice

Rendered from docs/openapi.yaml — request body, errors, tabs, and response.

The section below is produced by a single call — `operation "createInvoice"`. Nothing here is hand-written:

## Create an invoice

`POST` `/v1/invoices`

Creates a new invoice for a customer and returns it.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | — | The unique invoice identifier. |
| `amount` | integer | ✓ | Amount due, in the smallest currency unit (e.g. cents). |
| `currency` | usd \| eur \| gbp | ✓ | Three-letter ISO currency code. |
| `customer` | object | — | The customer the invoice is billed to. |
| `customer.id` | string | — |  |
| `customer.email` | string | — |  |
| `created` | integer | — | Unix timestamp of creation. |

| Scenario | Status | Type |
| --- | --- | --- |
| Missing or invalid API key. | 401 | `authentication_error` |
| The invoice failed validation. | 422 | `validation_error` |

cURL

JavaScript

Ruby

Python

```console
curl -X POST 'https://api.example.com/v1/invoices' \
  -H "Content-Type: application/json" \
  -d '{
  "id": "inv_1a2b3c",
  "amount": 4200,
  "currency": "usd",
  "customer": {
    "id": "cus_9f8e7d",
    "email": "ada@example.com"
  },
  "created": 1720000000
}'
```

```javascript
const response = await fetch("https://api.example.com/v1/invoices", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({"id":"inv_1a2b3c","amount":4200,"currency":"usd","customer":{"id":"cus_9f8e7d","email":"ada@example.com"},"created":1720000000}),
});
const data = await response.json();
```

```ruby
require "net/http"
require "json"

uri = URI("https://api.example.com/v1/invoices")
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request.body = {
  "id": "inv_1a2b3c",
  "amount": 4200,
  "currency": "usd",
  "customer": {
    "id": "cus_9f8e7d",
    "email": "ada@example.com"
  },
  "created": 1720000000
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
```

```python
import requests

response = requests.post(
    "https://api.example.com/v1/invoices",
    headers={"Content-Type": "application/json"}, json={
  "id": "inv_1a2b3c",
  "amount": 4200,
  "currency": "usd",
  "customer": {
    "id": "cus_9f8e7d",
    "email": "ada@example.com"
  },
  "created": 1720000000
},
)
data = response.json()
```

```json
{
  "id": "inv_1a2b3c",
  "amount": 4200,
  "currency": "usd",
  "customer": {
    "id": "cus_9f8e7d",
    "email": "ada@example.com"
  },
  "created": 1720000000
}
```

## Live: getInvoice (with x-codeSamples)

This operation ships x-codeSamples, so they replace the generated tabs.

`getInvoice` carries `x-codeSamples` (a Ruby SDK tab and a CLI tab) in the spec, so the bridge renders those instead of the generic curl/JS/Ruby/Python snippets — and substitutes the `id` parameter's example into the path:

## Retrieve an invoice

`GET` `/v1/invoices/{id}`

Fetches a single invoice by its ID.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | ✓ | The invoice ID. |

| Scenario | Status | Type |
| --- | --- | --- |
| Missing or invalid API key. | 401 | `authentication_error` |

Ruby SDK

CLI

```ruby
Billing::Invoice.retrieve("inv_1a2b3c")
```

```shell
billing invoices retrieve inv_1a2b3c
```

```json
{
  "id": "inv_1a2b3c",
  "amount": 4200,
  "currency": "usd",
  "customer": {
    "id": "cus_9f8e7d",
    "email": "ada@example.com"
  },
  "created": 1720000000
}
```

## Live: listInvoices (filter the tabs)

A GET with query parameters; here we keep just the curl and Ruby tabs.

Pass `clients:` to filter and order the generated client tabs — here `clients: %i[curl ruby]`:

## List invoices

`GET` `/v1/invoices`

Returns a paginated list of invoices, most recent first.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | — | Maximum number of invoices to return (1–100). |
| `status` | open \| paid \| void | — | Filter by invoice status. |

cURL

Ruby

```console
curl -X GET 'https://api.example.com/v1/invoices?limit=20'
```

```ruby
require "net/http"
require "json"

uri = URI("https://api.example.com/v1/invoices?limit=20")
request = Net::HTTP::Get.new(uri)
request["Content-Type"] = "application/json"


response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
```

```json
[
  {
    "id": "inv_1a2b3c",
    "amount": 4200,
    "currency": "usd",
    "customer": {
      "id": "cus_9f8e7d",
      "email": "ada@example.com"
    },
    "created": 1720000000
  }
]
```

## Lookup, prose, and errors

By id or verb+path; append prose with a block; unknown ids raise.

```ruby
operation :delete, "/v1/invoices/{id}"            # verb + path (id-less specs)
operation "createInvoice", clients: %i[curl ruby] # only these tabs
operation "createInvoice" do |op|                 # append prose in the section
  op.md("Idempotency keys are honored for 24 hours.")
end
```

> **Warning:** An unknown `operationId` raises `DocsKit::OpenApi::OperationNotFound` (naming the available ids); an external/remote `$ref` raises `DocsKit::OpenApi::UnsupportedRef`. Authoring or validating the spec itself is out of scope — bring your own.