API Docs

Endpoints.

Every route under /api/v1, then how targeting and idempotency work.

1345 words · ~6 min read

All paths are relative to https://apidrop.kinkyraven.com/api/v1. Every route takes the Bearer key from Authentication and can answer 401 unauthorized or 429 rate_limited. Every route except GET /relay/me can answer 403 patreon_required. Those three are not repeated below.

Routes marked relayed travel to the user's app and can also fail with the targeting errors.

Account

GET /relay/me

Who the key belongs to, whether the supporter gate passes, and the live rate counters. This is the only route the gate does not block, so use it as your health check.

{
  "user": {"id": "8f3d...", "email": "user@example.com"},
  "key": {"id": "c1a2...", "label": "puppyclicker2", "created_at": 1752768000},
  "patreon": {"active": true},
  "rate_limits": {
    "per_key_per_min": 120,
    "per_account_per_min": 240,
    "used_this_window": {"key": 3, "account": 3}
  }
}

patreon.active is the supporter gate under its historical name. used_this_window includes this call.

GET /relay/online

Whether any of the user's computers is connected. Answered from presence, never relayed.

{
  "online": true,
  "clients": [
    {"client_id": "d4e5...", "platform": "windows", "name": "Gaming PC", "online": true, "last_seen": 1752768123}
  ]
}

The client_id values are what you pass to pin a target.

Effect library

GET /relay/library

Relayed. The user's whole effect library as {"effects": [...]}. Each entry is the effect's own file shape, with id, name and version added. Effects that cannot render, such as audio and effect sets, are left out.

Optional query: client_id.

GET /relay/effect/{effect_id}

Relayed. One library entry in the same shape, or 404 effect_not_found.

Optional query: client_id.

POST /relay/trigger

Relayed. Fire a library effect on the whole screen. Idempotent, see Idempotency.

{
  "effect_id": "glow",
  "duration_secs": 60,
  "client_id": "d4e5...",
  "clientEventId": "evt_01J..."
}
  • effect_id is required.
  • duration_secs is optional. Leave it out and the effect stays until the session ends or someone stops it.
  • client_id and clientEventId are optional.
  • params is accepted and ignored today.

Success is 200 {"ok": true, "effect_id": "glow"}.

The app can refuse with 403 remote_disabled, 409 session_off, 404 effect_not_found, 403 premium_required, 403 high_risk_disabled, 403 not_remotely_fireable or 403 unlock_unavailable. Errors explains each.

Your manual triggers

Manual triggers are the rules the user marked as fire-by-hand. Firing one goes through the app's rules engine, so the rule's own effects, durations, cooldowns and chance apply, and every stop path the user has reaches it. The app's last-trigger feed shows the fire as api:<key label>.

GET /relay/rules

Relayed. The user's fire-able manual triggers. Enabled, genuinely manual, not gated behind anything.

{"rules": [{"id": "r_9f2...", "name": "Bubbles"}]}

Optional query: client_id.

POST /relay/rules/trigger

Relayed. Fire one trigger. Idempotent.

{"rule_id": "r_9f2...", "client_id": "d4e5...", "clientEventId": "evt_01J..."}

Only rule_id is required. Success is 200 {"ok": true, "rule_id": "r_9f2..."}.

App refusals: 403 remote_disabled, 409 session_off, 404 rule_not_found. A disabled or gated trigger also answers rule_not_found. An app from before this route answers app_outdated.

POST /relay/rules/stop

Relayed. End what one trigger put up and nothing else.

{"rule_id": "r_9f2...", "client_id": "d4e5..."}

Success is 200 {"ok": true, "rule_id": "r_9f2..."}. Not idempotency-keyed, since a repeated stop is just another stop.

A stop for a trigger that put nothing up, or that has since been disabled or deleted, is a 200 no-op. A rule can stop being firable while its effects are still on screen, and refusing there would strand exactly what this call exists to end. No session_off, no rule_not_found.

App refusals: 403 remote_disabled, app_outdated. Server-side: 400 bad_request (missing rule_id), 400 bad_rule (over 128 characters or control characters).

POST /relay/rules/clear

Relayed. The panic stop. Every live effect off, whatever put it up.

The body is optional. Send nothing, or {"client_id": "d4e5..."}. With a Content-Type: application/json header send at least {}.

Success is 200 {"ok": true}. Not idempotency-keyed.

App refusals: 403 remote_disabled, app_outdated.

