Skip to content

Components / Sections

API Reference

An API reference in one section: a sidebar of groups and their operations; for each operation its method and path, what it does, its path, query and header parameters, the fields of its body (nested fields under a disclosure, as deep as they go) and its responses; and beside it, sticky on a wide screen, the request in every language with a copy button and the response examples by status. The language picked once switches every sample on the page through radios and :has(), with no script; a small script remembers it for the next page. A type with more to say (a format, a range) carries it in a Tooltip, and the sidebar marks the operation in view where the browser has scroll-target-group. It takes operations already shaped, the way a template turns an OpenAPI document into them at build.

Live demo

API reference

Every request takes your key as a bearer token. Sample API, made up for this demo.

Base URLhttps://api.sitename.example/v1

Language

Emails

Send an email, then follow it until it lands.

Send an email

POST/emails

Queues one email. It is sent within seconds; follow it with GET /emails/{id} or a webhook.

Body

  • fromstringFormat: email.required

    An address on a domain you have verified.

  • tostringFormat: email.required

  • subjectstringAt most 998 characters.required

  • textstringoptional

    The plain-text body.

  • tagsarray of objectsoptional

    Labels you can filter the logs by.

    Fields 2
    • namestringoptional

    • valuestringoptional

Responses

202
Queued.
422
A field is missing or the domain is not verified.

Request

curl -X POST "https://api.sitename.example/v1/emails" \
  -H "Authorization: Bearer $SITENAME_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@sitename.example",
    "to": "ada@example.com",
    "subject": "Your receipt",
    "text": "string",
    "tags": [
      {
        "name": "kind",
        "value": "receipt"
      }
    ]
  }'
const res = await fetch('https://api.sitename.example/v1/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SITENAME_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'hello@sitename.example',
    to: 'ada@example.com',
    subject: 'Your receipt',
    text: 'string',
    tags: [
      {
        name: 'kind',
        value: 'receipt',
      },
    ],
  }),
});
const data = await res.json();
import os
import requests

res = requests.post(
    "https://api.sitename.example/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['SITENAME_KEY']}"},
    json={
        "from": "hello@sitename.example",
        "to": "ada@example.com",
        "subject": "Your receipt",
        "text": "string",
        "tags": [
            {
                "name": "kind",
                "value": "receipt",
            },
        ],
    },
)
print(res.json())
{
  "id": "em_7Hq2kD9x",
  "status": "queued",
  "created_at": "2026-10-09T08:14:22Z"
}
{
  "error": "domain_not_verified",
  "message": "Verify sitename.example before sending from it."
}

Get an email

GET/emails/{id}

Path parameters

  • idstringrequired

    The id the send returned.

Responses

200
The email and where it is.

Request

curl "https://api.sitename.example/v1/emails/em_7Hq2kD9x" \
  -H "Authorization: Bearer $SITENAME_KEY"
const res = await fetch('https://api.sitename.example/v1/emails/em_7Hq2kD9x', {
  headers: {
    Authorization: `Bearer ${process.env.SITENAME_KEY}`,
  },
});
const data = await res.json();
import os
import requests

res = requests.get(
    "https://api.sitename.example/v1/emails/em_7Hq2kD9x",
    headers={"Authorization": f"Bearer {os.environ['SITENAME_KEY']}"},
)
print(res.json())
{
  "id": "em_7Hq2kD9x",
  "status": "queued",
  "created_at": "2026-01-01T00:00:00Z"
}

Domains

The domains you send from, and whether their records check out.

List domains

GET/domains

Query parameters

  • limitinteger1 to 100.optional

    How many to return.

    Default20

Responses

200
The domains, newest first.

Request

curl "https://api.sitename.example/v1/domains" \
  -H "Authorization: Bearer $SITENAME_KEY"
const res = await fetch('https://api.sitename.example/v1/domains', {
  headers: {
    Authorization: `Bearer ${process.env.SITENAME_KEY}`,
  },
});
const data = await res.json();
import os
import requests

res = requests.get(
    "https://api.sitename.example/v1/domains",
    headers={"Authorization": f"Bearer {os.environ['SITENAME_KEY']}"},
)
print(res.json())
{
  "data": [
    {
      "name": "sitename.example",
      "status": "verified"
    }
  ],
  "has_more": false
}

Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.

Measured

JavaScript of its own

404 B raw

285 B gzip.

Uses CopyButton and the shared runtime (1.9 kB raw, once per site). With those included: 2.9 kB raw.

CSS 28.6 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · newly available

Chrome 131 · Firefox 143 · Safari 18.4; needs has, details-content. Elsewhere: without ::details-content the sidebar on a wide screen opens with a click; without :has() the first language shows.

Install Pro

The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:

terminal
npx shadcn@latest add @moonarc-pro/api-reference
src/pages/index.astro
---
import ApiReference from '@/components/moonarc/pro/ApiReference.astro';
import { reference } from '../lib/openapi';   // your OpenAPI document, shaped at build
---
<ApiReference
  title="API reference"
  intro="Every request takes your key as a bearer token."
  base="https://api.example.com/v1"
  languages={[{ label: 'cURL', lang: 'sh' }, { label: 'JavaScript', lang: 'js' }, { label: 'Python', lang: 'python' }]}
  groups={reference.groups}
