DevelopersDocumentation

FUG Scripts & Playback API Reference

Motion script storage, autonomous playback control, and live status

This reference describes every route implemented by the FUG scripts and playback API groups, including request and response data, access scope, operational behavior, and common errors.

Related resources:


1. Production and Authentication

Environment Base URL
Production https://fug-prd.feelme.com

The OpenAPI schema is available at /api/openapi.json. Swagger UI is available at /api/docs.

Every public scripts and playback route requires a Device Connection Key:

Authorization: DCK <device_connection_key>

Requests with a JSON body also require:

Content-Type: application/json

The authenticated DCK establishes two scopes:

A deployment without a configured store does not register either API group. All scripts and playback paths return 404 Not Found in that configuration.


2. Endpoint Summary

Method Path Auth Success Purpose
POST /api/scripts DCK 201 Upload script content inline.
POST /api/scripts/from-url DCK 201 Fetch and store a script from a public URL.
GET /api/scripts DCK 200 List scripts owned by the caller.
GET /api/scripts/{script_id} DCK 200 Read metadata for any existing script.
DELETE /api/scripts/{script_id} DCK owner 200 Delete an owned script.
POST /api/playback/play DCK 202 Start, resume, or switch playback.
POST /api/playback/pause DCK 202 Pause playback.
POST /api/playback/seek DCK 202 Move to an absolute media position.
POST /api/playback/sync DCK 202 Request drift correction.
POST /api/playback/speed DCK 202 Change playback speed.
POST /api/playback/offset DCK 202 Set an absolute timing offset.
POST /api/playback/stop DCK 202 Stop playback.
GET /api/playback/status DCK 200 stream Stream playback and device status over SSE.

3. Script Formats and Processing

Supported formats

source_format Input structure
funscript JSON object containing an actions array of { "at": milliseconds, "pos": 0..100 }.
autoblow_csv Delimited rows in time_ms,position order. Comma, semicolon, and tab delimiters are detected. An optional header row is accepted.

Format detection follows this precedence:

  1. Explicit source_format from the request.
  2. A JSON, Funscript, or CSV hint from content_type.
  3. Content sniffing: a leading { selects Funscript; other content selects Autoblow CSV.

Funscript processing applies the optional inverted flag and optional range scaling before normalization. inverted defaults to false; range defaults to 100; version is ignored.

Every parsed timeline is normalized as follows:

Current configurable defaults

Limit Default Failure
Script lifetime 172,800 seconds (48 hours) Expired scripts return 404.
Inline content 5,000,000 characters 413
URL response body 5,242,880 bytes (5 MiB) 413
URL fetch timeout 10 seconds 504
Timeline duration 14,400,000 ms (4 hours) Later points are truncated.
Timeline points 200,000 422
Scripts per owner 10 409
name 256 UTF-8 bytes 422
author 256 UTF-8 bytes 422
Tag count 16 422
Total tag text 1,024 UTF-8 bytes 422

Scripts are immutable. A replacement uses delete followed by another upload. Playback refreshes the script storage lifetime.


4. Common Script Schemas

ScriptMetadataResponse

Field Type Always present Description
script_id string Yes Server-generated 32-character hexadecimal identifier.
source_format funscript \| autoblow_csv Yes Parsed source format.
point_count integer Yes Number of normalized timeline points.
duration_ms integer Yes Timestamp of the last retained point.
created_at ISO 8601 date-time Yes UTC creation time.
updated_at ISO 8601 date-time Yes UTC metadata update time.
name string or null No Optional display name.
tags string array No Optional tags; defaults to an empty array.
author string or null No Optional author metadata.
source_url string or null Owner responses only Original URL for URL imports; null for inline uploads. Omitted from non-owner metadata responses.

Example:

{
  "script_id": "4df8eead6bad4eecb50b236bf49d1c09",
  "source_format": "funscript",
  "point_count": 820,
  "duration_ms": 120000,
  "created_at": "2026-08-18T10:15:30Z",
  "updated_at": "2026-08-18T10:15:30Z",
  "name": "Demo motion",
  "tags": ["demo", "browser"],
  "author": "Integration team",
  "source_url": null
}

Script error body

Domain errors use a string detail:

{
  "detail": "timeline is empty after truncation"
}

FastAPI request validation errors use an array:

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "content"],
      "msg": "Field required",
      "input": {}
    }
  ]
}

