PuppyClicker
Open Webapp Download
API overview

public api

API v2

Build your own clients, devices, and automations on PuppyClicker. v2 covers nearly everything the app can do.

Base URL: https://puppyclicker-api.boundfire.com/api/v2
The v1 endpoint (GET /api/v1/click?key=..., see the v1 reference) is unchanged and stays supported.

On this page
  • Concepts and terminology
  • Authentication
  • Errors
  • Rate limits
  • CORS
  • Endpoints: GET /me · GET /puppies · GET /puppies/:id/actions · POST /puppies/:id/actions · GET /clicks/received · POST /clicks/self · GET /stream
  • Behaviour and semantics
  • Managing API keys
  • Recipes

Concepts and terminology

  • Puppy: an accepted friend that the key owner controls. The v2 puppies namespace contains only your accepted friends.
  • Action: something you send a puppy. A click, a message, an osc (OpenShock zap, vibrate, or sound), a rest (the puppy's configured webhook button), an integration (a delegated third-party button, for example Chaster add-time), or a sequence (a custom action the puppy built and shared: a saved program of clicks, zaps, vibrations, beeps, and waits that runs on them). Which actions a puppy supports depends on what hardware and webhooks they have configured and what they have delegated to you.
  • Self-click: clicking yourself to ring your own devices.
  • Self actions: /puppies/self/actions targets yourself, no friendship needed. Supports click, osc, rest, and sequence (your own sequences).
  • Key owner / acting user: the key is the identity. Every action runs as the key's owner.

Authentication

Every request needs an API key, created in the app under Settings → Public API Keys. Send it as a bearer token:

Authorization: Bearer pak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

For the SSE stream you can pass the key as a query parameter instead, ?key=pak_..., since browser EventSource can't set request headers.

Errors

Every error is JSON with a human error message and a stable machine code:

{ "error": "Puppy not found", "code": "PUPPY_NOT_FOUND" }
HTTPcodeMeaning
400INVALID_REQUESTBody or query failed validation. error is the first validation problem.
400TARGET_NOT_CONFIGUREDThe puppy doesn't have that hardware or webhook set up (for example an osc action to a puppy with no OpenShock device).
400UNSUPPORTED_SELF_ACTIONmessage or integration sent to /puppies/self/actions. These never work on yourself. Doesn't consume the action budget.
401MISSING_API_KEYNo key in the header or query.
401INVALID_API_KEYUnknown key.
401KEY_NOT_V2Key predates v2. Create a fresh key in Settings.
403ACCOUNT_BANNEDThe key owner's account is banned from PuppyClicker :c.
404PUPPY_NOT_FOUNDNo such puppy or you're not (yet) an accepted friend. The API never reveals which.
404ACTION_NOT_FOUNDThe rest/integration/sequence action id doesn't exist, is inactive, or isn't delegated/shared with you.
409SEQUENCE_RUNNINGA sequence is already running on that puppy. One run at a time per puppy; wait for it to finish (see worstCaseMs).
429RATE_LIMITEDOver budget; retry after retryAfter seconds (also in the Retry-After header).
429STREAM_LIMITToo many concurrent SSE connections for your account.
500INTERNALServer error. Please report on Discord. Safe to retry with backoff.
502UPSTREAM_FAILEDThe puppy's device, webhook, or integration failed to respond. The click is still recorded.

Rate limits

Two per-key budgets over a rolling 5-minute window, plus a per-account cap on stream connections:

BudgetFreeSupporter
Actions (every POST)20200
Reads (every GET except /stream)240240
Concurrent SSE stream connections (per account)15
  • The action budget scales with the owner's support tier; a freshly upgraded account may stay on the free limit for a few minutes until its cached tier refreshes. Reads are the same for everyone.
  • There is a global per-IP limit of 300 requests / 10 minutes applied before key lookup, so it covers invalid-key traffic too. That's what stops key guessing.
  • On 429 please honour the Retry-After header (seconds), especially if other people will run your app.

CORS

/api/v2 allows any origin.

Endpoints

GET /me

The key owner's own profile. Costs 1 read.

{
  "id": "3f1c8a2e-5b04-4d97-9a61-2e7c0f8b41d3",
  "username": "puppy",
  "poundUsername": "puppyyyy123",
  "pronouns": "they/them",
  "terminology": "puppy",
  "createdAt": "2026-01-05T10:20:30.000Z"
}

poundUsername, pronouns, and terminology may be null.

GET /puppies

Your accepted friends, each with a capability summary. Ordered by your saved friend order. Costs 1 read.

{
  "puppies": [
    {
      "id": "9c4d7b10-2f83-4e6a-b5c1-7d0928ea3f45",
      "username": "goodboy",
      "poundUsername": null,
      "pronouns": null,
      "terminology": "puppy",
      "profilePictureUrl": null,
      "capabilities": {
        "click": true,
        "message": true,
        "osc": true,
        "rest": false,
        "integrations": false,
        "sequences": false
      }
    }
  ]
}

capabilities tells you which action types will work for that puppy right now:

CapabilityTrue when
clickAlways.
messageAlways.
oscThe puppy has a validated OpenShock device configured.
restThe puppy has at least one active REST/webhook button.
integrationsThe puppy has delegated at least one integration button to owners.
sequencesThe puppy has shared at least one custom action sequence with owners.

Use GET /puppies/:id/actions for the full parameter metadata behind these flags.

GET /puppies/:id/actions

Everything you can send this puppy, with parameter metadata per action kind. Costs 1 read. Returns 404 PUPPY_NOT_FOUND if :id isn't one of your accepted friends.

Replace :id with self to get self actions: click always, plus osc (your own device and caps), rest (your own buttons), and sequence (your own sequences) when configured. message and integration are never listed on self. Your own user id does not work here, only self.

{
  "actions": [
    {
      "type": "click",
      "params": {
        "intensity": { "min": 0, "max": 100, "optional": true },
        "duration": { "min": 0, "max": 30000, "optional": true },
        "message": { "maxLength": 150, "optional": true }
      }
    },
    {
      "type": "message",
      "params": { "message": { "minLength": 1, "maxLength": 150, "required": true } }
    },
    {
      "type": "osc",
      "subtypes": ["Shock", "Vibrate", "Sound", "Stop"],
      "maxIntensity": 60,
      "maxDuration": 5000,
      "online": true,
      "params": {
        "intensity": { "min": 0, "max": 60, "required": true },
        "duration": { "min": 300, "max": 5000, "required": true }
      }
    },
    {
      "type": "rest",
      "id": "b21e6f4a-8c37-4d52-9e08-1a4b6c3d5f72",
      "name": "Open the door",
      "description": "…",
      "icon": "door",
      "variables": [ /* config-defined variable schema */ ]
    },
    {
      "type": "integration",
      "id": "7e05a9c3-6b18-4f2d-8a71-c93f0d24b6e8",
      "provider": "chaster",
      "providerLabel": "Chaster",
      "action": "add_time",
      "label": "Add 10 min",
      "icon": "lock",
      "params": { /* provider/action param schema */ }
    },
    {
      "type": "sequence",
      "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "name": "Warmup",
      "worstCaseMs": 13000,
      "steps": ["vibrate", "wait", "shock"]
    }
  ]
}

Per kind:

  • click: always present. Optional intensity (0-100), duration (0-30000 ms), message (up to 150 chars).
  • message: always present. Required message (1-150 chars). Officially unsupported, but it works fine in the client. Listed for posterity.
  • osc: present only when the puppy has a validated OpenShock device. Carries the puppy's own safety caps (maxIntensity, maxDuration) reflected into params.max, plus online (whether the device is reachable right now). The server also clamps commands to these caps regardless of what you send. subtype selects the effect.
  • rest: one entry per active REST/webhook button. id is the config id to pass back, or the literal "legacy" for older single-endpoint setups. variables is the button's own variable schema.
  • integration: one entry per delegated integration button. Pass id back; params describes any runtime parameters the action accepts (for example how much time to add).
  • sequence: one entry per custom action the puppy shared with owners (on self: your own). steps is a summary of the step kinds in order (click, shock, vibrate, beep, wait, integration); worstCaseMs is the longest the run can take. No parameters. The puppy authored the whole program.

POST /puppies/:id/actions

Send an action to a puppy. Consumes 1 action from the action budget. The body is a JSON object discriminated on type.

typeFields
clickintensity? 0-100, duration? 0-30000 (ms), message? up to 150 chars
messagemessage 1-150 chars (required)
oscsubtype Shock | Vibrate | Sound | Stop, intensity 0-100, duration 300-30000 (ms)
restid (config id, or "legacy"), variables? (object of number | bool | string), message? up to 150 chars
integrationid (button UUID), params? (object)
sequenceid (sequence UUID)

Click intensity is a no-op for now.

{ "type": "click", "intensity": 50, "duration": 800, "message": "boop" }
{ "type": "message", "message": "good puppy" }
{ "type": "osc", "subtype": "Vibrate", "intensity": 30, "duration": 1000 }
{ "type": "rest", "id": "b21e6f4a-8c37-4d52-9e08-1a4b6c3d5f72", "variables": { "speed": 3 }, "message": "hi" }
{ "type": "integration", "id": "7e05a9c3-6b18-4f2d-8a71-c93f0d24b6e8", "params": { "addSeconds": 600 } }
{ "type": "sequence", "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" }

Success response:

{ "success": true, "dndSuppressed": false, "message": "…" }
  • dndSuppressed is present on click, message, osc, rest, and integration results. It's true only when the puppy is in Do Not Disturb and shares their DND status; otherwise it's always false. A private DND state is never revealed to you. See Behaviour and semantics.
  • message echoes any response text the upstream REST or integration returned; absent for plain clicks and messages.
  • sequence results also carry runId and stepCount. The whole run costs 1 action, however many steps it has. The response returns as soon as the run starts. The steps then run server side, waits included, so a run can outlive your request by up to worstCaseMs.
curl -X POST \
  -H "Authorization: Bearer pak_..." \
  -H "Content-Type: application/json" \
  -d '{"type":"click","message":"boop"}' \
  https://puppyclicker-api.boundfire.com/api/v2/puppies/<puppyId>/actions

Failure modes worth handling: 404 PUPPY_NOT_FOUND (not your friend), 400 TARGET_NOT_CONFIGURED (for example osc to a puppy with no device), 404 ACTION_NOT_FOUND (bad, inactive, or non-delegated rest/integration/sequence id), 409 SEQUENCE_RUNNING (a sequence is already running on that puppy), 502 UPSTREAM_FAILED (their device or webhook errored, but the click is still recorded), 429 RATE_LIMITED.

Self actions

POST to /puppies/self/actions to act on yourself. Same bodies, same budget. Differences from a friend target:

  • click behaves like POST /clicks/self: it's recorded as type api under your key's name, with intensity and duration saved if you sent them.
  • osc fires your own device under your own safety caps. A Stop always reaches the device, even during DND, so you can always halt a running command.
  • rest triggers your own buttons (id from the self action list, or "legacy").
  • sequence runs one of your own sequences on yourself (they don't need to be shared with owners).
  • message and integration return 400 UNSUPPORTED_SELF_ACTION.
  • dndSuppressed reports your own DND state. Quiet hours suppress your own zaps and pushes too; the click is still recorded.

GET /clicks/received

Cursor-paginated feed of clicks you received, newest first. Costs 1 read.

Query params:

ParamNotes
take1-50, default 30.
cursorThe previous page's nextCursor. Omit for the first page.
{
  "items": [
    {
      "id": "d84f2b67-0a95-4c31-8be2-5f7a19c04d6b",
      "type": "click",
      "source": null,
      "message": "boop",
      "intensity": null,
      "duration": null,
      "timestamp": "2026-07-13T12:00:00.000Z",
      "senderId": "3f1c8a2e-5b04-4d97-9a61-2e7c0f8b41d3",
      "senderUsername": "owner",
      "senderPoundUsername": null,
      "hidden": false
    }
  ],
  "nextCursor": "2026-07-13T12:00:00.000Z_d84f2b67-0a95-4c31-8be2-5f7a19c04d6b"
}
  • type is the click kind: click, zap (OpenShock), api (API / REST / integration / self), and so on.
  • source is where it originated (null for a direct friend click, or values like pound / landing for social and landing-page clicks).
  • hidden: true rows come from a user you've blocked or who blocked you. Their senderId / senderUsername / senderPoundUsername are null, but the click itself is still listed.
  • nextCursor is null when there are no more rows. To page, pass it back verbatim as cursor.

POST /clicks/self

Records a self-click and rings your own devices, same as the v1 click. Consumes 1 action.

Body (optional):

{ "message": "optional, up to 150 chars" }

Response:

{ "success": true, "dndSuppressed": false }

Here dndSuppressed reports your own DND state, since the recipient is you. true means your quiet hours suppressed the push. The click was still recorded and streamed.

curl -X POST -H "Authorization: Bearer pak_..." \
  -H "Content-Type: application/json" -d '{"message":"coffee"}' \
  https://puppyclicker-api.boundfire.com/api/v2/clicks/self

Same effect as GET /api/v1/click. v1 isn't going anywhere.

GET /stream

A Server-Sent Events stream of your real-time alerts. These are the same events the desktop app receives (incoming clicks, messages, self-clicks, Pound social events), each with dndSuppressed flags.

Send the key as a bearer header, or as ?key= for browser EventSource, which can't set headers:

# In a browser you'd use EventSource("…/api/v2/stream?key=pak_…").
# From a shell with a header:
curl -N -H "Authorization: Bearer pak_..." \
  https://puppyclicker-api.boundfire.com/api/v2/stream

# Or with the query key:
curl -N "https://puppyclicker-api.boundfire.com/api/v2/stream?key=pak_..."

Frame types on the wire:

  1. ready, sent once on connect:
    event: ready
    data: {"connectedAt":"2026-07-13T12:00:00.000Z"}
  2. Keepalive comments every 25 seconds (SSE comment lines, ignore them):
    : keepalive
  3. alert events, your actual notifications:
    event: alert
    data: {"id":"d84f2b67-0a95-4c31-8be2-5f7a19c04d6b","kind":"click","senderUsername":"owner","body":"boop","timestamp":"2026-07-13T12:00:00.000Z","dndSuppressed":false}

Alert data fields. New ones may be added over time, so ignore unknown keys:

FieldNotes
idThe click or message id.
kindclick | message | comment | reaction_emoji | reaction_button.
senderUsernameDisplay name of the sender (or the key name for self and API clicks).
bodyMessage text, or *click* for a bare click.
timestampISO 8601.
durationPress-and-hold duration (ms), on click alerts that carry one.
sourcepound | landing for social and landing-page clicks; absent for friend clicks.
dndSuppressedtrue when you're in DND. The event is delivered so live views update, but clients should skip the native notification and sound.
testtrue for self-test clicks.

Concurrency is capped per account (1 free / 5 supporter).

Behaviour and semantics

v2 actions behave the same as the app. A few things to know:

  • Friendship. Every action needs an accepted friendship; otherwise 404 PUPPY_NOT_FOUND.
  • Do Not Disturb suppresses delivery. During the recipient's quiet hours the server drops the push and any physical trigger (OpenShock), but still records the click and still fires the real-time alert, flagged.
  • dndSuppressed privacy. On actions you send to a puppy, the flag is true only when the puppy both is in DND and shares their DND status; otherwise always false. On POST /clicks/self it reflects your own state truthfully.
  • OpenShock clamping. The server clamps osc intensity and duration to the target puppy's safety caps (maxIntensity, maxDuration), regardless of what you request. Still, send values within their caps so your app shows users what will actually happen.
  • REST variables. The server clamps them to the button's configured limits before the webhook fires.
  • Sequences run on their author, server side. One run per puppy at a time (409 SEQUENCE_RUNNING). The server clamps every device step to the puppy's current safety caps, and before each step re-checks that the run is still allowed (friendship, blocks, Do Not Disturb, device still configured). If not, it quietly stops the rest. An osc Stop cuts the puppy's current device step but not the rest of the run; only the puppy (or the app) can stop the run itself.
  • Notifications are rate limited per recipient, separately from your request budget. REST pings, for example, are capped at about 30 per 5 minutes per recipient.
© 2026 KinkyRvn · PuppyClicker Docs · Privacy · Terms · Delete account · Status