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
| Permission | Description |
|---|---|
entry.create | Create entries, upload documents, create reimbursements |
entry.edit | Edit entries, update status |
entry.void | Void entries, create refunds |
entry.review | Approve/reject documents, request documents |
entry.reconcile | Mark entries as reconciled |
entry.note | Add notes to entries |
entry.export | Export entries to CSV |
accounting.manage | Manage accounting connections and sync operations |
accounting.review | Review entries in accounting queue |
accounting.categories | Manage chart of accounts |
accounting.periods | Manage accounting periods |
accounting.mappings | Manage party and tax mappings |
party.create | Create parties |
party.edit | Edit parties |
integration.manage | Manage API key integrations |
workspace.read | View workspaces |
workspace.create | Create workspaces |
workspace.edit | Edit workspaces |
user.read | View users |
user.edit | Edit users |
user.delete | Delete 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:
- have the required permission
- 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.