API Reference
Boundfire Public API
Read and act on your Boundfire account from your own apps and scripts, using an API key.
Overview
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:
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:
| Endpoint | What it does |
|---|---|
GET /v1/me/api-keys | List your keys. The raw key is never returned here. |
POST /v1/me/api-keys | Create a key, body { "name": "..." }. The raw bf_ key is returned once, in this response only. |
DELETE /v1/me/api-keys/:id | Revoke a key. |
Rate limits
| Scope | Limit |
|---|---|
| Per IP, before authentication | 60 requests/min |
| Per API key | 20 requests/min |
| Per API key | 60 requests/hour |
POST /fire-consequence, per API key | 10 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:
{ "data": { ... } }
Errors return the matching HTTP status with a code and message:
{ "error": { "code": "NOT_FOUND", "message": "Task not found" } }
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_INPUT | Malformed request, e.g. a bad UUID. |
| 401 | UNAUTHORIZED | Missing or invalid API key. |
| 403 | FORBIDDEN | Key is valid but you don't have permission for this. |
| 404 | NOT_FOUND | Doesn't exist, or isn't yours. |
| 422 | VALIDATION_ERROR | Body failed validation. |
| 429 | RATE_LIMITED | See rate limits above. |
| 500 | INTERNAL_ERROR | Something 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
/list-tasks
Lists your tasks scheduled for today, with completion status and a summary of each task's enabled triggers.
{
"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
/create-task
Creates a quick one-off verify task, optionally copying your default triggers onto it.
| Field | Type | Description |
|---|---|---|
name | string | Required. 1 to 255 characters. |
due_date | string | null | YYYY-MM-DD. Defaults to today; can't be in the past. |
due_time | string | null | HH:MM. Defaults to 23:59. |
defaultTriggersEnabled | boolean | null | Defaults to true. Copies your enabled default triggers onto the task. |
{
"data": {
"task": { "id": "uuid", "name": "Do the dishes" },
"triggers_applied": 2
}
}
Task details
/task-details/:id
Returns one task (must be yours and not archived) plus today's completion.
{
"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
/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):
waterandstepsare completed by logging progress in the appphotoandvideoneed a fresh capture in the appjournalis 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.
{
"data": {
"success": true,
"status": "completed",
"points_awarded": 5,
"streak_incremented": true,
"new_streak": 12,
"triggers_executed": 1
}
}
Fail a task
/fail-task/:id
Fails a task for today, applying failure points and executing failure consequences.
{
"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
/list-subs
Lists your active connections where you're the guide and have been granted the view_tasks permission.
{
"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-sub-stats/:subId
Today's task summary for one sub, in the sub's timezone. Requires the view_tasks permission; otherwise 403.
{
"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.
/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.
{
"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
/fire-consequence
Fires one integration immediately. Rate limited to 10 fires per minute per key.
| Field | Type | Description |
|---|---|---|
integration_id | uuid | Required. From list-consequences. |
action_config | object | Required. Validated against that plugin's action_schema. |
idempotency_key | string | null | 1 to 255 characters. At-most-once dedupe for retries of the same fire. |
{
"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.