5. Scripts Endpoints

5.1 POST /api/scripts

Stores Funscript or Autoblow CSV content included in the JSON request. The route is suitable for a browser file picker, generated content, or content already held by an application backend.

Request body

Field Type Required Default Description
content string Yes Full script source.
source_format enum or null No Auto-detect funscript or autoblow_csv.
content_type string or null No null Format hint such as application/json or text/csv.
name string or null No null Display name, up to the configured byte limit.
tags string array No [] Search or application metadata.
author string or null No null Author metadata.
max_duration_ms integer or null No Global limit Per-upload truncation limit. The lower of this value and the global limit applies.

Request

curl -X POST https://fug-prd.feelme.com/api/scripts \
  -H "Authorization: DCK $DCK" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "{\"actions\":[{\"at\":0,\"pos\":10},{\"at\":1000,\"pos\":90}]}",
    "source_format": "funscript",
    "name": "Two-point demo",
    "tags": ["demo"],
    "author": "Integration team"
  }'

Response

Route-specific errors

Status Condition
400 Content cannot be parsed in the selected or detected format.
409 Owner script quota is full.
413 Inline character limit is exceeded.
422 Point, duration, metadata, or tag limits fail.

5.2 POST /api/scripts/from-url

Fetches a script on the FUG server, parses the result, and stores the normalized timeline. Raw source content is discarded after parsing.

Request body

Field Type Required Default Description
url string Yes Public source URL. HTTPS is required by default.
source_format enum or null No Auto-detect funscript or autoblow_csv.
name string or null No null Display name.
tags string array No [] Search or application metadata.
author string or null No null Author metadata.
max_duration_ms integer or null No Global limit Per-upload truncation limit.

The fetcher rejects loopback, private, link-local, multicast, reserved, and cloud-metadata addresses. Redirects are followed manually, with validation on every hop and a maximum of three redirects. A HEAD pre-check and streamed byte cap protect the download size.

Request

curl -X POST https://fug-prd.feelme.com/api/scripts/from-url \
  -H "Authorization: DCK $DCK" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://cdn.example.com/motion/demo.funscript",
    "name": "Remote demo",
    "tags": ["remote"]
  }'

Response

Route-specific errors

Status Condition
400 URL policy blocks the address, or fetched content cannot be parsed.
409 Owner script quota is full.
413 Advertised or streamed response size exceeds the cap.
422 Point, duration, metadata, or tag limits fail.
502 Upstream transport, redirect, empty-body, or HTTP failure occurs.
504 Remote fetch exceeds the configured timeout.

5.3 GET /api/scripts

Returns metadata for scripts owned by the authenticated DCK. Results use newest-first order. Expired script records are removed from the owner index during listing.

Request

curl https://fug-prd.feelme.com/api/scripts \
  -H "Authorization: DCK $DCK"

Response

[
  {
    "script_id": "4df8eead6bad4eecb50b236bf49d1c09",
    "source_format": "funscript",
    "point_count": 820,
    "duration_ms": 120000,
    "created_at": "2026-08-18T10:15:30Z",
    "updated_at": "2026-08-18T10:15:30Z",
    "name": "Demo motion",
    "tags": ["demo"],
    "author": null,
    "source_url": null
  }
]

5.4 GET /api/scripts/{script_id}

Returns metadata for an existing script. Any valid DCK can read metadata by ID. The owner receives source_url; a different DCK receives the same metadata with source_url omitted.

Path parameter

Parameter Type Description
script_id string Server-generated script identifier.

Request

curl https://fug-prd.feelme.com/api/scripts/4df8eead6bad4eecb50b236bf49d1c09 \
  -H "Authorization: DCK $DCK"

Response


5.5 DELETE /api/scripts/{script_id}

Deletes the metadata, normalized timeline, and owner index entry. Only the creating DCK can delete the script.

Request

