Fluffer by MotorbunnyDevelopers

Remote API

API referenceBeta

Endpoints of the Fluffer Remote API: status, vibrate, rotate, patterns, stop, tips, fuel and command results, with parameters, responses and errors.

Base URL: https://api.fluffer.app/v1

Every request needs the header Authorization: Bearer flk_…. Send JSON bodies with Content-Type: application/json (an empty body is fine where nothing is required). Browsers may call the API too (CORS is open), but only from places you keep private: anyone who can read a page’s source can read a key in it.

#GET /me

Checks a key and returns what it may do.

{ "key_id": "0d3e…", "scopes": ["device.control", "tips.publish"] }

#GET /status

Whether the user’s Fluffer app is listening, and what their toy is doing. Scope: device.status.

{
  "listening": true,
  "paused": false,
  "last_seen": "2026-10-13T18:21:04.512Z",
  "toy": { "type": "Buck 2", "battery": 80 },
  "motors": [
    { "index": 1, "kind": "vibe", "on": true, "level": 0.6, "until": "2026-10-13T18:21:07.512Z" },
    { "index": 2, "kind": "twirl", "on": false, "level": 0.5, "until": null }
  ],
  "app_version": "3.1.0"
}
FieldMeaning
listeningThe app is open with Remote control on and reported in within the last 90 seconds.
pausedThe user tapped Stop; motion is ignored until they tap Resume.
last_seenWhen the app last reported in, or null if it never has.
toytype (the model family, e.g. "Buck 2", "Motorbunny", or "phone" for the phone’s own vibration) and battery 0–100 or null. Null when no toy is connected.
motorsUp to 4: index 1–4, kind (vibe, twirl, thrust, thrust-side), on (false when the user switched it off in the app), level 0–1 (twirl: 0.5 is still), until (when it stops) or null.
app_versionThe Fluffer app’s version, or null.

Nothing personal is in it: no names, emails, locations or other apps.

#GET /commands/{id}

What the app did with a command, by the id from its 202. A key with device.status can read any of its user’s commands; any other key only the ones it sent. Anything else is 404.

{ "id": "6f1c0d7e-…", "action": "vibrate", "created_at": "…", "result": "played", "reason": null, "handled_at": "…" }
FieldValues
resultnull until the app handles it, then played, partly (some of it played) or refused. It never changes once set.
reasonpaused, motor_off, no_toy, not_listening, power_limit, expired, or null.
handled_atWhen the app handled it, or null.

The user can switch any motor off with its power button in the app. A command that needs a motor that’s off comes back partly (the other motors played) or refused (nothing played), with reason motor_off. Old commands are cleared after a while (motion after an hour, tips and fuel after 7 days); then this returns 404.

#POST /vibrate

Vibrates the toy for a while, then stops. Scope: device.control.

FieldTypeNotes
strengthnumber 0–1All motors. Use this or the per-motor fields.
m1 … m4number 0–1Per motor; any you leave out follow m1.
duration_msnumber50 to 900000 (15 minutes). Default 5000.
{ "strength": 0.4, "duration_ms": 10000 }

Sending a new vibrate replaces the previous one. To change strength smoothly, send updates as often as you like within the rate limit.

#POST /rotate

Rotation (“twirl”) for toys that have it. Scope: device.control.

FieldTypeNotes
speednumber −1–1The sign is the direction. Required.
duration_msnumber50 to 900000. Default 5000.

#POST /pattern

Plays a named pattern. Scope: device.control.

FieldTypeNotes
namestringpulse, impact, tap, wave, rampUp, rampDown, fade or heartbeat
paramsobjectOptional numbers, below. Unknown ones are ignored.
ParamRangeUsed by
strength0–1pulse, impact, tap, heartbeat
amplitude0–1wave
from, to0–1rampUp, rampDown
fromStrength0–1fade
durationMs10–900000pulse, wave, rampUp, rampDown, fade, heartbeat
frequencyHz0.1–10wave
bpm30–200heartbeat
steps1–100rampUp, rampDown

A heartbeat without durationMs plays for 15 minutes; send stop to end it sooner.

{ "name": "wave", "params": { "amplitude": 0.8, "frequencyHz": 0.5, "durationMs": 60000 } }

#POST /stop

Stops whatever the API started. Any key may call it; no body.

#POST /tip

Sends a tip to the user’s Fluffer. It feeds their Tip4Fuel exactly like a Chaturbate tip: it adds fuel by their own token-to-fuel settings, shows in their tip history with an API badge, and reaches games they’ve allowed to see tips. With is_anon (or no tipper), no name is stored or shown. Works while the user’s Remote control mode is Tips only or Tips + control. Scope: tips.publish.

FieldTypeNotes
tokenswhole number1 to 100000. Required.
tipperstringUp to 60 characters.
messagestringUp to 280 characters.
is_anonbooleanHide the tipper’s name.
external_idstringYour id for this tip (up to 128 characters). Sending the same id again is recorded once, so retries are safe.

#POST /fuel

Adds fuel straight to the user’s Tip4Fuel tank (no tip history entry). Games they’ve allowed to see tips get it as a fuel change with source api. Scope: device.fuel.

FieldTypeNotes
amountnumberMore than 0, up to 100.

#Responses

A successful command returns 202 with its id (for GET /commands/{id}) and the command as Fluffer will play it (after limits are applied):

{ "id": "…", "action": "pattern", "kind": "haptic_pattern", "payload": { "name": "wave", "params": { "amplitude": 0.8 } }, "received_at": "…" }

A repeated tip with the same external_id returns 202 with "duplicate": true and the original id.

#Errors

Errors are JSON: { "error": "…" }.

StatusMeaning
400The body isn’t valid (the message says which field).
401Missing, unknown or revoked key.
403The key doesn’t have the scope this needs.
404No such endpoint, or no such command for this key.
413Body larger than 8 KB.
429Over the rate limit; wait for Retry-After seconds.
503Temporarily unavailable; try again shortly.

#Delivery

Commands play while the Fluffer app is open on the user’s phone with Remote control on. Motion commands that arrive while it isn’t listening are dropped rather than played late (a stale “vibrate” should never fire minutes later). Tips and fuel sent during a brief disconnect are caught up when the app reconnects. Use GET /me to check a key, GET /status (scope device.status) to see whether the user’s app is listening right now, and GET /commands/{id} to see whether a command played.

#Examples

// Node 18+
const res = await fetch('https://api.fluffer.app/v1/pattern', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.FLUFFER_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'pulse', params: { strength: 0.7 } }),
});
console.log(res.status, await res.json());
# Python, with requests
python3 - <<'PY'
import os, requests
r = requests.post("https://api.fluffer.app/v1/vibrate",
    headers={"Authorization": f"Bearer {os.environ['FLUFFER_KEY']}"},
    json={"strength": 0.5, "duration_ms": 4000})
print(r.status_code, r.json())
PY