Skip to content

API Specification

Version: 1.0.0-draft Date: 2026-03-12 Status: Draft Base URL: https://api.betblocker.com/v1 (hosted) or https://<self-hosted>/v1 Protocol: HTTPS only (TLS 1.3 minimum)


  1. Conventions
  2. Authentication Scheme
  3. Shared Data Models
  4. Endpoint Groups
  5. Error Handling
  6. Rate Limiting
  7. Pagination
  8. Versioning

  • All request bodies are JSON (Content-Type: application/json).
  • Path parameters use snake_case: /devices/:device_id.
  • Query parameters use snake_case: ?from_version=42.
  • All timestamps are ISO 8601 in UTC: 2026-03-12T14:30:00Z.
  • UUIDs are v7 (time-ordered) unless otherwise noted.

Every successful response uses a consistent envelope:

{
"data": { ... },
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-03-12T14:30:00Z"
}
}

Paginated list responses:

{
"data": [ ... ],
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-03-12T14:30:00Z"
},
"pagination": {
"total": 142,
"page": 1,
"per_page": 50,
"total_pages": 3
}
}
{
"error": {
"code": "ENROLLMENT_NOT_FOUND",
"message": "No enrollment found with the given ID.",
"details": { ... }
},
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-03-12T14:30:00Z"
}
}
Auth TypeHeader
User JWTAuthorization: Bearer <jwt>
Device mTLSMutual TLS client certificate (certificate CN = device ID)
Device TokenX-Device-Token: <token> (fallback for platforms without mTLS)
Admin JWTAuthorization: Bearer <jwt> (JWT contains role: admin claim)

Access tokens are short-lived (15 minutes). Refresh tokens are long-lived (30 days) and stored hashed in the database.

Access Token Claims:

{
"sub": "acc_uuid",
"email": "user@example.com",
"role": "user | partner | authority | admin",
"iat": 1710254400,
"exp": 1710255300,
"jti": "tok_uuid"
}

Refresh Token: Opaque 256-bit random string, stored as SHA-256 hash in PostgreSQL. Bound to a specific user agent and IP prefix for rotation detection.

Devices authenticate via one of two mechanisms:

  1. mTLS (preferred): Device presents a client certificate issued during registration. Certificate CN contains the device ID. Certificate is signed by the BetBlocker device CA.
  2. Device Token (fallback): For platforms where mTLS is impractical (some mobile contexts), a long-lived opaque token is issued at registration and stored in hardware-backed keystore. Sent via X-Device-Token header.

Both mechanisms bind the device identity to the request. The API validates device ownership against the authenticated account (for user-initiated requests) or directly against the device record (for agent-initiated requests like heartbeat).


{
"id": "acc_01H...",
"email": "user@example.com",
"display_name": "Jane Doe",
"role": "user",
"email_verified": true,
"mfa_enabled": false,
"timezone": "America/New_York",
"locale": "en-US",
"organization_id": null,
"subscription_tier": "standard",
"created_at": "2026-03-12T14:30:00Z",
"updated_at": "2026-03-12T14:30:00Z"
}
FieldTypeDescription
idstring (UUID)Unique account identifier, prefixed acc_
emailstringUnique email address
display_namestringUser-chosen display name (2-100 chars)
roleenumOne of: user, partner, authority, admin
email_verifiedbooleanWhether email has been verified
mfa_enabledbooleanWhether MFA is configured
timezonestringIANA timezone identifier
localestringBCP 47 locale tag
organization_idstring (UUID) | nullAssociated organization, if any
subscription_tierenumOne of: free, standard, partner_tier, institutional
created_atstring (datetime)Account creation timestamp
updated_atstring (datetime)Last modification timestamp
{
"id": "dev_01H...",
"account_id": "acc_01H...",
"name": "Jane's MacBook Pro",
"platform": "macos",
"os_version": "15.3.1",
"agent_version": "1.2.0",
"hostname": "janes-mbp.local",
"status": "active",
"blocklist_version": 1247,
"last_heartbeat_at": "2026-03-12T14:25:00Z",
"certificate_fingerprint": "sha256:ab12cd34...",
"enrollment_id": "enr_01H...",
"created_at": "2026-03-12T10:00:00Z",
"updated_at": "2026-03-12T14:25:00Z"
}
FieldTypeDescription
idstring (UUID)Unique device identifier, prefixed dev_
account_idstring (UUID)Owning account
namestringUser-friendly device name (1-100 chars)
platformenumOne of: windows, macos, linux, android, ios
os_versionstringOS version string
agent_versionstringSemver of installed agent
hostnamestringDevice hostname
statusenumOne of: pending, active, offline, unenrolling, unenrolled
blocklist_versionintegerLatest blocklist version confirmed by device
last_heartbeat_atstring (datetime) | nullLast successful heartbeat
certificate_fingerprintstring | nullSHA-256 fingerprint of device mTLS cert
enrollment_idstring (UUID) | nullCurrently active enrollment
created_atstring (datetime)Registration timestamp
updated_atstring (datetime)Last modification timestamp
{
"id": "enr_01H...",
"device_id": "dev_01H...",
"account_id": "acc_01H...",
"enrolled_by": "acc_01H...",
"tier": "partner",
"status": "active",
"protection_config": {
"dns_blocking": true,
"app_blocking": true,
"browser_blocking": false,
"vpn_detection": "alert",
"tamper_response": "alert_partner"
},
"reporting_config": {
"level": "aggregated",
"blocked_attempt_counts": true,
"domain_details": false,
"tamper_alerts": true
},
"unenrollment_policy": {
"type": "partner_approval",
"cooldown_hours": null,
"requires_approval_from": "acc_01H..."
},
"unenrollment_request": null,
"created_at": "2026-03-12T10:00:00Z",
"updated_at": "2026-03-12T10:00:00Z",
"expires_at": null
}
FieldTypeDescription
idstring (UUID)Unique enrollment identifier, prefixed enr_
device_idstring (UUID)The enrolled device
account_idstring (UUID)The device owner
enrolled_bystring (UUID)Account that created the enrollment (self, partner, or authority)
tierenumOne of: self, partner, authority
statusenumOne of: pending, active, unenroll_requested, unenroll_approved, unenrolling, unenrolled, expired
protection_configProtectionConfigWhat blocking layers are active and how bypass attempts are handled
reporting_configReportingConfigWhat data is visible and to whom
unenrollment_policyUnenrollmentPolicyRules governing how unenrollment works
unenrollment_requestUnenrollmentRequest | nullPresent when unenrollment has been requested
created_atstring (datetime)Enrollment creation timestamp
updated_atstring (datetime)Last modification timestamp
expires_atstring (datetime) | nullOptional expiration (authority tier may set)

ProtectionConfig:

FieldTypeDescription
dns_blockingbooleanDNS/network layer active
app_blockingbooleanApplication blocking active (Phase 2)
browser_blockingbooleanBrowser content blocking active (Phase 3)
vpn_detectionenumdisabled, log, alert, lockdown
tamper_responseenumlog, alert_user, alert_partner, alert_authority

ReportingConfig:

FieldTypeDescription
levelenumnone, aggregated, detailed, full_audit
blocked_attempt_countsbooleanInclude count of blocked attempts
domain_detailsbooleanInclude specific blocked domains
tamper_alertsbooleanReport tamper detection events

UnenrollmentPolicy:

FieldTypeDescription
typeenumtime_delayed, partner_approval, authority_approval
cooldown_hoursinteger | nullHours to wait before completing unenrollment (self tier, 24-72)
requires_approval_fromstring (UUID) | nullAccount that must approve (partner/authority tiers)

UnenrollmentRequest:

FieldTypeDescription
requested_atstring (datetime)When unenrollment was requested
requested_bystring (UUID)Who requested it
reasonstring | nullOptional reason
eligible_atstring (datetime) | nullWhen time-delayed unenrollment completes
approved_atstring (datetime) | nullWhen approval was granted
approved_bystring (UUID) | nullWho approved
{
"id": "evt_01H...",
"device_id": "dev_01H...",
"enrollment_id": "enr_01H...",
"type": "block",
"category": "dns",
"severity": "info",
"payload": {
"domain": "example-casino.com",
"query_type": "A",
"source_app": "com.google.chrome",
"blocklist_rule_id": "blk_01H..."
},
"occurred_at": "2026-03-12T14:30:00Z",
"received_at": "2026-03-12T14:30:01Z"
}
FieldTypeDescription
idstring (UUID)Unique event identifier, prefixed evt_
device_idstring (UUID)Source device
enrollment_idstring (UUID)Associated enrollment
typeenumSee Event Types below
categoryenumdns, app, browser, tamper, enrollment, heartbeat, system
severityenuminfo, warning, critical
payloadobjectType-specific structured data
occurred_atstring (datetime)When event occurred on device
received_atstring (datetime)When API received event