curl -X DELETE https://fug-prd.feelme.com/api/scripts/4df8eead6bad4eecb50b236bf49d1c09 \
  -H "Authorization: DCK $DCK"

Response

{
  "status": "deleted",
  "script_id": "4df8eead6bad4eecb50b236bf49d1c09"
}

Route-specific errors

Status Condition
403 Script exists but belongs to another DCK.
404 Script is unknown or expired.

6. Common Playback Behavior

Every playback control targets the device identified by the authenticated DCK. No playback request accepts a device identifier in the body.

Control routes update durable desired state or publish a drift-correction event and then return 202 Accepted. The player-worker applies the action asynchronously. Physical device execution is not confirmed by the HTTP acknowledgement.

PlaybackAccepted

Field Type Always present Description
status string Yes Acceptance result such as started, resumed, switching, rejected, or accepted.
request_id string Yes Server-generated correlation identifier. A directly applied control normally uses this value as SSE origin; reconciliation can use system.
detail string or null No Additional context, mainly for a rejected duplicate play request.
{
  "status": "accepted",
  "request_id": "f9e45c14631c4f90939c16aebd0d65bd",
  "detail": null
}

The status stream is the source of observable playback results. Control requests do not wait for a connected device, active player, or physical command acknowledgement.

Current configurable playback defaults

Setting Default Behavior
Speed range 0.5..2.0 Play and speed values are clamped server-side.
Pause timeout 900 seconds A long paused session stops automatically.
Sync drift threshold 500 ms Smaller sync corrections are ignored.
Sync minimum interval 5 seconds More frequent sync corrections are ignored.
SSE keepalive 15 seconds Comment frames keep idle connections open.
SSE streams per API instance 100 Additional streams receive 429.

Device profiles

Profile Default Position mapping Speed ratio
keon_wifi Yes Inverted, clamped to device range 0..99 180
keon2 No Non-inverted, clamped to device range 0..99 250

An unknown profile for a new session is detected by the player-worker after HTTP acceptance and produces an SSE error frame with detail: "unknown profile". A seamless switch keeps the active session profile because the switch control does not carry a replacement profile.


7. Playback Control Endpoints

7.1 POST /api/playback/play

Starts a new playback session, resumes a paused session, or switches an active device to another script.

Request body

Field Type Required Default Description
script_id string Yes Existing script identifier. Ownership is not required.
start_ms integer, minimum 0 No 0 Absolute starting position.
speed_scale number No 1.0 Initial playback rate, clamped to the configured range.
profile string or null No keon_wifi Device motion profile for a newly started session. Supported values are keon_wifi and keon2; a seamless switch retains the active profile.

Request

curl -X POST https://fug-prd.feelme.com/api/playback/play \
  -H "Authorization: DCK $DCK" \
  -H "Content-Type: application/json" \
  -d '{
    "script_id": "4df8eead6bad4eecb50b236bf49d1c09",
    "start_ms": 0,
    "speed_scale": 1.0,
    "profile": "keon_wifi"
  }'

Acceptance outcomes

status Condition Processing behavior
started No live player owns the device. Desired state becomes playing; a start request enters the worker queue.
resumed The same script exists in a paused live session. A play control resumes the current owner.
switching A different script is requested during a live session. The current owner loads the new timeline without releasing the device lease.
rejected The same script is already playing. No restart occurs; detail contains already playing this script, and a rejection frame is published.

Example:

{
  "status": "started",
  "request_id": "f9e45c14631c4f90939c16aebd0d65bd",
  "detail": null
}

Route-specific errors

Status Condition
404 script_id does not exist at request time.
422 script_id is missing, start_ms is negative, or another field has an invalid JSON type.

A script can expire between HTTP validation and worker loading. That race produces a later SSE frame with state error and detail script not found.


7.2 POST /api/playback/pause

Freezes the playback clock at the current position and sets durable desired state to paused.

Request

No request body is defined or required.

curl -X POST https://fug-prd.feelme.com/api/playback/pause \
  -H "Authorization: DCK $DCK"

Response

The route does not reject a missing active player. Desired state is still recorded, and a later status subscription can be primed from that state.


