Skip to content

For developers

Anything the editor does, the API does.

A REST API on one OpenAPI contract, checked in CI, with the CLI, typed SDKs and an MCP server built on it. Filter on any field, expand relations, keep your schema in a file, react to webhooks and serve images by URL, with keys scoped to each job. Here is the same request five ways.

$ curl -G \
  -d "filter[published_at][exists]=true" \
  -H "Authorization: Bearer $NOHEAD_API_KEY" \
  "https://api.nohead.io/v1/collections/posts/records"
  {
    "data": [
      {
        "id": "rec_01M3MSCJ…",
        "data": {
          "title": "Design tokens as content",
          "author": "rec_01M5…"
        }
      }
    ],
    "meta": { "has_more": true, "next_cursor": "…" }
  }

A contract you can build on. No surprises past the first call.

The same rules on every endpoint, so the second resource you integrate takes a tenth of the time of the first.

Stable, prefixed IDs
rec_, col_, key_: sortable, unique, and never a database row number. Slugs and field keys are for humans and stay stable when labels change; a renamed key keeps working for six months.
One error shape
{ error: { type, message, request_id, details } }, with the offending field and a machine-readable code. The request ID is in the server logs too.
Cursor pagination
limit, cursor and sort on every list; meta.next_cursor and has_more tell you when to stop. A cursor is bound to the sort it was issued for.
Filters and expansion
filter[price][lt]=50 with eq, ne, ranges, in and exists on any field, dates that understand today, and expand=author,tags to embed related records and assets without changing the record's shape.
Optimistic concurrency
ETag on reads, If-Match on writes. A stale write gets a 412 instead of overwriting someone's work.
Idempotent writes
Send an Idempotency-Key and a retry within 24 hours returns the first result instead of repeating the change. The CLI does it for you.
Upserts for syncs
Create with "upsert_on": "slug" and the record that already has that slug is updated instead, so a sync can run again without duplicates. Batches change up to 100 records at once.
Rate limits you can see
RateLimit-Limit, -Remaining and -Reset on every response. A 429 carries Retry-After and is neither performed nor metered.
Images by URL
An image's image_url takes its size, crop and format in the URL, with no API call per rendition.
Published-only keys
A key that can only see published records, for the site in production. Scopes such as records:read or schema:write keep every other key as small as its job.

One CLI for people, CI and agents. Scriptable on purpose.

Model a collection in five lines, then change records with a note, a revision check and a dry run. Every command uses the public API, and every change it makes shows up in history as made via the CLI.

# A collection and its fields; each change is kept in its history
$ nohead collections create posts
$ nohead fields add posts title --type text --required
$ nohead fields add posts body --type rich-text
# A relation points at another collection; an asset field holds a file
$ nohead fields add posts author --type relation --target authors
$ nohead fields add posts cover --type asset
# Records start unpublished, at revision 1
$ nohead records create posts --set title="Hello, world"
  Created rec_01M3MSCJ15FNCV670PF2KYKF5M (revision 1, unpublished)
# Publish when it's ready
$ nohead records publish rec_01M3MSCJ…

An output contract scripts can depend on

  • stdout carries only results. With --json, exactly one JSON document: the API's own representation.
  • stderr carries errors and diagnostics, as the API's error envelope when --json is on.
  • Exit codes: 0 success, 1 API error, 2 usage error, 3 API unreachable, 4 unexpected failure.
  • Retries for 429s and transient failures, but never for a write without an idempotency key.
  • Pages: --all follows every cursor, so a script never writes a pagination loop.
nohead.schema.yaml
# nohead.schema.yaml, reviewed like any other code
collections:
  posts:
    fields:
      title: { type: text, required: true }
      summary: long_text
      body: rich_text
      author: { type: relation, collection: authors }
      cover: asset
      tags: { type: text, multiple: true }
terminalin CI
$ nohead schema diff
  + field posts.summary (long_text)
  ~ field posts.title: required true
  ! field posts.reading_time: text -> integer (field migration)

  posts.reading_time migration preview (conditional): 17 records, 16 converted, 1 invalid
$ nohead schema push --yes --allow-migrations --clear-invalid
  Applied 3 changes.

Schema as code. Reviewed, then applied.

Pull the schema into a file, review it like any other change, and let CI apply it.

  • schema diff shows the plan: additions, changes and, for anything that rewrites values, a preview of how many records convert, are cleared or cannot be converted.
  • schema push --yes applies additions and edits. It never deletes a collection or a field, and it refuses field migrations unless you pass --allow-migrations.
  • Tightening a field (making it required, shortening a maximum length) is checked the same way: if records would break it, the push stops before changing anything.
  • schema diff --exit-code fails when the project has drifted from the file: a one-line CI check.

SDK for your language. Or any HTTP client.

Typed clients on the published contract, smoke-tested against the real API in CI: authentication, records, pagination, idempotent retries, uploads and error handling.

TypeScript

const nohead = new Nohead()
nohead.records.list("posts")

Python

nohead = Nohead()
nohead.records.list("posts")

Ruby

nohead = Nohead::Client.new
nohead.records.list("posts")

You tell us

Need another language? Let us know which.

The contract itself is at /openapi.json; generate a client for anything else from it.

Webhooks, signed and retried. React instead of polling.

Subscribe a URL to the events you care about. Each delivery is signed with the Standard Webhooks scheme, retried with backoff for over a day, and logged with every attempt.

  • record.created
  • record.updated
  • record.published
  • record.unpublished
  • record.deleted
  • record.*
  • draft.*
  • field.*
  • asset.*

Payloads carry the change's actor and via, so an integration that writes back can skip its own changes. Deliveries are at least once; deduplicate on the webhook-id header. Respond within ten seconds; anything else is retried after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 10 h.

import { unwrapWebhook } from "@nohead/sdk/webhooks"
import { revalidateTag } from "next/cache"

export async function POST(request: Request) {
  // Checks the signature and the timestamp, or throws
  const event = await unwrapWebhook(
    await request.text(), // the raw body, as signed
    request.headers,
    { secret: process.env.NOHEAD_WEBHOOK_SECRET! }
  )

  // Skip the changes this integration made itself
  if (event.actor.id === process.env.NOHEAD_KEY_ID) {
    return new Response(null, { status: 204 })
  }

  if (event.type === "record.published" && event.data.record) {
    revalidateTag(event.data.record.collection, "max")
  }
  return new Response(null, { status: 204 })
}

Building with agents?

The CLI, the MCP server and /llms.txt give coding agents, chat apps and your own agents the same API, with keys scoped to their job and every change on the record.

Nohead for agents

Questions, answered.

About the API, search, images, schema as code, webhooks and the SDKs.

Ask us directly








Read the contract, then build.

Create a project key with the scopes your job needs, point the CLI or an SDK at it, and everything you do is in history from the first request.