Преминете към основното съдържание

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 email
  • user_id — UUID
  • plan — free | pro
  • device_id — device identifier
  • provider — google | azure | github | email
  • iat — issued at
  • exp — expiration (default: 7 days)

Middleware​

MiddlewareDescription
RequireAuthRequires valid JWT. Returns 401 if missing/invalid
OptionalAuthParses JWT if present, continues without auth if missing
LoginRateLimit10 requests/minute per IP
SensitiveEndpointRateLimit5 requests/minute per IP
RefreshRateLimit10 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:

ParamTypeDescription
redirect_uristringWhere to redirect after auth (must match allowed prefixes)
statestringOpaque state passed back to client
device_idstringDevice 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:

ParamValues
providergoogle, azure, github

GET /auth/callback/:provider​

OAuth callback handler. Exchanges authorization code for user info, creates/updates user, issues JWT.

Auth: None

Behavior:

  1. Validates nonce from session
  2. Exchanges code for token
  3. Fetches user info from provider API
  4. Creates or updates user in database
  5. Syncs Stripe subscription status
  6. Checks device account limits (free users only)
  7. Issues JWT token
  8. Redirects to redirect_uri?token=<jwt>&state=<state>

Error Responses:

StatusReason
429Device account limit exceeded (free users)
500OAuth 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:

StatusReason
401Invalid credentials
403Email not verified
429Device 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:

StatusReason
404Key not found
409Key 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:

StatusDescription
unlimitedPro user, no limits
trialWithin 7-day trial period
activeFree user with remaining quota
cooldownQuota 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:

StatusReason
400User already on Pro plan

POST /billing/webhook​

Stripe webhook endpoint. Validates signature using STRIPE_WEBHOOK_SECRET.

Auth: Stripe signature verification

Handled events:

EventAction
checkout.session.completedUpgrade user to Pro, store subscription ID
customer.subscription.updatedSync plan status (active→pro, past_due→free)
customer.subscription.deletedDowngrade to free
invoice.payment_failedLog 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:

MetricTypeLabelsDescription
licensing_requests_totalcountermethod, path, statusHTTP request count
licensing_request_duration_secondshistogrammethod, pathRequest latency
licensing_auth_logins_totalcounterprovider, statusLogin attempts
licensing_auth_refreshes_totalcounterstatusToken refreshes
licensing_billing_checkouts_totalcounterstatusCheckout sessions created
licensing_billing_webhooks_totalcounterevent, statusWebhook events processed
licensing_usage_records_totalcounterplanUsage recordings