Friends

You never choose an effect for a friend. You choose one of the triggers the friend set up, and only where the friend has allowed the key's owner control. Every friend call passes the same checks as pressing the button on the app's friend control page.

  • The friendship must exist.
  • The friend's consent switch toward the key's owner must be on.
  • To fire, the friend needs an active supporter subscription. Stops and clears skip this check on purpose. A membership lapsing between a fire and its stop must not strand effects on their screen.

There is no client_id on friend routes. The call goes to the friend's oldest-registered online computer.

Friend routes spend extra budgets shared with the control page, on top of the per-key and per-account limits. Each is listed below.

GET /relay/friends

The key owner's friends list. Answered by the server.

{
  "friends": [
    {"user_id": "7c1d...", "name": "kit", "online": true, "can_control": true}
  ]
}

name is the public username they chose, never a login email. can_control is their switch toward the key's owner. With it off, the routes below answer 403 or 404, and online always reads false.

GET /relay/friends/{user_id}/rules

Relayed off the friend's computer. Their manual triggers, as they configured them.

{"rules": [{"id": "r_ab3...", "name": "Bubbles"}]}

Budgets: 20 per minute per caller, 10 per minute per caller-and-friend pair.

Errors: 404 not_friends, 403 friend_control_disabled, 503 device_offline, 504 client_timeout, 502 rules_unavailable.

POST /relay/friends/{user_id}/trigger

Relayed. Fire one of the friend's triggers. Idempotent.

{"rule_id": "r_ab3...", "clientEventId": "evt_01J..."}

Success is 200 {"ok": true, "rule_id": "r_ab3..."}.

Budgets: 30 per minute per sender, 10 per minute per sender-and-friend pair.

Errors: 400 bad_request, 404 not_friends, 403 friend_trigger_disabled, 403 target_patreon_required, 403 app_outdated, 503 device_offline, 504 client_timeout. The friend's app can also answer remote_disabled, session_off and rule_not_found.

POST /relay/friends/{user_id}/stop

Relayed. Undo one trigger fired at the friend.

{"rule_id": "r_ab3..."}

Success is 200 {"ok": true, "rule_id": "r_ab3..."}. A stop for a trigger that put nothing up is a 200 no-op. Not idempotency-keyed.

Budgets: its own 30 per minute per sender and 10 per pair, independent of the fire budget, so a spent fire budget never blocks the way back down.

Errors: as for the fire, minus target_patreon_required, plus 400 bad_rule.

POST /relay/friends/{user_id}/clear

Relayed. Everything off the friend's screen. No body.

Success is 200 {"ok": true}. Not idempotency-keyed.

Budgets: its own 30 per minute per sender and 10 per pair.

Errors: as for the stop, minus anything about rule_id.

Targeting

Every relayed call has to reach exactly one of the user's computers. The client_id values come from GET /relay/online.

With a client_id:

  • Not on the account: 404 client_not_found.
  • Offline: 503 device_offline.

Without one, the server auto-routes:

  • Exactly one computer online: the call goes there.
  • Several online: 409 ambiguous_client, with a clients array of the online ones so you can pick.
  • None online: 503 device_offline.

If the app does not answer within 10 seconds you get 504 client_timeout.

Most users have one computer, so leaving client_id out and handling 409 on the rare multi-machine account is a reasonable design.

Idempotency

POST /relay/trigger, POST /relay/rules/trigger and POST /relay/friends/{user_id}/trigger honour an Idempotency-Key header. The body field clientEventId is an alias for it. The header wins when both are present, and they share one key space.

Idempotency-Key: evt_01JXYZ...
  • Keys are scoped per account and per endpoint, and per friend on the friend route. The same key on a different trigger route can never replay another's response.
  • Keys are remembered for 5 minutes.
  • A retry with the same key returns the first execution's response byte for byte, status and body included, with an Idempotency-Replayed: true header. It is not delivered to the device again. This includes mapped failures like 503 device_offline.
  • 429 rate_limited responses are never stored, because nothing happened. Retrying re-executes.
  • Empty keys and keys over 128 characters are ignored. The call runs without dedupe.
  • The key dedupes side effects only. Two truly concurrent first executions with the same key can both deliver. Send one, then retry.

Stop and clear routes take no key. Repeating them is harmless.

Generate one key per logical event on your side, not per HTTP attempt. That way a timeout, a dropped connection and an impatient retry all collapse into a single fire on the user's screen.