Event Types:

TypeDescriptionCategory
blockGambling domain/app/content blockeddns, app, browser
bypass_attemptUser attempted to bypass blockingdns, app, browser
tamper_detectedAgent tampering detectedtamper
tamper_self_healedAgent recovered from tamper attempttamper
vpn_detectedVPN/proxy/Tor activity detecteddns
enrollment_createdNew enrollment activatedenrollment
enrollment_modifiedEnrollment config changedenrollment
unenroll_requestedUnenrollment requestedenrollment
unenroll_completedUnenrollment completedenrollment
heartbeatPeriodic status reportheartbeat
agent_startedAgent process startedsystem
agent_updatedAgent updated to new versionsystem
blocklist_updatedBlocklist synced to new versionsystem
{
"id": "blk_01H...",
"domain": "example-casino.com",
"pattern": null,
"category": "online_casino",
"source": "curated",
"confidence": 1.0,
"status": "active",
"added_by": "acc_01H...",
"reviewed_by": "acc_01H...",
"evidence_url": "https://...",
"tags": ["casino", "slots", "uk-licensed"],
"blocklist_version_added": 1200,
"blocklist_version_removed": null,
"created_at": "2026-03-12T10:00:00Z",
"updated_at": "2026-03-12T10:00:00Z"
}
FieldTypeDescription
idstring (UUID)Unique entry identifier, prefixed blk_
domainstring | nullExact domain to block (mutually exclusive with pattern)
patternstring | nullGlob/regex pattern for wildcard blocking
categoryenumonline_casino, sports_betting, poker, lottery, bingo, fantasy_sports, crypto_gambling, affiliate, payment_processor, other
sourceenumcurated (manual), automated (discovery pipeline), federated (agent report), community (public list import)
confidencefloat0.0-1.0 confidence score. Curated entries are 1.0.
statusenumpending_review, active, inactive, rejected
added_bystring (UUID) | nullAccount that added the entry
reviewed_bystring (UUID) | nullAccount that reviewed/approved
evidence_urlstring | nullURL to evidence supporting the classification
tagsstring[]Freeform classification tags
blocklist_version_addedintegerBlocklist version when entry was activated
blocklist_version_removedinteger | nullBlocklist version when entry was deactivated
created_atstring (datetime)Creation timestamp
updated_atstring (datetime)Last modification timestamp
{
"id": "org_01H...",
"name": "Recovery Center of Austin",
"type": "therapy_practice",
"owner_id": "acc_01H...",
"member_count": 12,
"device_count": 34,
"settings": {
"default_enrollment_tier": "authority",
"default_protection_config": { ... },
"default_reporting_config": { ... }
},
"created_at": "2026-03-12T10:00:00Z",
"updated_at": "2026-03-12T10:00:00Z"
}
FieldTypeDescription
idstring (UUID)Unique org identifier, prefixed org_
namestringOrganization display name (2-200 chars)
typeenumtherapy_practice, court_program, family, employer, other
owner_idstring (UUID)Account that owns the org
member_countintegerNumber of member accounts
device_countintegerNumber of devices under org enrollments
settingsOrgSettingsDefault config for enrollments created through this org
created_atstring (datetime)Creation timestamp
updated_atstring (datetime)Last modification timestamp
{
"id": "ptr_01H...",
"account_id": "acc_01H...",
"partner_account_id": "acc_01H...",
"status": "active",
"role": "accountability_partner",
"permissions": {
"view_reports": true,
"approve_unenrollment": true,
"modify_enrollment": false
},
"invited_by": "acc_01H...",
"invited_at": "2026-03-12T10:00:00Z",
"accepted_at": "2026-03-12T11:00:00Z",
"created_at": "2026-03-12T10:00:00Z",
"updated_at": "2026-03-12T11:00:00Z"
}
FieldTypeDescription
idstring (UUID)Unique partner relationship identifier, prefixed ptr_
account_idstring (UUID)The user who has the partner
partner_account_idstring (UUID)The partner’s account
statusenumpending, active, revoked
roleenumaccountability_partner, therapist, authority_rep
permissionsPartnerPermissionsWhat the partner can do
invited_bystring (UUID)Who initiated the relationship
invited_atstring (datetime)Invitation timestamp
accepted_atstring (datetime) | nullAcceptance timestamp
created_atstring (datetime)Creation timestamp
updated_atstring (datetime)Last modification timestamp

PartnerPermissions:

FieldTypeDescription
view_reportsbooleanCan view enrollment reports
approve_unenrollmentbooleanCan approve unenrollment requests
modify_enrollmentbooleanCan modify enrollment protection/reporting config

All auth endpoints are unauthenticated (no JWT required) unless noted.


Create a new account.

Auth: None

Request Body:

{
"email": "user@example.com",
"password": "strongPassw0rd!",
"display_name": "Jane Doe",
"timezone": "America/New_York",
"locale": "en-US"
}
FieldTypeRequiredValidation
emailstringYesValid email, max 255 chars, unique
passwordstringYesMin 12 chars, must contain uppercase, lowercase, digit, and special char
display_namestringYes2-100 chars
timezonestringNoValid IANA timezone. Default: UTC
localestringNoValid BCP 47 tag. Default: en-US

Response: 201 Created

