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

# Calling the API

> Arguments, typed models, whole lists, ids and uploads.

Every route in the API reference is a method on one of the client's accessors: `pages()`,
`blogPosts()`, `faqItems()`, `fetchskiMedia()` and so on. A call is one line.

```ts theme={"dark"}
import { createSiteClient } from '@fetchski/attract-cms-sdk';

const site = createSiteClient();

const blog = await site.blogPosts().getBlog({ id });
const entries = await site.formSubmissions().listFormSubmissions({ formId }); // needs forms:read
const images = await site.fetchskiMedia().listMediaImages({ q: 'beach' }); // needs images:read
```

## Arguments

> [card] One object per call
>
> Path, query and body fields side by side: `listBlogs({ page: 2, perPage: 20 })`. Leave it out when
> nothing is required.

> [card] A body is fields too
>
> `updateTestimonial({ id, quote: 'Great service' })` sends `{"quote":"Great service"}`.

> [card] null is not sent
>
> `null` or `undefined` leaves a field out, so a `PATCH` changes only what you name.

> [card] Dates and files
>
> A date takes a `Date` or a string; a file a `File` named with its extension.

## Models

Every answer is a typed model, decoded for you.

```ts theme={"dark"}
const blog = await site.blogPosts().getBlog({ id });

blog.title; // string: always sent, so never optional
blog.publishedAt; // Date
blog.id; // the wire's _id
```

> [accordion] What each type becomes
>
> A field the site always sends is never optional. Date-times and Unix times are `Date`s. Lists are
> readonly arrays; maps readonly records. A field the site may send as `null` is typed `| null`.

> [accordion] Enums stay open
>
> An enum is its known values or any string a newer site adds: `'draft' | 'published' | (string & {})`.
> Compare against its constant, `AposMode.Draft`.

> [accordion] Answered, not required to write
>
> Some models are both an answer and a body. A field the site always sends (a testimonial's `slug`,
> `visibility`, `archived`) is required when it answers, yet stays optional when you write:
> `updateTestimonial({ id, quote: '…' })` leaves the others alone.

> [accordion] Every answer is checked
>
> Against its type, before your code sees it. A value of the wrong type rejects with a
> `SiteRequestFailed` naming the field, never coerced.

## Whole lists, missing documents, jobs

```ts Every page theme={"dark"}
for await (const item of site.faqItems().listFaqItemsAll({ perPage: 50 })) {
  console.log(item.title);
}
```

```ts Null for a 404 theme={"dark"}
const maybe = await site.blogPosts().findBlog({ id }); // Blog | null
```

```ts A job theme={"dark"}
const job = await site.blogPosts().archiveBlogs({ ids }); // a PendingJob: job.jobId
```

* **`…All()`** fetches a page at a time as you iterate: see [Paging](https://docs.fetchski-attract.com/typescript/paging.md).
* **`find…()`** sits beside each `get…()`: only a 404 becomes `null`.
* **A job's `wait()`** needs the job route, which only the management key reaches: on an API key it
  throws `SiteConfigurationError`.

## What an API key reaches

> [tab] Content
>
> Every content type, with that type's scopes: `content:<type>:read` lists and reads the published
> documents, `:write` creates and changes drafts (and reads them), `:publish` takes them live, and
> `:delete` deletes. Pages, the media libraries and Site settings (`global:*`) included. Each
> method's reference page names its scope under **Access**; [Required scopes](https://docs.fetchski-attract.com/scopes.md) lists them
> all.

> [tab] Feature routes
>
> Importing blog posts (with their images and videos), recording conversions, and reading
> `fetchskiMedia()` images and videos, form submissions, conversions and analytics: each with the
> scope its page names.

> [tab] Drafts and publishing
>
> On a type with drafts, a create or update in published mode (the default) also takes the document
> live, so the key needs the type's publish scope too. Pass `aposMode: 'draft'` to change only the
> draft, then publish it with a key that may. A type without drafts (redirects, photo feeds, the
> media folders) needs only its write scope.

> [tab] Never
>
> People, roles, API keys and the site's connections: those stay with a person signed in to the
> CMS. A call without the scope answers 403, and the message names it.

## Ids

A document's id names its locale and mode: `<document id>:<locale>:<mode>`.

**1. A create answers the id of its mode**

Published by default (`…:en:published`); `aposMode: 'draft'` makes only a draft (`…:en:draft`).

**2. Reads take either id**

Or the bare document id.

**3. Deleting takes the draft id**

It removes both copies. The draft id is the published id with the last part changed.

> [!WARNING]
>
> An empty or blank id is refused before anything is sent, as a `RequestNotSent`: it would otherwise call
> another route (an empty `getBlog` id would read the whole list).

## Uploads

Upload a `File` named with its extension: the site reads a file's type from its name.

```ts theme={"dark"}
const photo = new File([bytes], 'photo.jpg', { type: 'image/jpeg' });
```

A bare `Blob`, or a name without an extension, is refused before it leaves (`RequestNotSent`).

> [!NOTE]
>
> `uploadAttachment()` needs the write scope of a media type (`content:images:write`,
> `content:files:write` or `content:videos:write`).

An upload is signed like any write: a key with a signing secret signs it by its SHA-256, so the client
reads the file once to hash it before it is sent.

> [!NOTE]
>
> **A signed upload signs its SHA-256.** A multipart body isn't signed byte for byte: the request declares the
> hex SHA-256 of the exact body in `X-Fetchski-Content-SHA256` and signs that digest in the body's place. The
> site checks the signature before it reads the upload, then the bytes against the digest, and stores nothing
> that doesn't match. The SDKs do this for you; [Signing requests](https://docs.fetchski-attract.com/signing.md#uploads) shows it by hand.

## Timeouts

> [card] Slow routes set their own
>
> Each FetchskiAI route, and the blog import with images, carries its own timeout: the site's
> 600-second cap per AI call plus 15 seconds. You never pass one per call.

> [card] The larger one wins
>
> The client's timeout (15 seconds by default) covers every other route. Raise it, and every call gets
> at least that long.

## A route without a method

Every route the site documents has a method, but `request()` calls any route exactly as the methods
do: the key, its signature, retries, the timeout and the same errors.

```ts theme={"dark"}
const created = await site.request<{ _id: string }>('POST', '/my-route', {
  body: { title: 'Hello' }
});
```

**advanced: the engine underneath**

The SDK runs on a generated engine, `@fetchski/attract-cms-sdk/engine`. `site.api(BlogPostsApi)`
reaches it directly (wire-named models, `…Raw()` twins, a `RequestInit` such as `{ signal }` last),
signed and retried like every call, for the rare case the methods don't cover.

> [card] [Next: errors](https://docs.fetchski-attract.com/typescript/errors.md)
>
> One class per failure, and what the SDK retries for you.


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