API Reference

Boundfire Public API

Read and act on your Boundfire account from your own apps and scripts, using an API key.

Overview

Base URL
https://api.boundfire.com/v1/public

All requests and responses are JSON. Dates use your account's timezone.

Authentication

Send your API key in a header, either form:

Request
curl https://api.boundfire.com/v1/public/list-tasks \
  -H "X-Api-Key: bf_YOUR_KEY"

# or
curl https://api.boundfire.com/v1/public/list-tasks \
  -H "Authorization: Bearer bf_YOUR_KEY"

Keys are 51 characters: bf_ plus 48 hex. A key is a full-account credential and does not expire until revoked. Never put it in a query parameter.

Managing keys

Keys are managed with your normal app session (JWT), not an API key:

EndpointWhat it does
GET /v1/me/api-keysList your keys. The raw key is never returned here.
POST /v1/me/api-keysCreate a key, body { "name": "..." }. The raw bf_ key is returned once, in this response only.
DELETE /v1/me/api-keys/:idRevoke a key.

Rate limits

ScopeLimit
Per IP, before authentication60 requests/min
Per API key20 requests/min
Per API key60 requests/hour
POST /fire-consequence, per API key10 fires/min (counted within the 20/min bucket)

Exceeding a limit returns HTTP 429 with code: "RATE_LIMITED". Responses include standard RateLimit-* headers.

Responses & errors

Successful responses wrap the payload in data:

Success
{ "data": { ... } }

Errors return the matching HTTP status with a code and message:

Error
{ "error": { "code": "NOT_FOUND", "message": "Task not found" } }
StatusCodeMeaning
400INVALID_INPUTMalformed request, e.g. a bad UUID.
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENKey is valid but you don't have permission for this.
404NOT_FOUNDDoesn't exist, or isn't yours.
422VALIDATION_ERRORBody failed validation.
429RATE_LIMITEDSee rate limits above.
500INTERNAL_ERRORSomething broke on our end.

The action endpoints (complete-task, fail-task, fire-consequence) return HTTP 200 with success: false and an error string inside data for business outcomes, like a task that already failed. 4xx is reserved for request-level failures.

List tasks

GET /list-tasks

Lists your tasks scheduled for today, with completion status and a summary of each task's enabled triggers.

Response
{
  "data": {
    "date": "2026-08-12",
    "timezone": "Australia/Sydney",
    "tasks": [
      {
        "id": "uuid",
        "name": "Morning stretch",
        "type": "verify",
        "schedule": "daily",
        "due_time": "09:00:00",
        "type_config": { "type": "verify" },
        "status": "pending",
        "triggers": [
          { "event": "on_failure", "integration_type": "openshock" }
        ]
      }
    ]
  }
}

status is one of pending, completed, failed or cancelled.

Create a task

POST /create-task

Creates a quick one-off verify task, optionally copying your default triggers onto it.

FieldTypeDescription
namestringRequired. 1 to 255 characters.
due_datestring | nullYYYY-MM-DD. Defaults to today; can't be in the past.
due_timestring | nullHH:MM. Defaults to 23:59.
defaultTriggersEnabledboolean | nullDefaults to true. Copies your enabled default triggers onto the task.
Response
{
  "data": {
    "task": { "id": "uuid", "name": "Do the dishes" },
    "triggers_applied": 2
  }
}

Task details

GET /task-details/:id

Returns one task (must be yours and not archived) plus today's completion.

Response
{
  "data": {
    "task": {
      "id": "uuid",
      "name": "Morning stretch",
      "type": "verify",
      "schedule": "daily",
      "due_time": "09:00:00",
      "type_config": { "type": "verify" },
      "success_points": 5,
      "failure_points": 0
    },
    "today": {
      "date": "2026-08-12",
      "completion": { "status": "pending", "completed_at": null }
    }
  }
}

today.completion is null if there's no completion row yet.

Complete a task

POST /complete-task/:id

Completes a task for today. Idempotent: repeating a completed task returns success: true with already_completed: true.

Some types can't be completed here (parity with the app):

  • water and steps are completed by logging progress in the app
  • photo and video need a fresh capture in the app
  • journal is completed by writing a journal entry in the app

These, plus off-schedule days and already failed or cancelled tasks, return 200 with success: false and an error message.

On success, points and streak are awarded and the task's enabled on_complete triggers fire immediately (skipped while consequences are paused or Emergency Stop is active). Today's pending trigger jobs for the task are cancelled.

Response
{
  "data": {
    "success": true,
    "status": "completed",
    "points_awarded": 5,
    "streak_incremented": true,
    "new_streak": 12,
    "triggers_executed": 1
  }
}

Fail a task

POST /fail-task/:id

Fails a task for today, applying failure points and executing failure consequences.

Response
{
  "data": {
    "success": true,
    "status": "failed",
    "failure_points_applied": 3,
    "triggers_executed": 1
  }
}

If the fail is rejected (for example the task is already completed, failed or cancelled), you get 200 with success: false, an error, and zeroed counters.

List subs

GET /list-subs

Lists your active connections where you're the guide and have been granted the view_tasks permission.

Response
{
  "data": {
    "subs": [
      {
        "id": "connection-uuid",
        "sub_id": "user-uuid",
        "display_name": "pup",
        "avatar_url": "https://... (short-lived signed URL) or null",
        "timezone": "Europe/London"
      }
    ]
  }
}

Sub stats

GET /get-sub-stats/:subId

Today's task summary for one sub, in the sub's timezone. Requires the view_tasks permission; otherwise 403.

Response
{
  "data": {
    "sub": { "id": "uuid", "display_name": "pup", "timezone": "Europe/London" },
    "date": "2026-08-12",
    "summary": { "total": 4, "completed": 2, "pending": 1, "failed": 1 },
    "tasks": [
      {
        "id": "uuid",
        "name": "Morning stretch",
        "type": "verify",
        "due_time": "09:00:00",
        "status": "completed",
        "completed_at": "2026-08-12T08:12:00.000Z"
      }
    ]
  }
}

List consequences

Fire one of your configured integrations (OpenShock, Pavlok, Discord, webhooks, ...) directly. All safety gates are enforced server-side: integration enabled, device safety acknowledgement, and the account-wide pause / Emergency Stop.

GET /list-consequences

Lists your integrations as fireable consequences, including each plugin's action schema. Credentials are never returned. Store-restricted types (shock and chastity devices) are hidden from app-store client builds.

Response
{
  "data": {
    "consequences": [
      {
        "id": "integration-uuid",
        "type": "openshock",
        "display_name": "OpenShock",
        "name": "My shocker",
        "enabled": true,
        "requires_safety_ack": true,
        "safety_ack_satisfied": true,
        "supports_emergency_stop": true,
        "fireable": true,
        "action_schema": { "...plugin action schema...": "" }
      }
    ]
  }
}

fireable reflects enabled + safety ack only; a pause or Emergency Stop is enforced at fire time.

Fire a consequence

POST /fire-consequence

Fires one integration immediately. Rate limited to 10 fires per minute per key.

FieldTypeDescription
integration_iduuidRequired. From list-consequences.
action_configobjectRequired. Validated against that plugin's action_schema.
idempotency_keystring | null1 to 255 characters. At-most-once dedupe for retries of the same fire.
Response
{
  "data": {
    "success": true,
    "integration_type": "openshock",
    "integration_name": "My shocker",
    "fired_at": "2026-08-12T10:00:00.000Z",
    "deduplicated": false,
    "response_data": null
  }
}

Gate failures (disabled integration, missing safety ack, paused, unknown id) return 4xx. A request that passed every gate but failed at the device returns 200 with success: false and an error string.