TypeScript & PHP SDKs for the Chastify API
Two client libraries for the Chastify Developer API: control your lock session, build extensions and receive webhooks β without rewriting auth, retries or error handling.
What this project is
The e-cosplay.fr association builds these SDKs free of charge for the Chastify.net community. The Chastify API is rich (lock, tasks, devices, extensions, files, webhooks), but calling it properly takes a lot of boilerplate. The SDKs handle it, with the same design in both languages.
π Full API coverage
External API, extension session API, file storage, webhook verification and the iframe bridge.
π Automatic retries
Exponential backoff on 429 and 5xx, honouring the Retry-After header.
π§― Typed errors
One class per documented case: invalid token, no active session, device offlineβ¦
ποΈ Cache & logs
In-memory or Redis cache (off by default) and debug logs with secrets redacted.
Installation
| SDK | Package | Requirements |
|---|---|---|
| TypeScript / JS | @ecosplay/chastify-sdk (npm) | Node β₯ 18 or Bun β₯ 1.3 |
| PHP | e-cosplay/chastify-sdk (Composer) | PHP β₯ 8.1, ext-curl, ext-json, ext-mbstring, ext-openssl, ext-fileinfo |
The package is published on npm:
npm add @ecosplay/chastify-sdk # Node
bun add @ecosplay/chastify-sdk # Bun
The package is published on Packagist:
composer require e-cosplay/chastify-sdk
ext-redis (phpredis) is optional, only for the Redis cache.
Getting a key
| Key | Used for | Client option |
|---|---|---|
DEV token (user) |
Control your own active session (/api/apps/v1/*) |
devToken |
| App developer key | Public extensions used by others (/api/extensions/*), together with each session's mainToken |
appKey (+ appId) |
CHASTIFY_DEV_TOKENβ¦).
Quick start
Read the active session, then act on the lock:
import { ChastifyClient } from "@ecosplay/chastify-sdk";
const chastify = new ChastifyClient({ devToken: process.env.CHASTIFY_DEV_TOKEN! });
const session = await chastify.session.get();
console.log(session.lockData?.remainingSeconds);
await chastify.lock.addTime(600); // +10 min
await chastify.lock.freeze(1800); // freeze 30 min
await chastify.lock.unfreeze();
await chastify.tasks.assign({ taskText: "10 min meditation", points: 10 });
await chastify.logs.custom({ title: "Check-in", role: "extension" });
await chastify.device.vibrate({ intensityPct: 40, durationSeconds: 5 });
use Ecosplay\Chastify\ChastifyClient;
$chastify = new ChastifyClient(['devToken' => getenv('CHASTIFY_DEV_TOKEN')]);
$session = $chastify->session()->get();
echo $session['lockData']['remainingSeconds'] ?? 0;
$chastify->lock()->addTime(600); // +10 min
$chastify->lock()->freeze(1800); // freeze 30 min
$chastify->lock()->unfreeze();
$chastify->tasks()->assign(['taskText' => '10 min meditation', 'points' => 10]);
$chastify->logs()->custom(['title' => 'Check-in', 'role' => 'extension']);
$chastify->device()->vibrate(['intensityPct' => 40, 'durationSeconds' => 5]);
What the external API covers
| Area | Methods (JS: chastify.x, PHP: $chastify->x()) |
|---|---|
| session | get() |
| lock | applyTime, addTime, removeTime, freeze, unfreeze, toggleFreeze, pillory, endPillory |
| tasks | assign, startTimer, complete |
| hygiene | startUnlock |
| settings | patch |
| logs | custom |
| device | shock, stopShock, vibrate, stopVibration, allStop, setRandomShock, setBerserkShock |
| actions | run(name, params) β for any action, including future ones |
For devices, the wearer's configured intensity cap is always enforced server-side, whatever value you send.
Configuration
import { ChastifyClient, RedisCache, ConsoleLogger } from "@ecosplay/chastify-sdk";
import Redis from "ioredis";
const chastify = new ChastifyClient({
devToken: process.env.CHASTIFY_DEV_TOKEN!,
timeoutMs: 30_000, // default
maxRetries: 4, // 429 + 5xx + network
cache: new RedisCache(new Redis(process.env.REDIS_URL!)),
cacheTtlMs: 5000, // 0 = cache OFF (default)
logger: new ConsoleLogger(),
logLevel: "debug", // secrets redacted
});
await chastify.session.get({ cache: false }); // force refresh
use Ecosplay\Chastify\ChastifyClient;
use Ecosplay\Chastify\Cache\RedisCache;
use Ecosplay\Chastify\Logging\StderrLogger;
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$chastify = new ChastifyClient([
'devToken' => getenv('CHASTIFY_DEV_TOKEN'),
'timeoutMs' => 30000, // default
'maxRetries' => 4, // 429 + 5xx + network
'cache' => new RedisCache($redis), // or any PSR-16 cache
'cacheTtlMs' => 5000, // 0 = cache OFF (default)
'logger' => new StderrLogger(), // or any PSR-3 logger
]);
$chastify->session()->get(['cache' => false]); // force refresh
Code generator
Pick a call, fill in its parameters and copy ready-to-run code for your backend: cURL, Node or PHP.
Building an extension
Chastify loads your extension in an iframe and passes a context (including sessionId and
mainToken) in location.hash. The browser reads it through the bridge;
state-changing actions go through your backend, with the app key.
- Browser β read the context and session through the bridge (read-only, no secret).
- Backend β receive
sessionId+mainToken, then call the API withappKey.
1. Inside the iframe (JS only)
import { parseHashPayload, ChastifyBridgeClient, startAutoResizeToParent }
from "@ecosplay/chastify-sdk/bridge";
const ctx = parseHashPayload(location.hash);
const bridge = new ChastifyBridgeClient(ctx);
startAutoResizeToParent(ctx); // resizes the iframe
const session = await bridge.request("session.get");
const state = await bridge.request("state.get");
2. On your backend
const chastify = new ChastifyClient({ appKey: process.env.CHASTIFY_APP_KEY! });
const s = chastify.extensions.session(sessionId, mainToken);
await s.patchState({ wins: 3 }); // extension state
await s.patchMetadata({ unlockBlockers: ["Win 3 games"] });
await s.action("add_time", { deltaSeconds: 300 }); // reward or penalty
await s.recordProgress({ metric: "memory_win", amount: 1 });
await s.notify({ title: "Well done!", message: "Game won", target: "both" });
await s.files.upload({ data: bytes, filename: "proof.jpg" }, { purpose: "evidence" });
$chastify = new ChastifyClient(['appKey' => getenv('CHASTIFY_APP_KEY')]);
$s = $chastify->extensions()->session($sessionId, $mainToken);
$s->patchState(['wins' => 3]); // extension state
$s->patchMetadata(['unlockBlockers' => ['Win 3 games']]);
$s->action('add_time', ['deltaSeconds' => 300]); // reward or penalty
$s->recordProgress(['metric' => 'memory_win', 'amount' => 1]);
$s->notify(['title' => 'Well done!', 'message' => 'Game won', 'target' => 'both']);
$s->files()->upload(['contents' => $bytes, 'filename' => 'proof.jpg'], 'evidence');
mainToken is visible to the user.
Always validate sensitive actions on the server.
Webhooks
Chastify sends events (lock.time_changed, lock.frozen, task.assigned,
task.completed, task.failed, hygiene.started, dice.rolledβ¦) with an
x-webhook-token-hash header. The SDK verifies that hash and parses the event. Delivery is
at-least-once: dedupe on event.id.
import { ChastifyWebhooks } from "@ecosplay/chastify-sdk";
app.post("/chastify/webhook", express.raw({ type: "*/*" }), (req, res) => {
const event = ChastifyWebhooks.constructEvent(
req.body, req.headers, process.env.CHASTIFY_WEBHOOK_TOKEN!,
); // throws WebhookVerificationError on hash mismatch
if (event.event === "task.completed") { /* β¦ */ }
res.sendStatus(200);
});
use Ecosplay\Chastify\Webhooks;
$event = Webhooks::constructEvent(
file_get_contents('php://input'),
getallheaders(),
getenv('CHASTIFY_WEBHOOK_TOKEN'),
); // throws WebhookVerificationException on hash mismatch
if ($event['event'] === 'task.completed') { /* β¦ */ }
http_response_code(200);
Errors & limits
Every non-2xx response throws a typed error (Error suffix in JS, Exception in PHP) carrying status, code and message.
| Class | HTTP | Typical cases |
|---|---|---|
Authentication | 401 | missing_token, invalid_token, revoked_token |
Authorization | 403 | insufficient_scope, not_authorized |
NotFound | 404 | lock_not_found, no_device |
Conflict | 409 | no_active_lock_session, lock_ended |
Validation | 400 / 422 | invalid_params, unsupported_device |
RateLimit | 429 | retried automatically |
DeviceTimeout | 504 | device_timeout, device_offline |
Server / Network / Timeout | 5xx / β | retried automatically |
API limits per minute: read 300, write 120, action 30, upload 10. Don't blindly retry an action that may already have succeeded.
Working on the SDKs
Tests run entirely against a bundled mock server β the real API is never called.
git clone https://code.e-cosplay.fr/shoko/chastify-sdk-js.git
cd chastify-sdk-js && bun install && bun test
git clone https://code.e-cosplay.fr/shoko/chastify-sdk-php.git
cd chastify-sdk-php && composer install && composer test
If the Chastify API evolves, the SDKs will be updated on a best-effort basis, as the association's volunteer time allows. Suggestions and reports: contact@e-cosplay.fr.