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

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
  • planfree | pro
  • device_id — device identifier
  • providergoogle | 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