API reference

Base URL: https://onetrial.dev. Auth: Authorization: Bearer ot_test_... (server-side only). Full schema: openapi.json.
methodpathsummaryauth
POST/api/v1/visitor-tokenMint a visitor token (called by the browser snippet)none
POST/api/v1/decisionsScore a signup and return a decisionBearer key
GET/api/v1/decisionsList decisionsBearer key
GET/api/v1/decisions/{id}Get one decisionBearer key
POST/api/v1/decisions/{id}/feedbackLabel a decision as false or true positiveBearer key
POST/api/v1/signals/cardPush a card fingerprint collected on your own Stripe accountBearer key
GET/api/c/{challengeId}Visitor-safe challenge statusnone
POST/api/v1/challenges/{id}/completeHeadless: report the outcome of your own verification UIBearer key
GET/api/v1/allowlistList allowlist entriesBearer key
POST/api/v1/allowlistAdd a allowlist entryBearer key
DELETE/api/v1/allowlistRemove a allowlist entryBearer key
GET/api/v1/blocklistList blocklist entriesBearer key
POST/api/v1/blocklistAdd a blocklist entryBearer key
DELETE/api/v1/blocklistRemove a blocklist entryBearer key
GET/api/v1/webhooksList endpoints (add ?deliveries=<id> for its delivery log)Bearer key
POST/api/v1/webhooksRegister an endpoint; the signing secret is returned onceBearer key
DELETE/api/v1/webhooksRemove an endpointBearer key
GET/api/v1/integration/statusMachine-readable integration checklistBearer key
GET/api/v1/settingsWorkspace settingsBearer key
PATCH/api/v1/settingsUpdate thresholds, shadow mode, stack, plan priceBearer key
POST/api/v1/keysCreate another API keyBearer key
POST/api/device/codeStart device authorization (CLI/MCP)none
POST/api/device/tokenPoll for the approved API keynone

POST /api/v1/decisions

Request:
json
{ "visitorToken": "eyJ...", "email": "jane@example.com", "ip": "203.0.113.7", "cardFingerprint": "optional", "userId": "optional", "metadata": {} }
Response:
json
{ "decisionId": "uuid", "decision": "challenge", "score": 58, "reasons": [{ "code": "D_DEVICE_REPEAT", "message": "Device previously used for a consumed trial", "weight": 20.4 }], "challengeOptions": ["require_card"], "challengeId": "uuid", "challengeUrl": "https://onetrial.dev/c/uuid", "usage": { "used": 12, "limit": 500, "warn": false } }
In shadow mode the same call returns "decision": "allow", "shadow": true, "wouldBe": "challenge".

Coupon and promo redemptions

The same endpoint answers whether a promo code redemption is a repeat. Send event: "promo" with the code:
json
{ "event": "promo", "promoCode": "WELCOME50", "email": "jane@example.com", "ip": "203.0.113.7", "visitorToken": "eyJ...", "userId": "usr_8213" }
event defaults to signup, so existing calls are unchanged. A promo redemption is recorded as its own kind of event and never counts as a consumed trial: without that, a paying customer who redeems a coupon would gain a second trial on their own card and device, and later decisions about them would read it as a repeat.
The code is never treated as an identity, because a public campaign code is typed by thousands of unrelated people. It counts as evidence only when an account already linked to this person redeemed the same code.

Errors

Every 4xx/5xx body is { "error": { "code", "message", "fix" } }. fix is the exact corrective step; agents should act on it.
statuscodefix
401missing_api_key / invalid_api_keySend Authorization: Bearer ot_test_... from the dashboard
400validation_errorThe message names the field; correct it
400invalid_email / invalid_ipSend the raw email and the client IP (from x-forwarded-for)
429plan_limit_reachedFree plan cap hit this month; upgrade under Billing
409challenge_resolvedThe challenge already passed or failed; request a new decision
409stripe_not_connectedConnect Stripe or push fingerprints to POST /api/v1/signals/card