7.3 POST /api/playback/seek

Moves playback to an absolute media position. Seek represents explicit interface intent and updates durable desired state.

Request body

Field Type Required Description
position_ms integer, minimum 0 Yes Absolute media position in milliseconds.
curl -X POST https://fug-prd.feelme.com/api/playback/seek \
  -H "Authorization: DCK $DCK" \
  -H "Content-Type: application/json" \
  -d '{ "position_ms": 12000 }'

Response

The route accepts a position beyond the script duration. The player state and end-of-timeline behavior remain observable through SSE.


7.4 POST /api/playback/sync

Requests drift correction against an external media position. Sync is transient and does not change durable desired state.

Request body

Field Type Required Default Description
position_ms integer, minimum 0 Yes External media position in milliseconds.
client_ts number or null No null Reserved client timestamp. The current worker accepts the value without transport-delay compensation because no shared clock exists.
curl -X POST https://fug-prd.feelme.com/api/playback/sync \
  -H "Authorization: DCK $DCK" \
  -H "Content-Type: application/json" \
  -d '{ "position_ms": 12000, "client_ts": 1787048100.25 }'

Response

The worker applies sync only while playing, only when absolute drift exceeds the configured threshold, and only after the configured minimum interval. An applied correction emits a playback frame with detail: "sync". An ignored correction emits no dedicated result frame.


7.5 POST /api/playback/speed

Changes playback rate while preserving the current media position.

Request body

Field Type Required Description
speed number Yes Requested rate. The API clamps the value to the configured range, currently 0.5..2.0.
curl -X POST https://fug-prd.feelme.com/api/playback/speed \
  -H "Authorization: DCK $DCK" \
  -H "Content-Type: application/json" \
  -d '{ "speed": 1.5 }'

Response

Example clamping: a request containing 5.0 records and applies 2.0 under the current defaults.


7.6 POST /api/playback/offset

Sets an absolute signed timing offset. Repeating the same value is idempotent. A changed value shifts the effective media position by the offset delta.

Request body

Field Type Required Description
offset_ms integer Yes Absolute timing offset in milliseconds. Negative and positive values are accepted.
curl -X POST https://fug-prd.feelme.com/api/playback/offset \
  -H "Authorization: DCK $DCK" \
  -H "Content-Type: application/json" \
  -d '{ "offset_ms": -250 }'

Response


7.7 POST /api/playback/stop

Sets desired state to stopped and requests terminal playback cleanup. The player sends a best-effort device pause, publishes a final status frame, releases the device lease, and leaves the device room.

Request

No request body is defined or required.

curl -X POST https://fug-prd.feelme.com/api/playback/stop \
  -H "Authorization: DCK $DCK"

Response

No automatic resume follows a terminal state. Another play request is required for a new session.


8. GET /api/playback/status

Opens a long-lived Server-Sent Events stream for the authenticated device. The stream carries playback lifecycle frames and raw device status forwarded by the player-worker.

Request

GET /api/playback/status HTTP/1.1
Host: fug-prd.feelme.com
Authorization: DCK <device_connection_key>
Accept: text/event-stream

Browser integrations must use streaming fetch rather than native EventSource, because EventSource cannot attach the DCK header.

Response

Each application message uses an SSE data: field containing one JSON object:

data: {"type":"playback","device_key":"<device_connection_key>","state":"playing","position_ms":4200,"index":12,"point_count":820,"speed_scale":1.0,"offset_ms":0,"origin":"system","script_id":"4df8eead6bad4eecb50b236bf49d1c09","detail":null}

Playback frame

Field Type Description
type string Always playback.
device_key string Authenticated target DCK. The field should be treated as sensitive.
script_id string or null Current script identifier.
state enum idle, playing, paused, stopped, ended, error, or lease_lost.
position_ms integer Current absolute media position.
index integer Current normalized timeline index.
point_count integer Total timeline points. A primed or synthetic event can contain 0.
speed_scale number Current playback rate.
offset_ms integer Current absolute timing offset.
origin string Related control request_id or system.
detail string or null State context such as sync, unknown profile, or a terminal reason.

