> ## 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, answers, whole lists, jobs, uploads and ids.

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

```php theme={"dark"}
use Fetchski\AttractCmsLaravel\Facades\Attract;

$post = Attract::blogPosts()->getBlog(id: $id);
$entries = Attract::formSubmissions()->listFormSubmissions(formId: $formId); // needs forms:read
$images = Attract::fetchskiMedia()->listMediaImages(q: 'beach');            // needs images:read
```

> [tab] The default site
>
> `Attract::blogPosts()` reaches the site `FETCHSKI_ATTRACT_SITE` names.

> [tab] Another site
>
> ```php theme={"dark"}
> $site = Attract::site('globex');
> $faqs = $site->faqItems()->listFaqItems(perPage: 20);
> ```

> [tab] Injected
>
> The facade reaches `AttractManager`; inject it where you prefer:
>
> ```php theme={"dark"}
> use Fetchski\AttractCmsLaravel\AttractManager;
>
> public function __construct(private readonly AttractManager $attract) {}
>
> $post = $this->attract->blogPosts()->getBlog(id: $id);
> ```

> [!TIP]
>
> Print every method of an accessor with its route:
>
> ```bash Terminal theme={"dark"}
> php artisan attract:list blogPosts
> ```
>
> Try one on the real site:
>
> ```bash Terminal theme={"dark"}
> php artisan attract:call blogPosts.listBlogs perPage=5
> ```

## Arguments

> [card] Named and typed
>
> `listBlogs(perPage: 20, search: 'launch')`. Your editor completes them.

> [card] A body is arguments too
>
> `updateTestimonial(id: $id, quote: 'Great service', rating: 5)`: no model to build.

> [card] null is not sent
>
> A `PATCH` changes only what you name, so nothing else is touched.

> [card] Laravel's own values
>
> Carbon or a string for a date, a PHP enum or its value, `$request->file()` for a file.

## Answers

Answers are readonly objects with properties, never getters.

```php theme={"dark"}
$post = Attract::blogPosts()->getBlog(id: $id);

$post->title;          // string: always sent, so never null
$post->publishedAt;    // CarbonImmutable
$post->category;       // ?Collection
$post->toArray();      // the site's JSON
```

> [accordion] What each type becomes
>
> A field the site always sends is non-nullable. Lists are `Collection`s. Date-times and Unix times are
> `CarbonImmutable`. Nested objects are their own classes, under `Fetchski\AttractCmsLaravel\Data`.

> [accordion] Enums stay open
>
> A known value is a PHP enum case; a value a newer site adds stays its string, so an older SDK never
> breaks on it.

> [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 non-null when it answers, yet stays an optional argument when you write:
> `updateTestimonial(id: $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 is an error naming the field,
> never coerced.

## Whole lists, missing documents, jobs

```php Every page theme={"dark"}
Attract::faqItems()->listFaqItemsAll(perPage: 50)
    ->each(fn ($item) => logger()->info($item->title));
```

```php Null for a 404 theme={"dark"}
$post = Attract::blogPosts()->findBlog(id: $id);   // ?Blog
```

```php Wait for a job theme={"dark"}
$job = Attract::site('acme')->blogPosts()->archiveBlogs(ids: $ids)->wait();
```

* **`…All()`** returns a `LazyCollection` that fetches a page at a time as you iterate: see
  [Paging](https://docs.fetchski-attract.com/laravel/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` naming the job id.

## 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

Pass the file the request brought, as it is. It goes under the name it was uploaded with, as the site
reads a file's type from its extension:

```php theme={"dark"}
$site->attachments()->uploadAttachment(file: $request->file('photo'));
```

> [!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, which means
reading the file twice (hashed, then sent). An `UploadedFile`, a path or an `SplFileObject` all can; a
one-pass stream is sent unsigned, or refused when the key's `api_signing` mode signs writes.

> [!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.

```ini .env theme={"dark"}
FETCHSKI_ATTRACT_TIMEOUT=60   # seconds per attempt, for every route that has no longer one
```

## 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. It returns the decoded JSON.

```php theme={"dark"}
$created = Attract::site()->request('POST', '/my-route', body: ['title' => 'Hello']);
```

**advanced: the engine underneath**

The SDK runs on a generated engine, `fetchski/attract-cms-api-client`. `api()` reaches it directly, with
its own models and getters, for the rare case the methods don't cover:
`Attract::site()->api(TestimonialsApi::class)->updateTestimonial(id: $id, testimonial: new Testimonial([...]))`.

> [card] [Next: working with results](https://docs.fetchski-attract.com/laravel/working-with-results.md)
>
> Ids, counts and the fields you want, in one line.


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