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

# Custom tools reference

> Arguments, results, and usage for the purpose-built custom tools on the Fanvue MCP server, including the image-post upload and creation flow.

Alongside the tools that mirror the [Fanvue API](/docs/api-reference/overview), the MCP server includes **custom tools**, each purpose-built around a complete job and easy to spot by its `custom__` prefix. This page is the reference for every custom tool: the arguments it accepts and the fields it returns. Each tool returns a single JSON object as the text content of its result.

<Note>
  The custom toolset will grow over time as more jobs get their own purpose-built tool.
</Note>

## The image post flow

Publishing an image post takes three steps, two tool calls with an image upload in between:

<Steps>
  <Step title="Start the upload">
    Call `custom__start-image-upload`. It returns a `mediaUuid`, an `uploadId` and a short-lived `uploadUrl` for one image.
  </Step>

  <Step title="Upload the image">
    HTTP `PUT` the raw image bytes (not base64) to `uploadUrl`, with no `Authorization` header. Keep the `ETag` response header, it confirms the upload in the next step.
  </Step>

  <Step title="Create the post">
    Call `custom__create-image-post` with `image: { mediaUuid, uploadId, etag }` and the post fields. The post is created once the image is ready to display.
  </Step>
</Steps>

Run the flow once per image: a paid post with its own teaser image needs two uploads, one for the locked image, one for the free preview.

## `custom__start-image-upload`

Starts an image post (step 1 of 3) by reserving an upload slot for one image and returning everything the upload step needs. Takes no arguments.

Requires the `write:media` scope.

### Result

<ResponseField name="mediaUuid" type="string" required>
  UUID of the new media item. Pass it to `custom__create-image-post`, and use it to reference the media in your library afterwards.
</ResponseField>

<ResponseField name="uploadId" type="string" required>
  Opaque identifier of this upload. Pass it to `custom__create-image-post` exactly as received, don't parse or modify it.
</ResponseField>

<ResponseField name="uploadUrl" type="string" required>
  Short-lived URL to `PUT` the raw image bytes to. If it expires before the upload happens, call `custom__start-image-upload` again for a fresh one, an unused slot is harmless.
</ResponseField>

<ResponseField name="instructions" type="string" required>
  A reminder of the remaining steps, embedded in the result so an assistant can complete the flow without consulting this page.
</ResponseField>

```json Example result theme={null}
{
  "mediaUuid": "b8c47a91-3f2d-4a55-b7e8-1c9d2e4f5a6b",
  "uploadId": "dXBsb2FkLXNlc3Npb24tZXhhbXBsZQ",
  "uploadUrl": "https://storage.example.com/uploads/b8c47a91?signature=…",
  "instructions": "PUT the raw image bytes (not base64) to uploadUrl with no Authorization header, and keep the ETag response header. …"
}
```

## `custom__create-image-post`

Publishes a post carrying the uploaded image (step 3 of 3). It accepts the same post fields as [create a new post](/docs/api-reference/create-a-new-post), text, audience, pricing, scheduling, expiry, collections, plus the identifiers of the uploaded image. The post is only created once the image has finished processing and is ready to display, so a post never reaches the feed with broken media.

Requires the `write:media`, `read:media` and `write:post` scopes.

### Arguments

<ParamField body="image" type="object" required>
  The uploaded image to attach to the post.

  <Expandable title="image fields">
    <ParamField body="mediaUuid" type="string" required>
      `mediaUuid` returned by `custom__start-image-upload`.
    </ParamField>

    <ParamField body="uploadId" type="string" required>
      `uploadId` returned by the same `custom__start-image-upload` call.
    </ParamField>

    <ParamField body="etag" type="string" required>
      `ETag` response header of the HTTP `PUT` that uploaded the image bytes, quotes included or stripped.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="audience" type="string" required>
  Who can view the post: `subscribers` or `followers-and-subscribers`.
</ParamField>

<ParamField body="text" type="string">
  Text content of the post, up to 5,000 characters.
</ParamField>

<ParamField body="price" type="number">
  Price in cents for a pay-to-view post, minimum `300` (\$3.00). Omit for a free post.
</ParamField>

<ParamField body="publishAt" type="string">
  Future date/time to publish the post, in ISO 8601 format with a timezone (for example `2026-07-01T18:00:00Z`). When set, the post is scheduled instead of published immediately.
</ParamField>

<ParamField body="expiresAt" type="string">
  Date/time when the post expires, in ISO 8601 format with a timezone.
</ParamField>

<ParamField body="previewImage" type="object">
  Free teaser image shown to non-subscribers before they unlock a paid post. Upload it through its own `custom__start-image-upload` call; the fields are the same as `image`.

  <Expandable title="previewImage fields">
    <ParamField body="mediaUuid" type="string" required>
      `mediaUuid` returned by `custom__start-image-upload`.
    </ParamField>

    <ParamField body="uploadId" type="string" required>
      `uploadId` returned by the same `custom__start-image-upload` call.
    </ParamField>

    <ParamField body="etag" type="string" required>
      `ETag` response header of the HTTP `PUT` that uploaded the teaser bytes.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="collectionUuids" type="string[]">
  UUIDs of the collections to add the post to.
</ParamField>

### Result

The created post, the same shape as the [create a new post](/docs/api-reference/create-a-new-post) response.

<ResponseField name="uuid" type="string" required>
  Unique identifier of the created post.
</ResponseField>

<ResponseField name="createdAt" type="string" required>
  Date/time when the post was created (ISO 8601 format).
</ResponseField>

<ResponseField name="text" type="string | null" required>
  Text content of the post.
</ResponseField>

<ResponseField name="price" type="number | null" required>
  Price in cents for paid posts.
</ResponseField>

<ResponseField name="mediaPreviewUuid" type="string | null" required>
  UUID of the free preview media, when a `previewImage` was supplied.
</ResponseField>

<ResponseField name="audience" type="string" required>
  Audience the post is visible to.
</ResponseField>

<ResponseField name="publishAt" type="string | null" required>
  Future date/time when the post will be published, for scheduled posts.
</ResponseField>

<ResponseField name="publishedAt" type="string | null" required>
  Date/time when the post was published. `null` for scheduled posts that have not gone live yet.
</ResponseField>

<ResponseField name="expiresAt" type="string | null" required>
  Date/time when the post expires.
</ResponseField>

```json Example result theme={null}
{
  "uuid": "123e4567-e89b-12d3-a456-426614174000",
  "createdAt": "2026-06-12T18:00:00.000Z",
  "text": "Tonight's drop 🔥",
  "price": 999,
  "mediaPreviewUuid": "f1e2d3c4-b5a6-4789-8123-456789abcdef",
  "audience": "subscribers",
  "publishAt": null,
  "publishedAt": "2026-06-12T18:00:00.000Z",
  "expiresAt": null
}
```

### If a call fails

* A failed call never publishes a broken post: if the image could not be processed, no post is created and the error names the media that failed.
* An image that was already uploaded is not lost, the error lists the media UUIDs that remain in your media library, ready to attach to a post.
* The call is safe to retry with the same arguments; a retry picks up where the failed call left off rather than uploading or posting twice.
* If the error reports a rejected upload, check that the bytes were `PUT` to the `uploadUrl` and that `etag` is the `ETag` header from that `PUT`. An expired `uploadUrl` means starting over with `custom__start-image-upload`.