Playback states

State Meaning
idle No active playback state.
playing Timeline clock is advancing and due device commands can be emitted.
paused Timeline clock is frozen.
stopped Explicit stop or orphan reconciliation ended the session.
ended The final timeline point completed.
error A terminal processing, socket, profile, or repeated emit failure occurred.
lease_lost Another worker became authoritative; the old player stopped immediately.

Device status frame

Field Type Description
type string Always device_status.
payload object Raw device status received from the relay. The payload schema depends on the connected device and firmware.
received_at number Gateway receipt time as Unix epoch seconds.
{
  "type": "device_status",
  "payload": {
    "device_connection_key": "<device_connection_key>",
    "event_type": "<device_event_type>",
    "data": {}
  },
  "received_at": 1787048130.125
}

The stream should reconnect with backoff after transient failures. A fresh connection receives primed state, which removes any requirement for sticky API sessions.


9. Error Reference

Authentication errors

Status Example detail Cause
401 Unauthorized: Missing authorization header Header is absent.
401 Unauthorized: Invalid auth scheme, expected 'DCK <device_connection_key>' Scheme is not DCK.
401 Unauthorized: Missing DCK value Header contains no token.
401 Unauthorized: Invalid or missing device connection key Upstream verification rejects the DCK.
422 Unprocessable: Device connection key cannot be processed Verification service times out, fails, or returns invalid JSON.

Scripts and playback errors

Status Meaning Retry guidance
400 Bad Request Script parse failure or URL policy rejection. Correct the source or URL before another request.
403 Forbidden Script deletion attempted by a non-owner. No retry under the same DCK.
404 Not Found Script missing or expired; feature routes absent when the store is disabled. Refresh metadata and verify deployment configuration.
409 Conflict Per-owner script quota reached. Delete an unused owned script before upload.
413 Content Too Large Inline or remote source exceeds a configured cap. Reduce source size.
422 Unprocessable Content Pydantic validation or timeline/metadata limits fail. Correct fields or limits.
429 Too Many Requests SSE capacity reached on the API instance. Reconnect after a delay.
500 Internal Server Error Unexpected application or store failure. Record request context and apply bounded retry policy.
502 Bad Gateway Remote script fetch fails. Verify remote host and response.
504 Gateway Timeout Remote script fetch exceeds timeout. Retry later or use inline upload.

Asynchronous failure frames

HTTP 202 can precede an asynchronous error. Examples include:

Such failures appear as playback SSE frames, usually with state: "error" or state: "lease_lost" and a descriptive detail.

A network timeout after play creates ambiguous delivery. The status stream should be checked before another play request. Immediate blind retries can produce an unwanted resume, switch, or duplicate rejection.


10. Access and Lifecycle Rules

Rule Behavior
Script identity A UUID4 hexadecimal ID addresses each script globally.
Ownership The upload DCK owns the script. Owner identity is stored but never returned.
Listing Only the owner index is listed.
Metadata sharing Any valid DCK can read metadata by ID.
Source URL privacy Only the owner receives source_url.
Playback sharing Any valid DCK can play an existing script on the device identified by that DCK.
Mutation Script content cannot be edited or replaced in place.
Expiration Script records expire after the configured TTL; playback refreshes the TTL.
Device exclusivity A per-device lease permits one authoritative player across the worker fleet.
Terminal behavior stopped, ended, error, and lease_lost require another explicit play request.
Status delivery REST controls acknowledge intent; SSE reports observable state.

11. Minimal End-to-End Sequence

  1. Open GET /api/playback/status with the DCK header.
  2. Create a script through POST /api/scripts or POST /api/scripts/from-url.
  3. Retain the returned script_id.
  4. Send POST /api/playback/play with that identifier.
  5. Retain the returned request_id and compare later SSE origin, state, and control values.
  6. Send pause, seek, sync, speed, offset, or stop controls as required.
  7. Treat SSE state as the playback result.
  8. Stop active playback before early script cleanup.
  9. Delete temporary owned scripts when the normal TTL is not sufficient.

The live browser example implements this sequence without a framework or build step.