Authentication

All Tether API endpoints (except QR codes and the public branding endpoint) require a Bearer token. This page explains how to obtain and use tokens.

Interactive API docs available

When Tether is running, navigate to https://yourdomain.com/docs for Swagger UI — you can try all API calls directly in the browser with try-it-out functionality.

Obtaining a token

Post credentials to the login endpoint using application/x-www-form-urlencoded encoding (not JSON):

http
POST /api/auth/login Content-Type: application/x-www-form-urlencoded username=admin%40atechsolutions.org&password=yourpassword

Successful response:

json
{{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer" }}

Failed response (wrong credentials):

json
{{ "detail": "Incorrect email or password" }}

Using the token

Include the token in the Authorization header of every request:

http
GET /api/assets Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Token expiry

Tokens are valid for 7 days from the time of issue. After expiry, the API returns 401 Unauthorized. The client must log in again to get a new token.

If SECRET_KEY is rotated in .env and Tether restarted, all existing tokens are immediately invalidated and all users must log in again.

Tenant scoping

The API determines the active tenant from the HTTP Host header of the request:

Host headerTenant scope
atechsolutions.orgMSP root — MSP staff see all tenants; client users get an error
acme.atechsolutions.orgScoped to the Acme tenant — all queries restricted to Acme data
assets.acme.com (custom domain)Scoped to whichever tenant has that custom domain registered

MSP staff can also scope to a specific tenant from the root domain by adding a query parameter:

http
GET /api/assets?tenant=acme Host: atechsolutions.org Authorization: Bearer {msp-token} # Returns assets scoped to the "acme" tenant

Get current user

http
GET /api/auth/me Authorization: Bearer {token} # Response: {{ "id": 1, "name": "Sarah Chen", "email": "[email protected]", "role": "client_admin", "is_msp_staff": false, "tenant_id": 5, "tenant_slug": "acme", "permissions": [ "assets.view", "assets.create", "assets.edit", "assets.delete", "assets.checkout", "assets.checkin", "assets.import", "assets.export", "categories.manage", "locations.manage", "employees.manage", "users.manage", "reports.view", "settings.manage" ] }}

Change password

http
POST /api/auth/change-password Authorization: Bearer {token} Content-Type: application/json {{ "current_password": "oldpassword", "new_password": "newstrongpassword" }} # Success: 200 OK # Wrong current password: 400 Bad Request

Public endpoints (no auth required)

EndpointReturns
GET /api/public/brandingTenant name, logo URL, accent colour — used by the login page
GET /api/assets/{id}/qrQR code PNG for the asset tag
GET /api/invites/validate/{token}Invite token details for the account setup page
GET /api/demo/statsAsset count for the demo page counter (demo mode only)

Error response format

All API errors use a consistent format:

json
{{ "detail": "Human-readable error message" }}
HTTP statusMeaning
200 OKSuccess
400 Bad RequestValidation error — check the request body
401 UnauthorizedMissing or expired token
403 ForbiddenValid token but insufficient permissions
404 Not FoundResource does not exist (or belongs to a different tenant)
402 Payment RequiredAsset limit reached (SaaS mode only)
422 Unprocessable EntityRequest body failed Pydantic validation
500 Internal Server ErrorUnexpected error — check server logs
Last updated: May 2026