Moonlit API Reference
Base URL (production):
https://api.moonlit.io
Base URL (local dev):http://localhost:5500
API Version: v1
Content-Type:application/json
Authentication
All endpoints except those marked Public require a Bearer JWT token issued by your GoTrue auth instance.
Authorization: Bearer <your-jwt-token>Obtain a token by authenticating through the GoTrue endpoint:
POST /auth/v1/token?grant_type=password
Content-Type: application/json
{
"email": "you@company.com",
"password": "your-password"
}Health
GET /health
Check system status. No authentication required.
Response 200 OK
HealthyCompanies
POST /api/v1/companies — Public
Register a new company. Called once during initial account setup.
Request body
{
"companyName": "Acme Corp",
"industry": "Technology",
"country": "US",
"registrationNumber": "12345678"
}Response 201 Created
{
"id": "c9faf6b6-cc99-4351-988a-5695015769c2",
"companyName": "Acme Corp",
"industry": "Technology",
"country": "US",
"registrationNumber": "12345678",
"createdAt": "2025-01-15T10:00:00Z"
}GET /api/v1/companies/me — Auth Required
Get the authenticated company's profile.
Response 200 OK
{
"id": "c9faf6b6-cc99-4351-988a-5695015769c2",
"companyName": "Acme Corp",
"industry": "Technology",
"country": "US",
"contactEmail": "hr@acme.com",
"memberCount": 3,
"createdAt": "2025-01-01T00:00:00Z"
}PATCH /api/v1/companies/me — Auth Required
Update company profile.
Request body
{
"companyId": "c9faf6b6-cc99-4351-988a-5695015769c2",
"companyName": "Acme Corporation",
"contactEmail": "compliance@acme.com"
}Response 200 OK — Updated company object
POST /api/v1/companies/me/members — Auth Required
Invite a team member to your company account.
Request body
{
"email": "newmember@acme.com",
"fullName": "Jane Smith",
"role": "member"
}Roles: member, admin
Response 201 Created — Invitation sent; member receives email to complete sign-up
Employees
GET /api/v1/employees — Auth Required
List all registered employees for the authenticated company.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
pageSize | integer | 20 | Results per page (max 100) |
search | string | — | Search by name, MoonlitID, or country |
Response 200 OK
{
"data": [
{
"id": "emp-uuid",
"moonlitId": "MLT-abc123",
"fullName": "John Smith",
"jobTitle": "Senior Engineer",
"department": "Engineering",
"country": "US",
"employmentStartDate": "2024-01-15",
"isActive": true,
"createdAt": "2025-01-01T00:00:00Z"
}
],
"total": 150,
"page": 1,
"pageSize": 20
}POST /api/v1/employees — Auth Required
Register a new employee. All PII fields are hashed before storage.
Request body
{
"companyId": "c9faf6b6-cc99-4351-988a-5695015769c2",
"nationalId": "US-001-JOHNDOE",
"primaryEmail": "john@acme.com",
"fullName": "John Smith",
"employmentStartDate": "2025-01-01",
"jobTitle": "Senior Engineer",
"department": "Engineering"
}Response 201 Created — Employee object with assigned MoonlitID
POST /api/v1/employees/import — Auth Required
Bulk import employees from CSV.
Request — multipart/form-data
| Field | Type | Description |
|---|---|---|
file | file | CSV file with headers: nationalId, primaryEmail, fullName, employmentStartDate, jobTitle, department |
Response 202 Accepted
{
"jobId": "import-job-uuid",
"status": "processing",
"totalRows": 250
}POST /api/v1/employees/merge — Auth Required
Merge two duplicate employee records into a single canonical record.
Request body
{
"primaryEmployeeId": "emp-uuid-1",
"duplicateEmployeeId": "emp-uuid-2"
}Response 200 OK — Merged employee object
DELETE /api/v1/employees/{id} — Auth Required
Deactivate (soft-delete) an employee. The hashed record is retained.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Employee ID |
Response 204 No Content
Scans
POST /api/v1/scans/pre-hire — Auth Required
Run a pre-hire conflict scan against a candidate.
Request body
{
"requestingCompanyId": "c9faf6b6-cc99-4351-988a-5695015769c2",
"candidateNationalId": "US-002-JANEDOE",
"candidateEmail": "jane.doe@example.com",
"candidateFullName": "Jane Doe",
"idDocumentType": "NationalId",
"issuingCountry": "US"
}idDocumentType values: NationalId, Passport, DriversLicense, ResidencePermit
Response 200 OK
{
"scanId": "scan-uuid",
"conflictFound": true,
"trustScore": 35,
"trustTier": "Low",
"summary": "Active employment conflict detected",
"signals": [
{
"description": "Active employment detected at another Moonlit member company",
"severity": "Critical",
"isActiveConflict": true
}
],
"completedAt": "2025-01-15T10:30:01Z"
}Trust tiers
| Tier | Score Range | Recommendation |
|---|---|---|
| High | 85–100 | Proceed with hire |
| Medium | 50–84 | Review manually |
| Low | 0–49 | Escalate; do not proceed without investigation |
POST /api/v1/scans/consent — Auth Required
Record explicit background scan consent from a candidate.
Request body
{
"companyId": "company-uuid",
"candidateEmail": "jane.doe@example.com",
"consentTimestamp": "2025-01-15T09:00:00Z",
"consentIpAddress": "203.0.113.10"
}Response 201 Created — Consent record
GET /api/v1/scans — Auth Required
Get scan history for the authenticated company.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
pageSize | integer | 20 | Results per page |
from | ISO date | — | Filter from date |
to | ISO date | — | Filter to date |
Response 200 OK — Paginated list of scan summaries
PATCH /api/v1/scans/{id}/resolve — Auth Required
Mark a scan conflict as a resolved false positive.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Scan ID |
Request body
{
"resolutionNote": "Investigated — candidate's previous employment ended before our start date; registration overlap was a data entry delay."
}Response 200 OK — Updated scan object
Alerts
GET /api/v1/alerts — Auth Required
List conflict alerts for the authenticated company.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
pageSize | integer | 20 | Results per page |
Response 200 OK
{
"data": [
{
"id": "alert-uuid",
"moonlitId": "MLT-abc123",
"title": "Dual Employment Conflict",
"description": "Active employee detected at another Moonlit member company",
"severity": "Critical",
"isRead": false,
"createdAt": "2025-01-15T10:30:00Z"
}
],
"total": 3,
"page": 1,
"pageSize": 20
}PATCH /api/v1/alerts/{id}/read — Auth Required
Mark an alert as read.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | Alert ID |
Response 200 OK — Updated alert object
Dashboard
GET /api/v1/dashboard/stats — Auth Required
Retrieve key performance indicators for the dashboard.
Response 200 OK
{
"totalEmployees": 150,
"activeConflicts": 3,
"scansThisMonth": 42,
"avgTrustScore": 87.4,
"recentActivity": [
{
"id": "alert-uuid",
"title": "Dual Employment Conflict",
"description": "Jane Doe flagged",
"moonlitId": "MLT-abc123",
"severity": "Critical",
"createdAt": "2025-01-15T10:30:00Z"
}
]
}Webhooks
GET /api/v1/webhooks — Auth Required
List all webhook subscriptions for the company.
Response 200 OK — Array of webhook subscription objects
POST /api/v1/webhooks — Auth Required
Create a new webhook subscription.
Request body
{
"url": "https://yourapp.com/webhooks/moonlit",
"eventType": "ConflictDetected",
"secret": "your-signing-secret"
}Event types: ConflictDetected, AlertGenerated, ScanCompleted, EmployeeRegistered, AllEvents
Response 201 Created — Webhook subscription object including the assigned id
DELETE /api/v1/webhooks/{id} — Auth Required
Delete a webhook subscription.
Response 204 No Content
GET /api/v1/webhooks/event-types — Public
List all supported webhook event types.
Response 200 OK
["ConflictDetected", "AlertGenerated", "ScanCompleted", "EmployeeRegistered", "AllEvents"]Billing
GET /api/v1/billing/plans — Public
List available subscription plans.
Response 200 OK
[
{
"id": "plan-starter",
"name": "Starter",
"monthlyPrice": 49,
"annualPrice": 470,
"maxEmployees": 50,
"scansPerMonth": 20,
"webhooksEnabled": false,
"apiAccess": false
},
{
"id": "plan-growth",
"name": "Growth",
"monthlyPrice": 149,
"annualPrice": 1430,
"maxEmployees": 500,
"scansPerMonth": 200,
"webhooksEnabled": true,
"apiAccess": true
}
]GET /api/v1/billing/current — Auth Required
Get the current subscription plan and usage.
Response 200 OK — Active subscription with usage counters
POST /api/v1/billing/checkout — Auth Required
Create a Stripe checkout session to start or upgrade a subscription.
Request body
{
"planId": "plan-growth",
"interval": "monthly"
}Response 200 OK
{
"checkoutUrl": "https://checkout.stripe.com/pay/cs_..."
}POST /api/v1/billing/portal — Auth Required
Create a Stripe customer portal session for self-service billing management.
Response 200 OK
{
"portalUrl": "https://billing.stripe.com/session/..."
}GET /api/v1/billing/invoices — Auth Required
Get the company's invoice history.
Response 200 OK — Array of invoice objects with amount, date, and download URL
POST /api/v1/billing/initiate — Auth Required
Start a payment flow for non-Stripe providers.
Request body
{
"provider": "Paymob",
"planId": "plan-growth",
"interval": "monthly"
}Providers: Stripe, Paymob, Fawry, Manual
GET /api/v1/billing/status/{id} — Auth Required
Get the status of a payment transaction.
POST /api/v1/billing/subscription/cancel — Auth Required
Cancel the active subscription at the end of the billing period.
Errors
All error responses follow this structure:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"title": "Not Found",
"status": 404,
"detail": "Employee with ID 'emp-xyz' was not found",
"traceId": "00-abc123-def456-00"
}| Status | Meaning |
|---|---|
| 400 | Bad request — validation error in request body |
| 401 | Unauthorized — missing or invalid JWT |
| 403 | Forbidden — authenticated but insufficient permissions |
| 404 | Not found — resource does not exist |
| 422 | Unprocessable entity — business logic violation |
| 429 | Too many requests — rate limit exceeded |
| 500 | Internal server error — contact support |
Rate Limits
| Tier | Limit |
|---|---|
| Default | 100 requests / minute / user |
| Bulk import | 5 requests / minute / company |
When rate limited, the response includes:
HTTP/1.1 429 Too Many Requests
Retry-After: 30