Skip to main content

Authentication

JWT Authentication

Most API endpoints require a JWT Bearer token.

Login

POST /auth/login
Content-Type: application/json

{
"email": "[email protected]",
"password": "password123"
}

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"email": "[email protected]",
"name": "Admin User",
"permissions": ["entry.create", "entry.edit", ...]
}
}

Using the Token

Include the token in the Authorization header:

GET /entries?workspaceId=1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Token Refresh

Tokens expire after 15 minutes. The frontend automatically refreshes them 60 seconds before expiration. The refresh endpoint accepts tokens that have expired within a 7-minute grace period, which handles scenarios like browser tab suspension or brief network outages.

POST /auth/refresh
Authorization: Bearer <current-token>

The refresh endpoint uses a dedicated JwtRefreshAuthGuard that allows slightly-expired tokens (up to 7 minutes past expiration), unlike other endpoints which reject expired tokens immediately.

If the refresh fails due to a server error or network issue, the frontend retries after 30 seconds. If the token has expired beyond the grace period (401 response), the user is logged out.

When a browser tab returns to focus, the frontend checks token validity and triggers a refresh if needed. Any non-auth API call that receives a 401 response also triggers an automatic logout.

API Key Authentication

The Ingest API uses API key authentication:

POST /api/v1/ingest
X-API-Key: mk_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

API keys are managed through the Integrations API (/integrations).

Permissions

PermissionDescription
entry.createCreate entries, upload documents, create reimbursements
entry.editEdit entries, update status
entry.voidVoid entries, create refunds
entry.reviewApprove/reject documents, request documents
entry.reconcileMark entries as reconciled
entry.noteAdd notes to entries
entry.exportExport entries to CSV
accounting.manageManage accounting connections and sync operations
accounting.reviewReview entries in accounting queue
accounting.categoriesManage chart of accounts
accounting.periodsManage accounting periods
accounting.mappingsManage party and tax mappings
party.createCreate parties
party.editEdit parties
integration.manageManage API key integrations
workspace.readView workspaces
workspace.createCreate workspaces
workspace.editEdit workspaces
user.readView users
user.editEdit users
user.deleteDelete users

Access Scope Enforcement

Permissions alone are not sufficient to read or mutate finance data.

Moonlight also enforces object-level and workspace-level access for sensitive resources:

  • entries and their related activity (events, notes, related entries)
  • accounting connections, reviews, sync records, categories, mappings, and periods
  • tags, parties, integrations, and workspace-specific data
  • reporting and dashboard endpoints scoped by workspaceId

This means that a user must both:

  1. have the required permission
  2. belong to the workspace or project that owns the requested object

Passing another workspaceId, entryId, connectionId, or similar identifier is not enough without matching membership.