/>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: code-tabs, copy-button.

Props

PropTypeDefaultDescription
groups{ id; title; description?; operations }[]noneThe groups, each with its operations: { id, method, path, summary, description?, deprecated?, params?, body?, samples, responses }. params are fields with in (path, query, header); a field is { name, type, required?, description?, detail?, enum?, default?, children? }; samples are { label, code, lang? }, one per language; responses are { status, description?, example? }. Descriptions are paragraphs split by a blank line, with `code` in backticks.
languages{ label: string; lang?: string }[]noneOne to six languages every sample comes in, in this order; the first is picked at first. lang is the Shiki language id. An operation without a sample for one of them stops the build naming it.
basestringnoneThe base URL, printed in the introduction.
titlestringnoneThe page's heading.
introstringnoneParagraphs under the heading, split by a blank line; `code` in backticks.
idstring'ma-apiref'Prefix of the radio names and the localStorage key the language is kept under; unique on the page.
wordsPartial<Words>noneEvery word the reference prints around your content: Language, Endpoints, the parameter headings, Body, Responses, required and optional, Deprecated, One of, Default, Fields, Request.
level1 | 21Heading level of the title; groups are one below and operations two below.

Theming and overrides

The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.

Classes

.ma-apiref.ma-apiref__aside.ma-apiref__base.ma-apiref__block.ma-apiref__default.ma-apiref__dep.ma-apiref__docs.ma-apiref__endpoint.ma-apiref__enum.ma-apiref__fdesc.ma-apiref__fhead.ma-apiref__field.ma-apiref__fields.ma-apiref__fname.ma-apiref__ftype.ma-apiref__glink.ma-apiref__group.ma-apiref__groups.ma-apiref__gtitle.ma-apiref__inner.ma-apiref__intro.ma-apiref__label.ma-apiref__lang.ma-apiref__langlabel.ma-apiref__langs.ma-apiref__legend.ma-apiref__m.ma-apiref__main.ma-apiref__menu.ma-apiref__menulabel.ma-apiref__nav.ma-apiref__nested.ma-apiref__olink.ma-apiref__op.ma-apiref__ops.ma-apiref__otitle.ma-apiref__p.ma-apiref__panel.ma-apiref__path.ma-apiref__pcopy.ma-apiref__phead.ma-apiref__plabel.ma-apiref__req.ma-apiref__responses.ma-apiref__sample.ma-apiref__status.ma-apiref__statuses.ma-apiref__title.ma-apiref__var

Slots

none

Reduced motion

Nothing moves but the language picker's colour, which changes at once.

With ClientRouter

The remembering script is bound through the shared runtime and reads the language again on every page.

Why it is built this way

Replaces: Redoc · Swagger UI · Scalar API reference · Protocol (Tailwind Plus) API reference pages. See the migration table.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc

A code block with tabs (for example npm, pnpm, bun) and a copy button per panel.

every browserclick

Send the API keyA secret key from your dashboard, sent as a bearer token. with every request, and pass an Idempotency-KeyAny unique string. A retry with the same key returns the first response instead of acting twice. on writes.

Copy the request ID

hover, focus or tap

A short tip anchored to a word, a code or a button: it shows after a moment on hover and at once on focus or a tap, stays while the pointer moves onto it, flips when there is no room, and Escape hides it.

newly · Chrome 125 · Firefox 147 · Safari 26hoverclick

A documentation layout: a sticky sidebar with the current page marked, a reading-progress hairline bound to the scroll, the header and content revealed in two beats, and previous/next links.

Chrome 115 · not Firefox · Safari 26scroll

A quickstart as numbered steps down a rail that fills as the reader moves through them, each number lighting as its step reaches the middle of the screen.

Chrome 115 · not Firefox · Safari 26scroll

Email API

Email that arrives

One request sends it. Webhooks tell you when it lands, and every message keeps its own log.

Sample figures, made up for this demo

POST /v1/emails HTTP/1.1
Host: api.sitename.example
Authorization: Bearer sk_live_••••
Content-Type: application/json

{
  "from": "hello@sitename.example",
  "to": "ada@example.com",
  "subject": "Your receipt",
  "template": "receipt"
}

202 Accepted84 msfra1

{
  "id": "em_7Hq2kD9x",
  "status": "queued",
  "created_at": "2026-10-09T08:14:22Z"
}
GET /v1/emails/em_7Hq2kD9x HTTP/1.1
Host: api.sitename.example
Authorization: Bearer sk_live_••••

200 OK31 msfra1

{
  "id": "em_7Hq2kD9x",
  "status": "delivered",
  "opened": false,
  "events": [
    {
      "type": "delivered",
      "at": "2026-10-09T08:14:25Z"
    }
  ]
}
GET /v1/domains HTTP/1.1
Host: api.sitename.example
Authorization: Bearer sk_live_••••

200 OK27 msiad1

{
  "data": [
    {
      "name": "sitename.example",
      "status": "verified",
      "region": "eu-west"
    }
  ],
  "has_more": false
}

A hero for an HTTP API: the headline beside a console where a request is written line by line, a line runs while it is sent, the status lands with the time it took and the region that answered, and the JSON response streams in under it.

newly · Chrome 105 · Firefox 121 · Safari 15.4loadclick

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown