# Vault Media Upload via Onlymonster API

**What this doc is:** the upload process — the sequence of calls, the branching points, and the operational concerns (polling, retries, idempotency).

**What this doc is not:** a field reference. For request/response schemas, exact validation rules, error codes, and field-level constraints, see the [OpenAPI reference](https://docs.onlymonster.ai/integrations-and-api/openapi). This document does not duplicate the OpenAPI reference and will not be kept in sync with field-level changes.

***

## Overview

The upload flow lets an OnlyFans creator upload a media file to S3 (via presigned URLs), then export it to the OnlyFans vault as a post or message attachment. `Onlymonster API` orchestrates the upload but never sees the file bytes — clients PUT directly to S3.

The flow spans **six Onlymonster API endpoints** plus a direct client→S3 step, all rooted at:

```
/api/v0/accounts/{account_id}/vault/medias/uploads
```

| Method & Path             | Purpose                                                     |
| ------------------------- | ----------------------------------------------------------- |
| `GET /`                   | List existing uploads                                       |
| `POST /fans/verify`       | Resolve a fan link → `fan_id` (optional, before export)     |
| `POST /start`             | Reserve S3 key + return presigned URL(s)                    |
| *(client → S3)*           | PUT the file bytes                                          |
| `POST /finish`            | Finalize a multipart upload (multipart only)                |
| `POST /export`            | Queue the upload for processing & vault export              |
| `POST /{upload_id}/retry` | Re-run the export pipeline (from `uploaded` or `processed`) |

The optional `/fans/verify` step exists as a preflight: it lets the UI surface a bad fan link to the user immediately, before committing them to a full upload-and-export cycle that would have failed at the end.

Restrictions: only the `onlyfans` platform is supported. Requests against accounts on other platforms are rejected.

## Process Flow

The diagram below shows the happy-path lifecycle. Optional steps are noted inline.

```mermaid
sequenceDiagram
    autonumber
    participant C as Client
    participant A as Onlymonster API
    participant S as S3
    participant W as Webhook Receiver

    note over C,A: Optional — list existing uploads
    C-->>A: GET /uploads
    A-->>C: items, total

    note over C,A: Optional — resolve fan link
    C-->>A: POST /uploads/fans/verify
    A-->>C: fan_id

    C->>A: POST /uploads/start
    A-->>C: single-PUT OR multipart variant

    alt single PUT
        C->>S: PUT put_url (file body)
        S-->>C: 200 + ETag header
    else multipart
        loop for each part
            C->>S: PUT parts[i].put_url (chunk)
            S-->>C: 200 + ETag header
        end
        C->>A: POST /uploads/finish
        A-->>C: combined e_tag
    end

    C->>A: POST /uploads/export
    A-->>C: upload record (status=uploaded)

    alt Webhooks (Preferred)
        loop async status transitions
            A-->>W: POST webhook event (status updates)
        end
    else Polling
        loop poll until terminal status
            C->>A: GET /uploads
            A-->>C: items[]
        end
    end
```

> **Assumption:** the threshold at which `/start` returns the multipart variant instead of the single-PUT variant is a server-side decision and is not exposed through the API. Treat the response shape — not the file size — as the trigger for which upload strategy to use.

***

### Step 1 — List uploads (optional)

```bash
curl ".../accounts/$ACCOUNT_ID/vault/medias/uploads"
```

Use this to discover existing uploads (e.g. on app start, or to poll status after `/export`).

The endpoint supports an `expired` query flag — when `true`, the response includes uploads whose presigned URLs lapsed before `/finish` or `/export`. This is the recovery hook for the URL-expiry edge case (see Edge Cases).

***

### Step 2 — Verify fan link (optional)

```bash
curl -X POST ".../uploads/fans/verify" -d '{ "fan_link": "..." }'
```

Translates an OnlyFans fan profile URL into the `fan_id` you pass as `export_fan_id` in Step 6. Calling this *before* `/export` lets the UI catch a malformed or unknown fan link upfront, instead of letting the user complete a long upload only to fail at the export step.

***

### Step 3 — Start upload

```bash
curl -X POST ".../uploads/start" -d '{ "name": "...", "size": ..., "content_type": "..." }'
```

Reserves an S3 object key and returns presigned URL(s). **The response has two distinct shapes** — single-PUT or multipart — and the client must inspect which fields are present and branch accordingly. This branch is the core decision point of the flow.

#### Variant A — single PUT

```json
{
  "key": "...",
  "get_url": "https://....s3....amazonaws.com/...",
  "put_url": "https://....s3....amazonaws.com/..."
}
```

**Detect:** response has `put_url`. **Action:** issue a single `PUT` to `put_url` with the full file body.

#### Variant B — multipart

```json
{
  "key": "...",
  "get_url": "https://....s3....amazonaws.com/...",
  "upload_id": "...",
  "parts": [
    { "id": 1, "put_url": "...partNumber=1..." },
    { "id": 2, "put_url": "...partNumber=2..." }
  ]
}
```

**Detect:** response has `upload_id` and `parts[]`. **Action:** PUT each chunk to its `parts[i].put_url`, capture each ETag from the response header, then proceed to Step 5 (`/finish`).

***

### Step 4 — Upload bytes to S3

This step does **not** involve `Onlymonster API`. Bytes flow client → S3 directly.

#### Capturing the ETag

S3 returns the object's ETag in the `ETag:` HTTP response header on every successful `PUT`. You will need it as `e_tag` in Steps 5 and 6.

* **`curl`:** add `-D -` to dump headers, then grep:

  ```bash
  curl -X PUT "$PUT_URL" --data-binary @file.mp4 -H "content-type: video/mp4" -D - -o /dev/null \
    | awk -v IGNORECASE=1 '/^etag:/ { gsub(/[\r\n]/, "", $2); print $2 }'
  ```
* **AWS SDK (Node, Python, Go, etc.):** the `PutObject` / `UploadPart` response object exposes the ETag directly (e.g. `response.ETag` in the JS SDK).
* **`fetch` / browser:** read `response.headers.get('ETag')` (CORS-allowed by default for S3 presigned URLs that include `ETag` in `Access-Control-Expose-Headers`).

#### 4a — Single PUT

PUT the full file body to `put_url`, capture the `ETag:` header value for Step 6 as `e_tag`. **No** `/finish` call is required for this branch.

#### 4b — Multipart

PUT each chunk to its corresponding `parts[i].put_url`. PUTs may run concurrently. Collect each ETag and assemble `[{ part: i+1, e_tag: <etag> }, ...]` for Step 5. Order in the `parts` array sent to `/finish` does not have to match upload order, but each `part` number must match the corresponding `parts[].id` from the start response.

> **Assumption:** the presigned URL TTL is set server-side and is not exposed through the API. If a PUT fails after a long delay (e.g. expired signature), restart from Step 3. Use the `expired=true` query in Step 1 to surface in-flight uploads whose presigned URLs lapsed.

***

### Step 5 — Finish multipart upload

```bash
curl -X POST ".../uploads/finish" -d '{ "key": "...", "upload_id": "...", "parts": [...] }'
```

**Skip this step for the single-PUT variant** — there is no multipart to assemble.

On success the response is `{ "e_tag": "<combined-etag>" }`. Use that combined ETag as the `e_tag` field in Step 6.

***

### Step 6 — Export to vault

```bash
curl -X POST ".../uploads/export" -d '{ "key": "...", "e_tag": "...", "get_url": "...", "export_type": "...", ... }'
```

Queues the uploaded object for processing and posting/messaging on OnlyFans. On success, returns the upload record with `status=uploaded` and `media_id=null`. See the [OpenAPI reference](https://docs.onlymonster.ai/integrations-and-api/openapi) for the full request/response schema.

Process-decisive notes:

* `e_tag`: from Step 4a (single-PUT ETag) **or** Step 5 (multipart combined ETag).
* `get_url`: pass through the presigned GET URL returned by `/start`. Substituting any other URL will be rejected.
* `export_type`: `"post"` or `"message"`. `message` exports targeting a specific fan require `export_fan_id` (resolve via Step 2).

> **Assumption:** the lifecycle from `uploaded` → `processing` → `processed` → `exporting` → `exported` is driven by background processing. The transition triggers and timing are not directly observable; treat status as advisory and poll until terminal.

***

### Step 7 — Track status to terminal

Two ways to observe status transitions: subscribe to webhooks (preferred for production) or poll. See [webhook event types](https://docs.onlymonster.ai/integrations-and-api/webhook-integration?q=api#event-types) for the webhook flow. The rest of this section covers polling.

#### Polling (In case using webhooks is not possible)

Re-call `GET /uploads` (Step 1) and find the upload by `id`. The `status` field drives client behaviour.

#### Polling cadence

Endpoints are limited to **3 RPS per token**, so do not poll in a tight loop. Recommended: **exponential backoff starting at 2 seconds, doubling up to a 30-second cap**, with optional jitter. In typical conditions, polling for up to several minutes is sufficient. Clients should set a reasonable upper bound on total polling time and surface a timeout to the user if no terminal status is reached. On `429`, fall back to the same backoff.

```
delay_ms(attempt) = min(2_000 * 2^attempt, 30_000) + random(0..500)
```

Stop polling as soon as `status` is `exported`, `failed`, or `unprocessable`.

#### Status state machine

```mermaid
stateDiagram-v2
    [*] --> uploaded: POST /export

    uploaded --> processing
    processing --> processed
    processed --> exporting
    exporting --> exported

    uploaded --> failed
    processing --> failed
    processed --> failed
    exporting --> failed

    uploaded --> unprocessable
    processing --> unprocessable

    uploaded --> processing: POST /uploads/{id}/retry
    processed --> exporting: POST /uploads/{id}/retry

    exported --> [*]
    failed --> [*]
    unprocessable --> [*]
```

| Status          | Meaning                                                    | Client action                                 |
| --------------- | ---------------------------------------------------------- | --------------------------------------------- |
| `uploaded`      | Queued; awaiting processing                                | Keep polling                                  |
| `processing`    | The file is being processed in the background              | Keep polling                                  |
| `processed`     | File ready, awaiting OnlyFans export                       | Keep polling                                  |
| `exporting`     | Pushing to OnlyFans                                        | Keep polling                                  |
| `exported`      | **Terminal happy.** `media_id` is now set                  | Stop polling; surface success                 |
| `failed`        | **Terminal.** Background processing error                  | Surface to user; start a new upload if needed |
| `unprocessable` | **Terminal unrecoverable.** File is corrupt or unsupported | Surface to user; do not retry                 |

#### Decision flow

```mermaid
flowchart TD
    A[POST /uploads/start] --> B{has put_url?}
    B -- yes --> C[PUT bytes to put_url]
    B -- no --> D[PUT each part]
    C --> F[POST /uploads/export]
    D --> E[POST /uploads/finish]
    E --> F
    F --> G{Track Status}
    G -- Poll --> P[GET /uploads]
    G -- Webhook --> W[Receive Event]
    P --> H{status}
    W --> H
    H -- uploaded/processing/processed/exporting --> G
    H -- exported --> I[done — media_id set]
    H -- failed/unprocessable --> K[abort, surface to user]
```

***

### Step 8 — Retry export

```bash
curl -X POST ".../uploads/$UPLOAD_ID/retry"
```

Re-runs the export pipeline for an upload that has already passed the bytes-uploaded stage. Retry is accepted only when `status` is `uploaded` or `processed`. Calling retry on any other status returns an error.

On success, the upload is re-queued for processing (from `uploaded`) or re-queued for OnlyFans export (from `processed`). Resume polling as in Step 7.

Practical use: the typical trigger for retry is a stuck or stalled upload — the client sees `uploaded` or `processed` for far longer than expected and chooses to nudge the pipeline. Retry is **not** a recovery mechanism for `failed` or `unprocessable` — those are terminal. A `failed` upload cannot be recovered through retry; the client must start a new upload from `/start`.

***

## Edge Cases & Recommendations

* **5 GiB hard cap.** Larger files are rejected at `/start`. Most large videos will fall on the multipart path — design the client to handle both response variants of `/start` regardless of file size.
* **Presigned URL expiry.** If a PUT to S3 fails with a signature/expiry error, abandon the upload and restart from `/start`. The `expired=true` query on the list endpoint surfaces uploads whose URLs lapsed before `/finish` or `/export`.
* **Concurrent multipart parts.** PUTs to `parts[].put_url` can run in parallel. The `parts` array sent to `/finish` does not have to match upload order, but each `part` integer must match the `id` from `/start`.
* **`get_url` provenance.** Pass through the same `get_url` returned by `/start` to `/export`. Substituting any other URL (CDN, proxy, separately signed URL) will be rejected.
* **Network interruption mid single-PUT.** No resume protocol — restart from `/start`.
* **Idempotency on network timeout.** None of `/start`, `/finish`, or `/export` is safe to blindly retry after a network timeout — each call may have already created a fresh upload, completed a multipart, or queued a duplicate vault entry. On timeout, **re-fetch state via `GET /uploads`** and reconcile by `id` / `key` before deciding whether to retry. `/uploads/{id}/retry` is itself idempotent — repeating the call on the same `uploaded` or `processed` upload has no additional effect beyond the first.

  > **Assumption:** the idempotency claim for `/uploads/{id}/retry` is based on its observable behaviour as a "nudge" the worker, not on a verified implementation guarantee.
* **Fan-link verification.** Verify before `/export` so a bad fan link surfaces upfront rather than after a wasted upload-and-export cycle.

***

## Related

* [OpenAPI reference](https://docs.onlymonster.ai/integrations-and-api/openapi) — full schema definitions, validation rules, error codes, and try-it-out console.
* [Webhook event types](https://docs.onlymonster.ai/integrations-and-api/webhook-integration?q=api#event-types) — alternative to polling; subscribe to upload status transitions.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.onlymonster.ai/integrations-and-api/vault-media-upload-via-onlymonster-api.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
