> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nohead.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Use the API from TypeScript, Python or Ruby, with pagination, safe retries, uploads and webhook verification.

The SDKs cover every operation an API key can call, and every endpoint in the [API reference](https://docs.nohead.io/api-reference/overview.md) shows how to call it from each. They follow the API's conventions, so what you read about the API holds for them too.

| Language | Package | Needs | Source |
| - | - | - | - |
| [TypeScript and JavaScript](https://docs.nohead.io/sdks/typescript.md) | `@nohead/sdk` | Node 22+, Bun, Deno, Workers | [nohead-io/nohead-typescript](https://github.com/nohead-io/nohead-typescript) |
| [Python](https://docs.nohead.io/sdks/python.md) (sync and async) | `nohead` | Python 3.11+ | [nohead-io/nohead-python](https://github.com/nohead-io/nohead-python) |
| [Ruby](https://docs.nohead.io/sdks/ruby.md) | `nohead` | Ruby 3.3+ | [nohead-io/nohead-ruby](https://github.com/nohead-io/nohead-ruby) |

This page covers what the three share. Each language's page has its install, configuration and what is particular to it.

Need an SDK in another language? [Let us know](mailto:hello@nohead.io) which one. Meanwhile, anything can call the API over HTTP, or generate a client from [`/openapi.json`](https://docs.nohead.io/api-reference/overview).

## Install and connect

```bash TypeScript theme={null}
npm install @nohead/sdk
```

```bash Python theme={null}
pip install nohead
```

```ruby Ruby theme={null}
gem "nohead" # in your Gemfile
```

Create a client with a project API key, made under **API keys** in the web app. Each reads `NOHEAD_API_KEY` from the environment when you don't pass one; none reads the key that `nohead login` saves for the [CLI](https://docs.nohead.io/cli/overview.md), so give your app a key of its own. Writing content, drafts, uploads and webhooks needs a key with **Published records only** off and the write scopes (`records:write`, `assets:write`, …); new keys are published-only by default. API keys belong to a project, so you never pass a project ID, and you can name collections by slug.

```ts TypeScript theme={null}
import { Nohead } from "@nohead/sdk"

const nohead = new Nohead() // or new Nohead({ apiKey })
```

```python Python theme={null}
from nohead import Nohead

nohead = Nohead()  # or Nohead(api_key=...)
```

```ruby Ruby theme={null}
require "nohead"

nohead = Nohead::Client.new # or Nohead::Client.new(api_key: ...)
```

> [!WARNING]
>
> API keys are secrets: use the SDKs on servers, never in a browser.

## Read content

List methods give you a page, which you can also loop over: the SDK fetches the following pages as you go. Filters are the API's: a value to equal, or operators such as `{ exists: true }`.

```ts TypeScript theme={null}
for await (const post of nohead.records.list("posts", {
  filter: { published_at: { exists: true } },
  sort: "-published_at",
  expand: ["author"],
})) {
  console.log(post.data.title, post.expanded?.author)
}

// One page at a time
const page = await nohead.records.list("posts", { limit: 100 })
page.data // the records
page.meta.next_cursor
```

```python Python theme={null}
for post in nohead.records.list(
    "posts",
    filter={"published_at": {"exists": True}},
    sort="-published_at",
    expand=["author"],
):
    print(post.data["title"])

# One page at a time
page = nohead.records.list("posts", limit=100)
page.data  # the records
page.meta.next_cursor
```

```ruby Ruby theme={null}
nohead.records.list("posts", filter: { published_at: { exists: true } },
                             sort: "-published_at", expand: ["author"]).each do |post|
  puts post.data["title"]
end

# One page at a time
page = nohead.records.list("posts", limit: 100)
page.data # the records
page.meta.next_cursor
```

In TypeScript, describe a collection's fields and its records are [typed](https://docs.nohead.io/sdks/typescript.md#typed-record-data): `nohead.records.get<Post>(id)` makes `post.data.title` a string. In Python, results are Pydantic [models](https://docs.nohead.io/sdks/python.md#models), and a record's values are in its `data` dict. In Ruby, read [fields](https://docs.nohead.io/sdks/ruby.md#results) as methods or with `[]` (`post.data["title"]`); timestamps are Times.

## Write content

```ts TypeScript theme={null}
const post = await nohead.records.create("posts", {
  data: { title: "Hello", author: "rec_01J9ZQ3F8X" },
})
await nohead.records.update(post.id, { data: { title: "Hello again" } })
await nohead.records.publish(post.id)
```

```python Python theme={null}
post = nohead.records.create("posts", data={"title": "Hello", "author": "rec_01J9ZQ3F8X"})
nohead.records.update(post.id, data={"title": "Hello again"})
nohead.records.publish(post.id)
```

```ruby Ruby theme={null}
post = nohead.records.create("posts", data: { title: "Hello", author: "rec_01J9ZQ3F8X" })
nohead.records.update(post.id, data: { title: "Hello again" })
nohead.records.publish(post.id)
```

Every write is versioned and attributed to the key. Add a note to history with `changeNote` (TypeScript) or `change_note` (Python, Ruby).

## Review changes before they go live

A draft holds changes to a record until they are merged into it, now or at a set time, while the record keeps its current content. A record has one draft at most, and a merge changes content only: a live record stays live. Agents can propose changes this way for a person to review and merge.

```ts TypeScript theme={null}
const draft = await nohead.drafts.create(id, { data: { title: "Hello, revised" } })
const preview = await nohead.drafts.get(draft.id) // data as the record would read
await nohead.drafts.merge(draft.id) // or drafts.schedule(draft.id, { merge_at })
```

```python Python theme={null}
draft = nohead.drafts.create(record_id, data={"title": "Hello, revised"})
preview = nohead.drafts.get(draft.id)  # data as the record would read
nohead.drafts.merge(draft.id)  # or drafts.schedule(draft.id, merge_at=...)
```

```ruby Ruby theme={null}
draft = nohead.drafts.create(id, data: { title: "Hello, revised" })
preview = nohead.drafts.get(draft.id) # data as the record would read
nohead.drafts.merge(draft.id) # or drafts.schedule(draft.id, merge_at: ...)
```

## Retries and idempotency

Failed requests are retried twice by default:

* connection errors and timeouts
* `429 rate_limited`, waiting for `Retry-After` when it is at most a minute (a longer wait raises `RateLimitError` at once)
* `500`, `502`, `503` and `504`
* `409` with detail code `in_progress`: the same write, still running

Each attempt times out after 60 seconds. Set `timeout` and `maxRetries` (`max_retries`) on the client to change them. TypeScript also takes them for one call, and in Python `with_options` returns a client with other settings. [Errors and retries](https://docs.nohead.io/guides/errors-and-retries.md) covers what each error means.

Every write carries an `Idempotency-Key` that stays the same across its retries, so a retry after a lost response never writes twice.

To make a write safe across retries of your own (a job that may run twice), pass the key yourself:

```ts TypeScript theme={null}
await nohead.records.create("posts", { data }, { idempotencyKey: `import-${row.id}` })
```

```python Python theme={null}
nohead.records.create("posts", data=data, idempotency_key=f"import-{row.id}")
```

```ruby Ruby theme={null}
nohead.records.create("posts", data: data, idempotency_key: "import-#{row.id}")
```

## Concurrent edits

Pass the record you read as `ifMatch` (`if_match`). If someone changed it since, the API refuses the write with `412` and nothing is written.

```ts TypeScript theme={null}
import { PreconditionFailedError } from "@nohead/sdk"

const post = await nohead.records.get(id)
try {
  await nohead.records.update(id, { data: { title } }, { ifMatch: post })
} catch (error) {
  if (error instanceof PreconditionFailedError) {
    // changed since (now at error.currentRevision): reload, and merge or ask
  } else throw error
}
```

```python Python theme={null}
from nohead import PreconditionFailedError

post = nohead.records.get(record_id)
try:
    nohead.records.update(record_id, data={"title": title}, if_match=post)
except PreconditionFailedError as error:
    ...  # changed since (now at error.current_revision): reload, and merge or ask
```

```ruby Ruby theme={null}
post = nohead.records.get(id)
begin
  nohead.records.update(id, data: { title: title }, if_match: post)
rescue Nohead::PreconditionFailedError => e
  # changed since (now at e.current_revision): reload, and merge or ask
end
```

## Errors

Each error `type` of the API is an error class: `ValidationError` (422), `NotFoundError` (404), `RateLimitError` (429) and so on, all subclasses of `APIError`. They carry the `status`, `type`, `message`, the request ID and the field-level `details`.

```ts TypeScript theme={null}
import { ValidationError } from "@nohead/sdk"

try {
  await nohead.records.create("posts", { data: {} })
} catch (error) {
  if (error instanceof ValidationError) console.log(error.details)
  else throw error
}
```

```python Python theme={null}
from nohead import ValidationError

try:
    nohead.records.create("posts", data={})
except ValidationError as error:
    for detail in error.details:
        print(detail.field, detail.code, detail.message)
```

```ruby Ruby theme={null}
begin
  nohead.records.create("posts", data: {})
rescue Nohead::ValidationError => e
  e.details.each { |detail| puts "#{detail.field}: #{detail.message}" }
end
```

## Upload files

Files go straight to storage, never through the API. `assets.upload` does the whole flow in one call: it creates the upload, sends the bytes to storage, completes the upload (which checks the file) and returns the `ready` asset. Reference the asset from an asset field.

```ts TypeScript theme={null}
import { openAsBlob } from "node:fs"
import { imageUrl } from "@nohead/sdk/images"

// Typed by the filename (cover.jpg: image/jpeg) when the Blob has no type.
const asset = await nohead.assets.upload(await openAsBlob("cover.jpg"), {
  filename: "cover.jpg",
})
await nohead.records.update(id, { data: { cover: asset.id } })
const url = imageUrl(asset, { width: 1200, format: "webp" }) // no request
```

```python Python theme={null}
from nohead.images import image_url

asset = nohead.assets.upload("cover.jpg")  # a path, bytes or a binary file
nohead.records.update(record_id, data={"cover": asset.id})
url = image_url(asset, width=1200, format="webp")  # no request
```

```ruby Ruby theme={null}
asset = nohead.assets.upload("cover.jpg") # a path or an IO
nohead.records.update(id, data: { cover: asset.id })
url = Nohead::Images.url(asset, width: 1200, format: "webp") # no request
```

[Images](https://docs.nohead.io/guides/images.md) covers image URLs and the loaders for Next.js, Astro and Nuxt. To search, see [API search](https://docs.nohead.io/guides/api-search.md).

## Verify webhooks

Webhook requests are signed. `unwrap` (`unwrapWebhook` in TypeScript) checks the signature and the timestamp and returns the event; it raises (or throws) when either is off. Pass the raw body, before any JSON parsing.

```ts TypeScript theme={null}
import { unwrapWebhook } from "@nohead/sdk/webhooks"

export async function POST(request: Request) {
  const event = await unwrapWebhook(await request.text(), request.headers, {
    secret: process.env.NOHEAD_WEBHOOK_SECRET!,
  })
  if (event.type === "record.published") {
    // e.g. revalidate the record's pages
  }
  return new Response(null, { status: 204 })
}
```

```python Python theme={null}
import os

from nohead.webhooks import unwrap

event = unwrap(request.get_data(), request.headers, secret=os.environ["NOHEAD_WEBHOOK_SECRET"])
if event.type == "record.published" and event.record:
    ...  # e.g. rebuild the record's pages
```

```ruby Ruby theme={null}
event = Nohead::Webhooks.unwrap(request.raw_post, request.headers,
                                secret: ENV.fetch("NOHEAD_WEBHOOK_SECRET"))
if event.type == "record.published"
  # e.g. rebuild the record's pages
end
```

The same event can arrive twice: deduplicate by the `webhook-id` header. [Webhooks](https://docs.nohead.io/guides/webhooks.md) covers the event types, the payload and rotating the secret.

## Versions

The SDKs have their own version numbers, separate from the API's `/v1`. Until 1.0, the SDKs, the CLI and the API may change in ways that break your code: pin a version, and read each release's **Breaking** entries before upgrading. From 1.0, the API only adds within `/v1`, so an older SDK keeps working; anything an SDK depends on is deprecated at least 12 months before it is removed, and the SDKs warn when they call something deprecated. Each SDK's releases on GitHub (linked from the table at the top) list what changed in each version.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.