Licensing API Reference
Base URL: https://api.dictaro.ai
Authentication
Most endpoints use Bearer token authentication with RS256-signed JWT tokens.
Authorization: Bearer <jwt_token>
JWT Claims:
sub— user emailuser_id— UUIDplan—free|prodevice_id— device identifierprovider—google|azure|github|emailiat— issued atexp— expiration (default: 7 days)
Middleware
| Middleware | Description |
|---|---|
RequireAuth | Requires valid JWT. Returns 401 if missing/invalid |
OptionalAuth | Parses JWT if present, continues without auth if missing |
LoginRateLimit | 10 requests/minute per IP |
SensitiveEndpointRateLimit | 5 requests/minute per IP |
RefreshRateLimit | 10 requests/minute per IP |
Health
GET /health
Health check endpoint.
Auth: None
Response:
{
"status": "ok",
"build": "2026-03-14T12:00:00Z"
}
OAuth Authentication
GET /auth/login-page
Renders HTML login page with OAuth provider buttons and Cloudflare Turnstile.
Auth: None
Query Parameters:
| Param | Type | Description |
|---|---|---|
redirect_uri | string | Where to redirect after auth (must match allowed prefixes) |
state | string | Opaque state passed back to client |
device_id | string | Device identifier for account limits |
GET /auth/login/:provider
Initiates OAuth flow by redirecting to the provider's authorization endpoint.
Auth: None, rate-limited
Path Parameters:
| Param | Values |
|---|---|
provider | google, azure, github |
GET /auth/callback/:provider
OAuth callback handler. Exchanges authorization code for user info, creates/updates user, issues JWT.
Auth: None
Behavior:
- Validates nonce from session
- Exchanges code for token
- Fetches user info from provider API
- Creates or updates user in database
- Syncs Stripe subscription status
- Checks device account limits (free users only)
- Issues JWT token
- Redirects to
redirect_uri?token=<jwt>&state=<state>
Error Responses:
| Status | Reason |
|---|---|
| 429 | Device account limit exceeded (free users) |
| 500 | OAuth token exchange or user info fetch failed |
POST /auth/refresh
Refreshes an existing JWT token.
Auth: None (reads token from body/header), rate-limited
Response:
{
"token": "<new_jwt_token>"
}
Email/Password Authentication
POST /auth/register
Register a new user with email and password.
Auth: None, rate-limited
Request Body:
{
"email": "user@example.com",
"password": "minimum8chars",
"name": "User Name",
"turnstile_token": "<cloudflare_turnstile_token>"
}
Response: 202 Accepted
Sends verification email with confirmation link.
POST /auth/login/email
Login with email and password.
Auth: None, rate-limited
Request Body:
{
"email": "user@example.com",
"password": "password",
"turnstile_token": "<token>",
"redirect_uri": "dictaro://auth",
"state": "random",
"device_id": "device123"
}
Error Responses:
| Status | Reason |
|---|---|
| 401 | Invalid credentials |
| 403 | Email not verified |
| 429 | Device account limit exceeded |
GET /auth/verify-email
Renders email verification confirmation page (does NOT consume the token — prevents email scanner bots).
Query: ?token=<raw_token>
POST /auth/confirm-email
Actually consumes the verification token and marks email as verified.
Request Body:
{
"token": "<raw_token>"
}
POST /auth/resend-verification
Resends verification email.
Request Body:
{
"email": "user@example.com",
"turnstile_token": "<token>"
}
POST /auth/forgot-password
Initiates password reset. Always returns 202 (prevents email enumeration).
Request Body:
{
"email": "user@example.com",
"turnstile_token": "<token>"
}
GET /auth/reset-password-page
Renders password reset form.
Query: ?token=<raw_token>
POST /auth/reset-password
Resets password using token.
Request Body:
{
"token": "<raw_token>",
"password": "newpassword8+"
}
License
POST /license/activate
Activates a license key for the authenticated user.
Auth: Required
Request Body:
{
"key": "ABCD1234EFGH5678IJKL9012MNOP3456",
"device_id": "device123"
}
Response:
{
"key_last4": "3456"
}
Error Responses:
| Status | Reason |
|---|---|
| 404 | Key not found |
| 409 | Key already activated by another user |
GET /account/status
Returns current account information.
Auth: Required
Response:
{
"email": "user@example.com",
"name": "User Name",
"plan": "pro",
"created_at": "2026-01-15T10:00:00Z",
"license_key_last4": "3456",
"subscription_expiry": "2026-04-15T10:00:00Z"
}
Usage & Quota
POST /usage/record
Records dictation usage.
Auth: Optional (user_id can be null for anonymous usage)
Request Body:
{
"duration_seconds": 45.5
}
Response: QuotaStatusResponse (see below)
GET /usage/quota-status
Returns current quota status.
Auth: Optional
Response:
{
"status": "active",
"plan": "free",
"quota_seconds": 600,
"used_seconds": 120.5,
"remaining_seconds": 479.5,
"cooldown_until": null,
"trial_ends_at": "2026-03-21T10:00:00Z"
}
Status values:
| Status | Description |
|---|---|
unlimited | Pro user, no limits |
trial | Within 7-day trial period |
active | Free user with remaining quota |
cooldown | Quota exhausted, in cooldown period |
GET /stats/public
Returns aggregated public statistics (for website display).
Auth: None
Response:
{
"total_users": 150,
"total_dictation_hours": 42.5,
"total_typing_saved_hours": 170.0,
"total_dictation_count": 3200
}
Pricing
GET /pricing/plans
Returns available subscription plans from Stripe.
Auth: None
Response:
{
"plans": [
{
"id": "price_xxx",
"interval": "month",
"amount": 999,
"currency": "eur"
},
{
"id": "price_yyy",
"interval": "year",
"amount": 9990,
"currency": "eur"
}
]
}
Billing
GET /billing/choose-plan
Renders plan selection page.
Auth: None
POST /billing/create-checkout
Creates a Stripe Checkout session.
Auth: Required
Request Body:
{
"price_id": "price_xxx",
"redirect_uri": "dictaro://billing",
"state": "random"
}
Response:
{
"checkout_url": "https://checkout.stripe.com/c/pay/..."
}
Error Responses:
| Status | Reason |
|---|---|
| 400 | User already on Pro plan |
POST /billing/webhook
Stripe webhook endpoint. Validates signature using STRIPE_WEBHOOK_SECRET.
Auth: Stripe signature verification
Handled events:
| Event | Action |
|---|---|
checkout.session.completed | Upgrade user to Pro, store subscription ID |
customer.subscription.updated | Sync plan status (active→pro, past_due→free) |
customer.subscription.deleted | Downgrade to free |
invoice.payment_failed | Log failure for monitoring |
GET /billing/checkout-success
Success redirect page after Stripe checkout.
Query: ?session_id=<id>&redirect_uri=<uri>&state=<state>
GET /billing/checkout-cancel
Cancel redirect page from Stripe checkout.
POST /account/portal-url
Creates a Stripe Customer Portal session URL.
Auth: Required
Response:
{
"url": "https://billing.stripe.com/p/session/..."
}
Metrics
GET /metrics (port 9090)
Prometheus metrics endpoint (internal, not publicly exposed).
Key metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
licensing_requests_total | counter | method, path, status | HTTP request count |
licensing_request_duration_seconds | histogram | method, path | Request latency |
licensing_auth_logins_total | counter | provider, status | Login attempts |
licensing_auth_refreshes_total | counter | status | Token refreshes |
licensing_billing_checkouts_total | counter | status | Checkout sessions created |
licensing_billing_webhooks_total | counter | event, status | Webhook events processed |
licensing_usage_records_total | counter | plan | Usage recordings |