{
"data": {
"account": {
"id": "acc_01H...",
"email": "user@example.com",
"display_name": "Jane Doe",
"role": "user",
"email_verified": false,
"created_at": "2026-03-12T14:30:00Z"
},
"access_token": "eyJ...",
"refresh_token": "rtk_...",
"expires_in": 900
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input (details include field-level errors)
409EMAIL_ALREADY_EXISTSAn account with this email already exists
429RATE_LIMIT_EXCEEDEDToo many registration attempts

Rate Limit: 5 requests per IP per hour.

Notes: A verification email is sent asynchronously. The account is functional immediately but certain features (partner invitations) require verified email.


Authenticate and receive tokens.

Auth: None

Request Body:

{
"email": "user@example.com",
"password": "strongPassw0rd!",
"mfa_code": "123456"
}
FieldTypeRequiredValidation
emailstringYesValid email
passwordstringYesNon-empty
mfa_codestringConditionalRequired if account has MFA enabled. 6-digit TOTP code.

Response: 200 OK

{
"data": {
"account": {
"id": "acc_01H...",
"email": "user@example.com",
"display_name": "Jane Doe",
"role": "user",
"email_verified": true,
"mfa_enabled": false
},
"access_token": "eyJ...",
"refresh_token": "rtk_...",
"expires_in": 900
}
}

Errors:

HTTP StatusCodeDescription
401INVALID_CREDENTIALSEmail or password is incorrect
401MFA_REQUIREDAccount has MFA enabled; mfa_code must be provided
401MFA_INVALIDThe provided MFA code is incorrect or expired
403ACCOUNT_LOCKEDToo many failed attempts; account temporarily locked
429RATE_LIMIT_EXCEEDEDToo many login attempts

Rate Limit: 10 requests per email per 15 minutes. 30 requests per IP per 15 minutes. After 5 consecutive failures for an email, lock account for 15 minutes.

Notes: The error response for INVALID_CREDENTIALS intentionally does not distinguish between “email not found” and “wrong password” to prevent user enumeration.


Exchange a valid refresh token for a new access token. Implements refresh token rotation: the old refresh token is invalidated and a new one is issued.

Auth: None (refresh token in body)

Request Body:

{
"refresh_token": "rtk_..."
}
FieldTypeRequiredValidation
refresh_tokenstringYesNon-empty

Response: 200 OK

{
"data": {
"access_token": "eyJ...",
"refresh_token": "rtk_...",
"expires_in": 900
}
}

Errors:

HTTP StatusCodeDescription
401INVALID_REFRESH_TOKENToken is invalid, expired, or already used
401TOKEN_FAMILY_REVOKEDReuse of a rotated token detected; entire token family revoked (potential theft)

Rate Limit: 20 requests per account per hour.

Notes: If a previously rotated refresh token is reused, this indicates potential token theft. The API revokes the entire token family (all refresh tokens for that account) and returns TOKEN_FAMILY_REVOKED, forcing re-authentication.


Revoke the current refresh token.

Auth: User JWT

Request Body:

{
"refresh_token": "rtk_..."
}
FieldTypeRequiredValidation
refresh_tokenstringYesNon-empty

Response: 204 No Content

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token

Rate Limit: Standard (see section 6).


Request a password reset email.

Auth: None

Request Body:

{
"email": "user@example.com"
}
FieldTypeRequiredValidation
emailstringYesValid email

Response: 202 Accepted

{
"data": {
"message": "If an account with that email exists, a reset link has been sent."
}
}

Errors:

HTTP StatusCodeDescription
429RATE_LIMIT_EXCEEDEDToo many reset requests

Rate Limit: 3 requests per email per hour. 10 requests per IP per hour.

Notes: Always returns 202 regardless of whether the email exists, to prevent user enumeration. The reset token is a 256-bit random value, valid for 1 hour, stored as SHA-256 hash in the database.


Reset password using a token received via email.

Auth: None

Request Body:

{
"token": "rst_...",
"new_password": "newStrongPassw0rd!"
}
FieldTypeRequiredValidation
tokenstringYesNon-empty
new_passwordstringYesSame password requirements as registration

Response: 200 OK

{
"data": {
"message": "Password has been reset. Please log in with your new password."
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORNew password does not meet requirements
401INVALID_RESET_TOKENToken is invalid, expired, or already used
429RATE_LIMIT_EXCEEDEDToo many reset attempts

Rate Limit: 5 requests per IP per hour.

Notes: On successful reset, all existing refresh tokens for the account are revoked (forces re-login on all sessions). The reset token is single-use.



Get the currently authenticated user’s full profile.

Auth: User JWT

Response: 200 OK

{
"data": {
"id": "acc_01H...",
"email": "user@example.com",
"display_name": "Jane Doe",
"role": "user",
"email_verified": true,
"mfa_enabled": false,
"timezone": "America/New_York",
"locale": "en-US",
"organization_id": null,
"subscription_tier": "standard",
"created_at": "2026-03-12T14:30:00Z",
"updated_at": "2026-03-12T14:30:00Z"
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token

Rate Limit: Standard.


Update the currently authenticated user’s profile. Partial update: only provided fields are changed.

Auth: User JWT

Request Body:

{
"display_name": "Jane D.",
"timezone": "Europe/London",
"locale": "en-GB",
"current_password": "oldPassw0rd!",
"new_password": "newStrongPassw0rd!"
}
FieldTypeRequiredValidation
display_namestringNo2-100 chars
timezonestringNoValid IANA timezone
localestringNoValid BCP 47 tag
current_passwordstringConditionalRequired when changing password or email
new_passwordstringNoSame requirements as registration
emailstringNoValid email, unique. Requires current_password. Triggers re-verification.

Response: 200 OK

{
"data": {
"id": "acc_01H...",
"email": "user@example.com",
"display_name": "Jane D.",
"role": "user",
"email_verified": true,
"mfa_enabled": false,
"timezone": "Europe/London",
"locale": "en-GB",
"organization_id": null,
"subscription_tier": "standard",
"created_at": "2026-03-12T14:30:00Z",
"updated_at": "2026-03-12T15:00:00Z"
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401UNAUTHORIZEDInvalid or expired access token
401INCORRECT_PASSWORDcurrent_password is wrong
409EMAIL_ALREADY_EXISTSThe new email is already in use

Rate Limit: 10 requests per account per hour.


View another account’s profile. Only accessible to partners who have an active relationship with the target account, authority representatives with active enrollment oversight, or admins.

Auth: User JWT (partner, authority, or admin role)

Path Parameters:

ParamTypeDescription
idstring (UUID)Target account ID

Response: 200 OK

Returns a filtered view of the Account model. Partners see id, display_name, email_verified, and created_at. Authority representatives see the same plus email. Admins see the full model.

{
"data": {
"id": "acc_01H...",
"display_name": "John Smith",
"email_verified": true,
"created_at": "2026-03-12T14:30:00Z"
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNo active partner/authority relationship with this account
404ACCOUNT_NOT_FOUNDAccount does not exist

Rate Limit: Standard.



Register a new device. Called by the agent during initial setup.

Auth: User JWT

Request Body:

{
"name": "Jane's MacBook Pro",
"platform": "macos",
"os_version": "15.3.1",
"agent_version": "1.2.0",
"hostname": "janes-mbp.local",
"hardware_id": "hw_sha256_...",
"csr": "-----BEGIN CERTIFICATE REQUEST-----\n..."
}
FieldTypeRequiredValidation
namestringYes1-100 chars
platformenumYesOne of: windows, macos, linux, android, ios
os_versionstringYes1-50 chars
agent_versionstringYesValid semver
hostnamestringYes1-255 chars
hardware_idstringYesSHA-256 hash of hardware identifiers (prevents duplicate registrations)
csrstringNoPEM-encoded certificate signing request for mTLS. If omitted, a device token is issued instead.

Response: 201 Created

{
"data": {
"device": {
"id": "dev_01H...",
"account_id": "acc_01H...",
"name": "Jane's MacBook Pro",
"platform": "macos",
"os_version": "15.3.1",
"agent_version": "1.2.0",
"hostname": "janes-mbp.local",
"status": "pending",
"enrollment_id": null,
"created_at": "2026-03-12T14:30:00Z"
},
"certificate": "-----BEGIN CERTIFICATE-----\n...",
"device_token": null,
"api_endpoints": {
"heartbeat": "/v1/devices/dev_01H.../heartbeat",
"config": "/v1/devices/dev_01H.../config",
"events": "/v1/events",
"blocklist": "/v1/blocklist"
}
}
}

If csr was provided, certificate contains the signed device certificate and device_token is null. If csr was omitted, certificate is null and device_token contains the opaque token.

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401UNAUTHORIZEDInvalid or expired access token
409DEVICE_ALREADY_REGISTEREDA device with this hardware_id is already registered to this account
422INVALID_CSRThe CSR is malformed or contains invalid data

Rate Limit: 5 devices per account per hour.


List all devices for the authenticated user. Partners and authorities can also see devices they have enrollment oversight for.

Auth: User JWT

Query Parameters:

ParamTypeDefaultDescription
statusenum(all)Filter by status: pending, active, offline, unenrolling, unenrolled
platformenum(all)Filter by platform
pageinteger1Page number
per_pageinteger50Items per page (max 100)

Response: 200 OK

{
"data": [
{
"id": "dev_01H...",
"account_id": "acc_01H...",
"name": "Jane's MacBook Pro",
"platform": "macos",
"status": "active",
"agent_version": "1.2.0",
"blocklist_version": 1247,
"last_heartbeat_at": "2026-03-12T14:25:00Z",
"enrollment_id": "enr_01H...",
"created_at": "2026-03-12T10:00:00Z"
}
],
"pagination": {
"total": 3,
"page": 1,
"per_page": 50,
"total_pages": 1
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token

Rate Limit: Standard.


Get full detail for a specific device.

Auth: User JWT (device owner, partner with relationship, authority with enrollment oversight, or admin)

Path Parameters:

ParamTypeDescription
idstring (UUID)Device ID

Response: 200 OK

Returns the full Device model as described in section 3.2.

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to view this device
404DEVICE_NOT_FOUNDDevice does not exist

Rate Limit: Standard.


Begin device unenrollment. This does not immediately delete the device; it triggers the enrollment’s unenrollment policy (time delay or approval). The device transitions to unenrolling status.

Auth: User JWT (device owner or admin)

Path Parameters:

ParamTypeDescription
idstring (UUID)Device ID

Request Body (optional):

{
"reason": "Switching to a new device."
}
FieldTypeRequiredValidation
reasonstringNoMax 500 chars

Response: 200 OK

{
"data": {
"device": {
"id": "dev_01H...",
"status": "unenrolling"
},
"unenrollment": {
"type": "time_delayed",
"eligible_at": "2026-03-14T14:30:00Z",
"message": "Unenrollment will complete after 48-hour cooling-off period."
}
}
}

For partner/authority enrollments:

{
"data": {
"device": {
"id": "dev_01H...",
"status": "unenrolling"
},
"unenrollment": {
"type": "partner_approval",
"requires_approval_from": "acc_01H...",
"message": "Your accountability partner has been notified and must approve this request."
}
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to unenroll this device
404DEVICE_NOT_FOUNDDevice does not exist
409ALREADY_UNENROLLINGAn unenrollment request is already in progress
409NO_ACTIVE_ENROLLMENTDevice has no active enrollment

Rate Limit: 3 requests per device per day.


Agent sends periodic heartbeat with status information. Used for device health monitoring and dead-man’s switch alerting.

Auth: Device cert or device token

Path Parameters:

ParamTypeDescription
idstring (UUID)Device ID (must match authenticated device)

Request Body:

{
"agent_version": "1.2.0",
"os_version": "15.3.1",
"blocklist_version": 1247,
"uptime_seconds": 86400,
"blocking_active": true,
"integrity_check": {
"binary_hash": "sha256:...",
"config_hash": "sha256:...",
"valid": true
},
"stats": {
"blocks_since_last_heartbeat": 14,
"dns_queries_since_last_heartbeat": 4821
}
}
FieldTypeRequiredValidation
agent_versionstringYesValid semver
os_versionstringYes1-50 chars
blocklist_versionintegerYesNon-negative
uptime_secondsintegerYesNon-negative
blocking_activebooleanYesWhether all blocking layers are functioning
integrity_checkIntegrityCheckYesAgent self-integrity verification
statsHeartbeatStatsNoOptional runtime statistics

Response: 200 OK

{
"data": {
"ack": true,
"server_time": "2026-03-12T14:30:00Z",
"next_heartbeat_seconds": 300,
"commands": [
{
"type": "update_blocklist",
"params": { "target_version": 1250 }
}
]
}
}
FieldTypeDescription
ackbooleanHeartbeat acknowledged
server_timestring (datetime)Server timestamp for clock drift detection
next_heartbeat_secondsintegerRecommended interval before next heartbeat
commandsCommand[]Pending commands for the agent to execute

Command Types:

TypeDescription
update_blocklistAgent should sync to target blocklist version
update_agentAgent update available
refresh_configEnrollment config has changed; agent should fetch /config
revoke_certificateDevice certificate has been revoked; agent should re-register

Errors:

HTTP StatusCodeDescription
401DEVICE_UNAUTHORIZEDInvalid device certificate or token
403DEVICE_ID_MISMATCHAuthenticated device does not match path parameter
404DEVICE_NOT_FOUNDDevice does not exist

Rate Limit: 120 requests per device per hour (minimum interval ~30 seconds).


Get the full active configuration for a device, including enrollment settings, protection config, and reporting config. Called by the agent on startup and when instructed via heartbeat command.

Auth: Device cert or device token

Path Parameters:

ParamTypeDescription
idstring (UUID)Device ID (must match authenticated device)

Response: 200 OK

{
"data": {
"device_id": "dev_01H...",
"enrollment": {
"id": "enr_01H...",
"tier": "partner",
"status": "active",
"protection_config": {
"dns_blocking": true,
"app_blocking": true,
"browser_blocking": false,
"vpn_detection": "alert",
"tamper_response": "alert_partner"
},
"reporting_config": {
"level": "aggregated",
"blocked_attempt_counts": true,
"domain_details": false,
"tamper_alerts": true
}
},
"blocklist": {
"current_version": 1250,
"download_url": "/v1/blocklist/delta?from_version=1247"
},
"heartbeat": {
"interval_seconds": 300,
"missed_threshold": 3
},
"agent_update": {
"latest_version": "1.3.0",
"download_url": "https://cdn.betblocker.com/agent/1.3.0/macos/betblocker-agent",
"signature": "sha256:...",
"mandatory": false
}
}
}

Errors:

HTTP StatusCodeDescription
401DEVICE_UNAUTHORIZEDInvalid device certificate or token
403DEVICE_ID_MISMATCHAuthenticated device does not match path parameter
404DEVICE_NOT_FOUNDDevice does not exist

Rate Limit: 30 requests per device per hour.



Create a new enrollment. Can be self-enrollment (user enrolls their own device) or partner/authority-initiated (partner enrolls a user’s device with the user’s prior consent via partner relationship).

Auth: User JWT

Request Body:

{
"device_id": "dev_01H...",
"tier": "partner",
"protection_config": {
"dns_blocking": true,
"app_blocking": false,
"browser_blocking": false,
"vpn_detection": "alert",
"tamper_response": "alert_partner"
},
"reporting_config": {
"level": "aggregated",
"blocked_attempt_counts": true,
"domain_details": false,
"tamper_alerts": true
},
"unenrollment_policy": {
"type": "partner_approval",
"cooldown_hours": null,
"requires_approval_from": "acc_01H..."
},
"expires_at": null
}
FieldTypeRequiredValidation
device_idstring (UUID)YesMust be a registered device
tierenumYesself, partner, authority
protection_configProtectionConfigNoDefaults per tier if omitted
reporting_configReportingConfigNoDefaults per tier if omitted
unenrollment_policyUnenrollmentPolicyNoDefaults per tier if omitted
expires_atstring (datetime)NoOptional expiration

Tier Defaults:

Tierdns_blockingvpn_detectiontamper_responsereporting levelunenrollment typecooldown_hours
selftrueloglognonetime_delayed48
partnertruealertalert_partneraggregatedpartner_approvaln/a
authoritytruelockdownalert_authorityfull_auditauthority_approvaln/a

Response: 201 Created

Returns the full Enrollment model as described in section 3.3.

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to enroll this device (e.g., partner tier requires active partner relationship)
404DEVICE_NOT_FOUNDDevice does not exist
409DEVICE_ALREADY_ENROLLEDDevice already has an active enrollment
422INVALID_TIER_CONFIGConfiguration is not valid for the selected tier (e.g., self tier cannot set authority_approval unenrollment)

Rate Limit: 10 requests per account per hour.

Notes:

  • For self tier: the authenticated user must own the device.
  • For partner tier: the authenticated user must have an active partner relationship with the device owner, and the partner relationship must include approve_unenrollment permission.
  • For authority tier: the authenticated user must be an authority representative with appropriate organization membership.
  • cooldown_hours for self tier must be between 24 and 72 (inclusive).

List enrollments visible to the authenticated user. Users see their own enrollments. Partners see enrollments where they are the enrolled_by or approval authority. Admins see all.

Auth: User JWT

Query Parameters:

ParamTypeDefaultDescription
statusenum(all)Filter by enrollment status
tierenum(all)Filter by tier
device_idstring (UUID)(all)Filter by specific device
pageinteger1Page number
per_pageinteger50Items per page (max 100)

Response: 200 OK

{
"data": [
{ "...Enrollment object..." }
],
"pagination": {
"total": 5,
"page": 1,
"per_page": 50,
"total_pages": 1
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token

Rate Limit: Standard.


Get full detail for a specific enrollment.

Auth: User JWT (enrollment owner, enrolled_by account, approval authority, or admin)

Path Parameters:

ParamTypeDescription
idstring (UUID)Enrollment ID

Response: 200 OK

Returns the full Enrollment model as described in section 3.3.

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to view this enrollment
404ENROLLMENT_NOT_FOUNDEnrollment does not exist

Rate Limit: Standard.


Modify an active enrollment’s configuration. Who can modify depends on the tier:

  • Self tier: The enrolled user can modify protection and reporting config.
  • Partner tier: The partner (enrolled_by) can modify protection and reporting config. The enrolled user can request changes, which the partner must approve (out of scope for Phase 1).
  • Authority tier: Only the authority representative can modify.

Auth: User JWT

Path Parameters:

ParamTypeDescription
idstring (UUID)Enrollment ID

Request Body:

{
"protection_config": {
"vpn_detection": "lockdown"
},
"reporting_config": {
"domain_details": true
},
"expires_at": "2027-03-12T00:00:00Z"
}
FieldTypeRequiredValidation
protection_configPartial<ProtectionConfig>NoPartial update; only provided fields change
reporting_configPartial<ReportingConfig>NoPartial update
unenrollment_policyPartial<UnenrollmentPolicy>NoOnly modifiable by enrolled_by (partner/authority). Self tier can change cooldown_hours within 24-72 range.
expires_atstring (datetime) | nullNoSet or clear expiration

Response: 200 OK

Returns the updated full Enrollment model.

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to modify this enrollment
404ENROLLMENT_NOT_FOUNDEnrollment does not exist
409ENROLLMENT_NOT_ACTIVEEnrollment is not in active status
422INVALID_TIER_CONFIGConfiguration change is invalid for the enrollment tier

Rate Limit: 10 requests per enrollment per hour.


Request unenrollment. Behavior depends on the enrollment tier:

  • Self tier: Starts the cooldown timer. After cooldown_hours, the enrollment completes unenrollment automatically.
  • Partner tier: Notifies the partner and waits for approval via POST /enrollments/:id/approve-unenroll.
  • Authority tier: Notifies the authority representative and waits for approval.

Auth: User JWT (enrollment owner)

Path Parameters:

ParamTypeDescription
idstring (UUID)Enrollment ID

Request Body:

{
"reason": "I have been in recovery for 2 years and feel confident."
}
FieldTypeRequiredValidation
reasonstringNoMax 1000 chars

Response: 200 OK

{
"data": {
"enrollment": {
"id": "enr_01H...",
"status": "unenroll_requested",
"unenrollment_request": {
"requested_at": "2026-03-12T14:30:00Z",
"requested_by": "acc_01H...",
"reason": "I have been in recovery for 2 years and feel confident.",
"eligible_at": "2026-03-14T14:30:00Z",
"approved_at": null,
"approved_by": null
}
},
"message": "Unenrollment request submitted. The 48-hour cooling-off period begins now."
}
}

For partner/authority tiers, eligible_at is null and message indicates that approval is required.

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to request unenrollment for this enrollment
404ENROLLMENT_NOT_FOUNDEnrollment does not exist
409ENROLLMENT_NOT_ACTIVEEnrollment is not in active status
409UNENROLL_ALREADY_REQUESTEDAn unenrollment request is already pending

Rate Limit: 3 requests per enrollment per day.


Approve a pending unenrollment request. Only callable by the account designated in unenrollment_policy.requires_approval_from.

Auth: User JWT (partner or authority representative)

Path Parameters:

ParamTypeDescription
idstring (UUID)Enrollment ID

Request Body:

{
"approved": true,
"note": "Recovery progress has been excellent. Approved."
}
FieldTypeRequiredValidation
approvedbooleanYestrue to approve, false to deny
notestringNoMax 1000 chars. Visible to the enrolled user.

Response: 200 OK

When approved:

{
"data": {
"enrollment": {
"id": "enr_01H...",
"status": "unenroll_approved",
"unenrollment_request": {
"requested_at": "2026-03-12T14:30:00Z",
"requested_by": "acc_01H...",
"reason": "I have been in recovery for 2 years.",
"eligible_at": null,
"approved_at": "2026-03-12T16:00:00Z",
"approved_by": "acc_01H..."
}
},
"message": "Unenrollment approved. The device will be unenrolled shortly."
}
}

When denied:

{
"data": {
"enrollment": {
"id": "enr_01H...",
"status": "active",
"unenrollment_request": null
},
"message": "Unenrollment request denied."
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot the designated approval authority for this enrollment
404ENROLLMENT_NOT_FOUNDEnrollment does not exist
409NO_PENDING_UNENROLLNo unenrollment request is pending

Rate Limit: Standard.



Get the current blocklist version number and metadata. Used by agents to determine if they need an update.

Auth: Device cert/token or User JWT

Response: 200 OK

{
"data": {
"version": 1250,
"entry_count": 48732,
"last_updated_at": "2026-03-12T12:00:00Z",
"signature": "sha256:...",
"size_bytes": 1048576
}
}
FieldTypeDescription
versionintegerMonotonically increasing version number
entry_countintegerTotal active entries
last_updated_atstring (datetime)When the blocklist was last compiled
signaturestringCryptographic signature for integrity verification
size_bytesintegerFull blocklist size in bytes

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid authentication

Rate Limit: 60 requests per device per hour.


Get incremental blocklist updates since a given version. Returns only the additions and removals between from_version and the current version.

Auth: Device cert/token or User JWT

Query Parameters:

ParamTypeRequiredValidation
from_versionintegerYesMust be > 0 and <= current version. If too old, returns FULL_SYNC_REQUIRED.

Response: 200 OK

{
"data": {
"from_version": 1247,
"to_version": 1250,
"additions": [
{
"domain": "new-casino-site.com",
"pattern": null,
"category": "online_casino"
},
{
"domain": null,
"pattern": "*.gambling-affiliate-network.net",
"category": "affiliate"
}
],
"removals": [
{
"domain": "false-positive-site.com"
}
],
"signature": "sha256:...",
"full_sync_url": "/v1/blocklist/full"
}
}
FieldTypeDescription
from_versionintegerThe version the delta is computed from
to_versionintegerThe version the delta brings you to
additionsDeltaEntry[]Entries added since from_version
removalsDeltaEntry[]Entries removed since from_version
signaturestringSignature covering the resulting full blocklist at to_version
full_sync_urlstringURL to download the full blocklist (fallback)

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORMissing or invalid from_version
401UNAUTHORIZEDInvalid authentication
410FULL_SYNC_REQUIREDfrom_version is too old; delta is unavailable. Agent must download full blocklist.

Rate Limit: 30 requests per device per hour.

Notes: The API retains delta history for the last 100 versions. Agents that fall further behind must perform a full sync.


Submit a federated report from an agent. When the agent’s heuristic engine encounters a domain it suspects is gambling-related but is not in the blocklist, it submits a report here for central review.

Auth: Device cert/token

Request Body:

{
"reports": [
{
"domain": "suspicious-gambling-site.com",
"detected_via": "heuristic",
"heuristic_score": 0.87,
"context": {
"matched_keywords": ["casino", "slots", "bonus"],
"redirect_chain": ["ad-network.com", "suspicious-gambling-site.com"],
"tls_cert_org": "Casino Holdings Ltd"
},
"occurred_at": "2026-03-12T14:25:00Z"
}
]
}
FieldTypeRequiredValidation
reportsFederatedReport[]Yes1-50 reports per request
reports[].domainstringYesValid domain name
reports[].detected_viaenumYesheuristic, redirect, content_match, user_report
reports[].heuristic_scorefloatNo0.0-1.0
reports[].contextobjectNoFreeform supporting evidence
reports[].occurred_atstring (datetime)YesWhen the detection occurred

Response: 202 Accepted

{
"data": {
"accepted": 1,
"duplicates": 0,
"message": "Reports queued for review."
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401DEVICE_UNAUTHORIZEDInvalid device authentication
422REPORTING_DISABLEDThe enrollment’s reporting config does not allow federated reports

Rate Limit: 60 requests per device per hour. Max 50 reports per request.

Notes: Reports are deduplicated by domain. The device ID and enrollment ID are recorded for provenance but are not exposed in the admin review queue (to preserve privacy). Reports from multiple devices increase the confidence score.


The following endpoints are restricted to admin users for managing the blocklist.


Create a new blocklist entry (curated).

Auth: Admin JWT

Request Body:

{
"domain": "new-gambling-site.com",
"pattern": null,
"category": "online_casino",
"evidence_url": "https://...",
"tags": ["casino", "uk-licensed"],
"notes": "Licensed UK gambling operator launched 2026-02."
}
FieldTypeRequiredValidation
domainstringConditionalRequired if pattern is null. Valid domain.
patternstringConditionalRequired if domain is null. Valid glob pattern.
categoryenumYesValid blocklist category
evidence_urlstringNoValid URL
tagsstring[]NoMax 20 tags, each max 50 chars
notesstringNoMax 2000 chars

Response: 201 Created

Returns the full BlocklistEntry model. Entry is created with status: active and confidence: 1.0 for curated entries.

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401UNAUTHORIZEDNot authenticated
403FORBIDDENNot an admin
409ENTRY_ALREADY_EXISTSDomain or pattern already exists in the blocklist

Rate Limit: 100 requests per admin per hour.


List and search blocklist entries.

Auth: Admin JWT

Query Parameters:

ParamTypeDefaultDescription
searchstring(none)Search domain/pattern text
categoryenum(all)Filter by category
sourceenum(all)Filter by source
statusenum(all)Filter by status
pageinteger1Page number
per_pageinteger50Items per page (max 200)

Response: 200 OK

Paginated list of BlocklistEntry objects.

Rate Limit: Standard.


Update a blocklist entry.

Auth: Admin JWT

Path Parameters:

ParamTypeDescription
idstring (UUID)Blocklist entry ID

Request Body:

{
"category": "sports_betting",
"status": "inactive",
"tags": ["sports", "decommissioned"]
}
FieldTypeRequiredValidation
categoryenumNoValid blocklist category
statusenumNoactive, inactive
tagsstring[]NoMax 20 tags
evidence_urlstringNoValid URL
notesstringNoMax 2000 chars

Response: 200 OK

Returns updated BlocklistEntry.

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDNot authenticated
403FORBIDDENNot an admin
404ENTRY_NOT_FOUNDEntry does not exist

Rate Limit: 100 requests per admin per hour.


Soft-delete a blocklist entry (sets status to inactive and records blocklist_version_removed).

Auth: Admin JWT

Path Parameters:

ParamTypeDescription
idstring (UUID)Blocklist entry ID

Response: 200 OK

Returns updated BlocklistEntry with status: inactive.

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDNot authenticated
403FORBIDDENNot an admin
404ENTRY_NOT_FOUNDEntry does not exist

Rate Limit: 100 requests per admin per hour.


List federated reports pending review.

Auth: Admin JWT

Query Parameters:

ParamTypeDefaultDescription
min_reportsinteger1Minimum number of agent reports for a domain
min_confidencefloat0.0Minimum aggregated confidence score
sortenumconfidence_descSort order: confidence_desc, reports_desc, oldest_first
pageinteger1Page number
per_pageinteger50Items per page (max 200)

Response: 200 OK

{
"data": [
{
"domain": "suspicious-site.com",
"report_count": 14,
"first_reported_at": "2026-03-10T08:00:00Z",
"last_reported_at": "2026-03-12T14:00:00Z",
"aggregated_confidence": 0.91,
"top_heuristic_matches": ["casino", "slots", "deposit bonus"],
"sample_context": {
"tls_cert_org": "Casino Holdings Ltd",
"redirect_chains": [["ad.net", "suspicious-site.com"]]
}
}
],
"pagination": { "..." }
}

Rate Limit: Standard.


POST /admin/blocklist/review-queue/:domain/resolve
Section titled “POST /admin/blocklist/review-queue/:domain/resolve”

Resolve a review queue item by promoting it to the blocklist or rejecting it.

Auth: Admin JWT

Path Parameters:

ParamTypeDescription
domainstringThe domain under review

Request Body:

{
"action": "promote",
"category": "online_casino",
"tags": ["casino"],
"notes": "Confirmed gambling site after manual review."
}
FieldTypeRequiredValidation
actionenumYespromote (add to blocklist) or reject (dismiss reports)
categoryenumConditionalRequired if action is promote
tagsstring[]NoMax 20 tags
notesstringNoMax 2000 chars

Response: 200 OK

If promoted, returns the created BlocklistEntry. If rejected, returns confirmation.

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDNot authenticated
403FORBIDDENNot an admin
404DOMAIN_NOT_IN_QUEUENo pending reports for this domain
409ENTRY_ALREADY_EXISTSDomain is already in the blocklist (for promote action)

Rate Limit: 100 requests per admin per hour.



Agent submits a batch of events. Events are written to TimescaleDB for time-series analysis.

Auth: Device cert/token

Request Body:

{
"events": [
{
"type": "block",
"category": "dns",
"severity": "info",
"payload": {
"domain": "example-casino.com",
"query_type": "A",
"source_app": "com.google.chrome"
},
"occurred_at": "2026-03-12T14:25:00Z"
},
{
"type": "tamper_detected",
"category": "tamper",
"severity": "critical",
"payload": {
"component": "dns_resolver",
"detail": "DNS config changed externally"
},
"occurred_at": "2026-03-12T14:26:00Z"
}
]
}
FieldTypeRequiredValidation
eventsEventInput[]Yes1-100 events per batch
events[].typeenumYesValid event type (see section 3.4)
events[].categoryenumYesValid event category
events[].severityenumYesinfo, warning, critical
events[].payloadobjectYesType-specific structured data, max 4KB per event
events[].occurred_atstring (datetime)YesMust be within last 7 days and not in the future

Response: 202 Accepted

{
"data": {
"accepted": 2,
"rejected": 0,
"errors": []
}
}

If some events fail validation, they are reported individually:

{
"data": {
"accepted": 1,
"rejected": 1,
"errors": [
{
"index": 1,
"code": "INVALID_EVENT_TYPE",
"message": "Unknown event type: 'foo'"
}
]
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERROREntire batch is invalid (e.g., empty events array)
401DEVICE_UNAUTHORIZEDInvalid device authentication
413PAYLOAD_TOO_LARGETotal request body exceeds 512KB

Rate Limit: 120 requests per device per hour. Max 100 events per request.

Notes: Events are subject to the enrollment’s reporting_config. The API filters out events that exceed the configured reporting level before storage. For example, if domain_details is false, the payload.domain field is stripped before persistence and replaced with a category-level aggregate.


Query events with filtering. Visibility is governed by the enrollment’s reporting config and the requester’s relationship to the enrollment.

Auth: User JWT

Query Parameters:

ParamTypeDefaultDescription
device_idstring (UUID)(all visible)Filter by device
enrollment_idstring (UUID)(all visible)Filter by enrollment
typeenum(all)Filter by event type
categoryenum(all)Filter by event category
severityenum(all)Filter by severity
fromstring (datetime)7 days agoStart of time range
tostring (datetime)nowEnd of time range
pageinteger1Page number
per_pageinteger50Items per page (max 200)

Response: 200 OK

{
"data": [
{
"id": "evt_01H...",
"device_id": "dev_01H...",
"enrollment_id": "enr_01H...",
"type": "block",
"category": "dns",
"severity": "info",
"payload": {
"domain": "example-casino.com",
"query_type": "A"
},
"occurred_at": "2026-03-12T14:25:00Z",
"received_at": "2026-03-12T14:25:01Z"
}
],
"pagination": { "..." }
}

Visibility Rules:

  • Enrollment owner (self tier, reporting = none): No events visible via API. Only local device display.
  • Enrollment owner (self tier, reporting = aggregated): Only aggregated counts, no domain details.
  • Partner: Sees events according to enrollment’s reporting_config. aggregated = counts only. detailed = domain details included.
  • Authority: Full access to all events per full_audit reporting level.
  • Admin: Full access to all events.

When the requester’s access level is aggregated, the payload field is replaced with:

{
"payload": {
"aggregated": true,
"count": 14,
"categories": { "online_casino": 10, "sports_betting": 4 }
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid query parameters
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to view events for this device/enrollment

Rate Limit: Standard.


Get aggregated event summary per enrollment. Useful for dashboard widgets.

Auth: User JWT

Query Parameters:

ParamTypeDefaultDescription
enrollment_idstring (UUID)(all visible)Filter by enrollment
device_idstring (UUID)(all visible)Filter by device
periodenumdayAggregation period: hour, day, week, month
fromstring (datetime)30 days agoStart of time range
tostring (datetime)nowEnd of time range

Response: 200 OK

{
"data": {
"enrollment_id": "enr_01H...",
"device_id": "dev_01H...",
"period": "day",
"from": "2026-02-12T00:00:00Z",
"to": "2026-03-12T23:59:59Z",
"summary": {
"total_blocks": 342,
"total_bypass_attempts": 2,
"total_tamper_events": 0,
"categories": {
"online_casino": 210,
"sports_betting": 98,
"affiliate": 34
}
},
"timeseries": [
{
"period_start": "2026-03-11T00:00:00Z",
"blocks": 18,
"bypass_attempts": 0,
"tamper_events": 0
},
{
"period_start": "2026-03-12T00:00:00Z",
"blocks": 24,
"bypass_attempts": 1,
"tamper_events": 0
}
]
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid query parameters
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to view events for this enrollment

Rate Limit: 30 requests per account per hour (aggregation queries are expensive).

Notes: The same visibility rules as GET /events apply. If the requester only has aggregated access, domain-level breakdowns are omitted and only category-level counts are shown.


These endpoints are specified now but will return 501 Not Implemented until Phase 2. The interface is defined to allow frontend development to proceed.


Create a new organization.

Auth: User JWT

Request Body:

{
"name": "Recovery Center of Austin",
"type": "therapy_practice",
"settings": {
"default_enrollment_tier": "authority",
"default_protection_config": { "..." },
"default_reporting_config": { "..." }
}
}
FieldTypeRequiredValidation
namestringYes2-200 chars
typeenumYestherapy_practice, court_program, family, employer, other
settingsOrgSettingsNoDefault enrollment configuration

Response: 201 Created

Returns the full Organization model. The creating account becomes the owner.

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401UNAUTHORIZEDInvalid or expired access token
501NOT_IMPLEMENTEDFeature not yet available (Phase 1)

List organizations the authenticated user belongs to.

Auth: User JWT

Response: 200 OK

Paginated list of Organization objects.


Get full detail for an organization.

Auth: User JWT (must be a member)

Path Parameters:

ParamTypeDescription
idstring (UUID)Organization ID

Response: 200 OK

Returns full Organization model.

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot a member of this organization
404ORGANIZATION_NOT_FOUNDOrganization does not exist

Update organization details.

Auth: User JWT (must be owner or admin of the org)

Request Body:

{
"name": "Austin Recovery Center",
"settings": { "..." }
}
FieldTypeRequiredValidation
namestringNo2-200 chars
typeenumNoValid org type
settingsOrgSettingsNoPartial update

Response: 200 OK

Returns updated Organization model.


Delete an organization. All member associations are removed. Active enrollments created through this org are not affected (they persist independently).

Auth: User JWT (must be owner)

Path Parameters:

ParamTypeDescription
idstring (UUID)Organization ID

Response: 204 No Content

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot the organization owner
404ORGANIZATION_NOT_FOUNDOrganization does not exist

Add a member to the organization.

Auth: User JWT (owner or admin of the org)

Request Body:

{
"account_id": "acc_01H...",
"role": "member"
}
FieldTypeRequiredValidation
account_idstring (UUID)YesMust be a valid account
roleenumYesowner, admin, member

Response: 201 Created

{
"data": {
"organization_id": "org_01H...",
"account_id": "acc_01H...",
"role": "member",
"added_at": "2026-03-12T14:30:00Z"
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to manage members
404ACCOUNT_NOT_FOUNDTarget account does not exist
409ALREADY_MEMBERAccount is already a member

List organization members.

Auth: User JWT (must be a member)

Response: 200 OK

Paginated list of member objects with account_id, role, and added_at.


DELETE /organizations/:id/members/:account_id

Section titled “DELETE /organizations/:id/members/:account_id”

Remove a member from the organization.

Auth: User JWT (owner or admin of the org)

Path Parameters:

ParamTypeDescription
idstring (UUID)Organization ID
account_idstring (UUID)Account to remove

Response: 204 No Content

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot authorized to manage members
403CANNOT_REMOVE_OWNERCannot remove the organization owner
404MEMBER_NOT_FOUNDAccount is not a member

Assign a device to an organization (for group management).

Auth: User JWT (owner or admin of the org)

Request Body:

{
"device_id": "dev_01H..."
}

Response: 200 OK

Errors:

HTTP StatusCodeDescription
403FORBIDDENNot authorized
404DEVICE_NOT_FOUNDDevice does not exist
409DEVICE_ALREADY_ASSIGNEDDevice is already assigned to an organization

List devices assigned to an organization.

Auth: User JWT (must be a member)

Response: 200 OK

Paginated list of Device objects.


These endpoints are only available on the hosted platform. Self-hosted deployments return 404 for all billing routes (the router does not register them when BILLING_ENABLED=false).


Create a new Stripe subscription.

Auth: User JWT

Request Body:

{
"plan": "standard",
"payment_method_id": "pm_..."
}
FieldTypeRequiredValidation
planenumYesstandard, partner_tier, institutional
payment_method_idstringYesStripe payment method ID (from Stripe.js on the frontend)

Response: 201 Created

{
"data": {
"subscription_id": "sub_...",
"plan": "standard",
"status": "active",
"current_period_start": "2026-03-12T00:00:00Z",
"current_period_end": "2026-04-12T00:00:00Z",
"price_cents": 1000,
"currency": "usd",
"cancel_at_period_end": false
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401UNAUTHORIZEDInvalid or expired access token
402PAYMENT_FAILEDStripe declined the payment method
409ALREADY_SUBSCRIBEDAccount already has an active subscription

Rate Limit: 5 requests per account per hour.


Get current subscription status.

Auth: User JWT

Response: 200 OK

{
"data": {
"has_subscription": true,
"subscription": {
"subscription_id": "sub_...",
"plan": "standard",
"status": "active",
"current_period_start": "2026-03-12T00:00:00Z",
"current_period_end": "2026-04-12T00:00:00Z",
"price_cents": 1000,
"currency": "usd",
"cancel_at_period_end": false,
"created_at": "2026-03-12T14:30:00Z"
},
"invoices": [
{
"id": "inv_...",
"amount_cents": 1000,
"currency": "usd",
"status": "paid",
"period_start": "2026-03-12T00:00:00Z",
"period_end": "2026-04-12T00:00:00Z",
"pdf_url": "https://..."
}
]
}
}

When no subscription exists:

{
"data": {
"has_subscription": false,
"subscription": null,
"invoices": []
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token

Rate Limit: Standard.


Stripe webhook handler. Receives Stripe events for subscription lifecycle management.

Auth: None (verified via Stripe webhook signature in Stripe-Signature header)

Request Body: Raw Stripe event payload (not JSON-parsed by application; signature verification requires raw body).

Handled Events:

Stripe EventAction
customer.subscription.createdRecord new subscription
customer.subscription.updatedUpdate subscription status/plan
customer.subscription.deletedMark subscription as cancelled
invoice.payment_succeededRecord successful payment
invoice.payment_failedNotify user, grace period begins
customer.subscription.trial_will_endSend trial ending notification

Response: 200 OK

{
"received": true
}

Errors:

HTTP StatusCodeDescription
400INVALID_SIGNATUREStripe signature verification failed

Rate Limit: None (Stripe controls the rate).

Notes: The webhook endpoint must be idempotent. Stripe may deliver the same event multiple times. Events are deduplicated by Stripe event ID.


Cancel the current subscription. The subscription remains active until the end of the current billing period (cancel_at_period_end = true).

Auth: User JWT

Request Body:

{
"reason": "No longer needed.",
"feedback": "too_expensive"
}
FieldTypeRequiredValidation
reasonstringNoMax 500 chars
feedbackenumNotoo_expensive, not_useful, switching_service, other

Response: 200 OK

{
"data": {
"subscription_id": "sub_...",
"status": "active",
"cancel_at_period_end": true,
"current_period_end": "2026-04-12T00:00:00Z",
"message": "Your subscription will remain active until 2026-04-12. You will not be charged again."
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
404NO_ACTIVE_SUBSCRIPTIONNo subscription to cancel
409ALREADY_CANCELLINGSubscription is already set to cancel at period end

Rate Limit: 3 requests per account per day.

Notes: Cancelling a subscription does not affect active enrollments. Devices remain enrolled and blocking continues. After the subscription lapses, the account is downgraded to the free tier. If the free tier does not support the account’s current device count, a grace period applies before enforcement.



Send an accountability partner invitation. The invitee receives an email with a link to accept.

Auth: User JWT

Request Body:

{
"email": "partner@example.com",
"role": "accountability_partner",
"permissions": {
"view_reports": true,
"approve_unenrollment": true,
"modify_enrollment": false
},
"message": "Hi, I would like you to be my accountability partner for gambling blocking."
}
FieldTypeRequiredValidation
emailstringYesValid email
roleenumYesaccountability_partner, therapist, authority_rep
permissionsPartnerPermissionsNoDefaults: view_reports: true, approve_unenrollment: true, modify_enrollment: false
messagestringNoMax 500 chars. Included in the invitation email.

Response: 201 Created

{
"data": {
"id": "ptr_01H...",
"account_id": "acc_01H...",
"partner_email": "partner@example.com",
"partner_account_id": null,
"status": "pending",
"role": "accountability_partner",
"permissions": {
"view_reports": true,
"approve_unenrollment": true,
"modify_enrollment": false
},
"invited_at": "2026-03-12T14:30:00Z",
"expires_at": "2026-03-19T14:30:00Z"
}
}

Errors:

HTTP StatusCodeDescription
400VALIDATION_ERRORInvalid input
401UNAUTHORIZEDInvalid or expired access token
403EMAIL_NOT_VERIFIEDInviter must have a verified email
409PARTNER_ALREADY_INVITEDAn active or pending partner relationship already exists with this email
422CANNOT_INVITE_SELFCannot invite yourself as a partner

Rate Limit: 10 invitations per account per day.

Notes: The invitation is valid for 7 days. If the invitee does not have a BetBlocker account, they will be prompted to register when accepting. The partner_account_id is populated when the invitation is accepted.


Accept a partner invitation. The invitee calls this endpoint with the invitation token received via email.

Auth: User JWT (the invitee must be logged in)

Request Body:

{
"token": "inv_..."
}
FieldTypeRequiredValidation
tokenstringYesNon-empty invitation token

Response: 200 OK

{
"data": {
"id": "ptr_01H...",
"account_id": "acc_01H...",
"partner_account_id": "acc_01H...",
"status": "active",
"role": "accountability_partner",
"permissions": {
"view_reports": true,
"approve_unenrollment": true,
"modify_enrollment": false
},
"invited_at": "2026-03-12T14:30:00Z",
"accepted_at": "2026-03-12T16:00:00Z"
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
401INVALID_INVITATION_TOKENToken is invalid, expired, or already used
409INVITATION_ALREADY_ACCEPTEDThis invitation has already been accepted
422CANNOT_ACCEPT_OWN_INVITATIONThe invitee cannot be the same as the inviter

Rate Limit: Standard.


List all partner relationships for the authenticated user. Returns both relationships where the user is the account holder and where the user is the partner.

Auth: User JWT

Query Parameters:

ParamTypeDefaultDescription
statusenum(all)Filter by status: pending, active, revoked
roleenum(all)Filter by relationship direction: my_partners (I invited them), partner_of (they invited me)
pageinteger1Page number
per_pageinteger50Items per page (max 100)

Response: 200 OK

{
"data": [
{
"id": "ptr_01H...",
"account_id": "acc_01H...",
"partner_account_id": "acc_01H...",
"partner_display_name": "John Smith",
"status": "active",
"role": "accountability_partner",
"direction": "my_partners",
"permissions": {
"view_reports": true,
"approve_unenrollment": true,
"modify_enrollment": false
},
"invited_at": "2026-03-12T14:30:00Z",
"accepted_at": "2026-03-12T16:00:00Z"
}
],
"pagination": { "..." }
}

The direction field indicates the relationship direction:

  • my_partners: Partners I invited (they oversee me)
  • partner_of: Users who invited me (I oversee them)

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token

Rate Limit: Standard.


Remove a partner relationship. Either party can initiate removal.

Auth: User JWT (either the account holder or the partner)

Path Parameters:

ParamTypeDescription
idstring (UUID)Partner relationship ID

Response: 200 OK

{
"data": {
"id": "ptr_01H...",
"status": "revoked",
"revoked_at": "2026-03-12T18:00:00Z",
"revoked_by": "acc_01H...",
"affected_enrollments": [
{
"enrollment_id": "enr_01H...",
"action": "downgraded_to_self",
"message": "Enrollment downgraded to self tier. Unenrollment policy changed to 48-hour time delay."
}
]
}
}

Errors:

HTTP StatusCodeDescription
401UNAUTHORIZEDInvalid or expired access token
403FORBIDDENNot a party to this partner relationship
404PARTNER_NOT_FOUNDPartner relationship does not exist
409ALREADY_REVOKEDPartner relationship is already revoked

Rate Limit: 5 requests per account per day.

Notes: When a partner relationship is removed, any enrollments that reference that partner for unenrollment approval are automatically downgraded:

  • The enrollment tier changes from partner to self.
  • The unenrollment policy changes to time_delayed with a 48-hour cooldown.
  • The enrolled user is notified of the change.
  • All pending unenrollment requests requiring partner approval are automatically approved.

These error codes can be returned by any endpoint.

HTTP StatusCodeDescription
400VALIDATION_ERRORRequest body or query parameters failed validation. details contains field-level errors.
401UNAUTHORIZEDMissing or invalid authentication token
401TOKEN_EXPIREDJWT has expired; use refresh token to obtain a new one
403FORBIDDENAuthenticated but insufficient permissions
404NOT_FOUNDResource does not exist
405METHOD_NOT_ALLOWEDHTTP method not supported for this path
409CONFLICTRequest conflicts with current state of the resource
413PAYLOAD_TOO_LARGERequest body exceeds size limit (1MB default)
422UNPROCESSABLE_ENTITYRequest is syntactically valid but semantically invalid
429RATE_LIMIT_EXCEEDEDToo many requests. Retry-After header indicates when to retry.
500INTERNAL_ERRORUnexpected server error. Request ID is logged for debugging.
501NOT_IMPLEMENTEDFeature is spec’d but not yet available
503SERVICE_UNAVAILABLEServer is temporarily unavailable (maintenance, overload)

When VALIDATION_ERROR is returned, the details field contains structured field-level errors:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed.",
"details": {
"fields": {
"email": ["must be a valid email address"],
"password": [
"must be at least 12 characters",
"must contain at least one uppercase letter"
]
}
}
}
}

All responses include rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1710255300
Retry-After: 60
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait before retrying (only on 429 responses)

Rate limits are applied per authentication principal (account ID, device ID, or IP for unauthenticated endpoints).

ContextLimitWindow
Standard (authenticated user)200 requests15 minutes
Device agent120 requests1 hour
Unauthenticated30 requests per IP15 minutes
Admin500 requests15 minutes
Webhook (Stripe)Unlimitedn/a

Certain endpoints have stricter limits as documented in their individual specifications. The most restrictive limit applies.

The rate limiter uses a sliding window algorithm with burst allowance. A client may use up to 20% of their window limit in a single second without being throttled, to accommodate legitimate burst patterns (e.g., page load triggering multiple API calls).


All list endpoints support cursor-based or offset-based pagination.

Query parameters:

ParamTypeDefaultDescription
pageinteger1Page number (1-indexed)
per_pageinteger50Items per page

Maximum per_page values vary by endpoint (documented per endpoint, typically 100-200).

Response includes a pagination object:

{
"pagination": {
"total": 142,
"page": 1,
"per_page": 50,
"total_pages": 3
}
}

Cursor Pagination (events and high-volume endpoints)

Section titled “Cursor Pagination (events and high-volume endpoints)”

For endpoints dealing with high-volume time-series data (events), cursor-based pagination is also supported:

ParamTypeDescription
cursorstringOpaque cursor from previous response
limitintegerItems to return (max 200)

Response:

{
"pagination": {
"next_cursor": "eyJ...",
"has_more": true
}
}

The API is versioned via URL path prefix: /v1/....

  • Minor additions (new optional fields in responses, new endpoints) are made without version bump.
  • Breaking changes (field removal, type changes, behavior changes) require a version bump (/v2/...).
  • Previous API versions are supported for at least 12 months after a new version is released.
  • The API-Version response header indicates the exact version: API-Version: 1.0.0.

Deprecated endpoints return a Deprecation header:

Deprecation: true
Sunset: 2027-06-01
Link: <https://api.betblocker.com/v2/equivalent>; rel="successor-version"

This specification is structured to map directly to OpenAPI 3.0:

  • Shared Data Models (section 3) map to components/schemas.
  • Endpoint Groups (section 4) map to paths, grouped by tags.
  • Error Codes (section 5) map to components/responses.
  • Auth Requirements map to components/securitySchemes with three schemes:
    • bearerAuth (HTTP bearer with JWT)
    • deviceCert (mutual TLS)
    • deviceToken (API key in X-Device-Token header)
  • Rate Limit annotations use the x-ratelimit extension.

The canonical OpenAPI YAML will be generated from this specification and maintained alongside it.


A WebSocket endpoint at /v1/ws is planned for real-time push to the web dashboard:

  • Connection auth: JWT passed as query parameter or in first frame.
  • Channels: device:{device_id}:status, enrollment:{enrollment_id}:events, account:{account_id}:notifications.
  • Message format: JSON with type, channel, data, and timestamp fields.
  • Guaranteed ordering: Messages within a channel are ordered by server-assigned sequence numbers.

Full WebSocket specification will be added in a separate document when the real-time push feature is implemented.