Authentication
Authenticate with the Mail.Land API using Bearer tokens or API keys
MailLand supports two authentication methods: Bearer tokens (JWT) for user sessions and API keys for server-to-server integration. Dashboard sessions send Authorization: Bearer <token>; server-to-server calls send X-API-Key: <key>.
Authentication Methods
| Method | Header | Use Case |
|---|---|---|
| Bearer Token | Authorization: Bearer <token> | User sessions, dashboard |
| API Key | X-API-Key: <key> | Server-to-server, automation |
Register
POST https://api.mail.land/v1/auth/register{
"email": "[email protected]",
"password": "SecureP@ssw0rd!",
"fullName": "John Doe",
"companyName": "My Company"
}curl -X POST https://api.mail.land/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "SecureP@ssw0rd!",
"fullName": "John Doe",
"companyName": "My Company"
}'| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address |
password | string | Yes | Password (8–128 characters; needs an uppercase letter, a lowercase letter, a digit and a special character) |
fullName | string | No | Full name |
companyName | string | No | Company / workspace name |
Unknown fields (such as a tenantName sent by mistake) are dropped silently.
{
"requiresEmailVerification": true,
"verificationTicket": "vt_abc123...",
"maskedEmail": "u***@example.com"
}The response status is 201.
After registration, verify the email address before logging in.
Email Verification
Registration emails a 6-digit code. Call POST /v1/auth/verify-email-ticket with { "ticket": "<verificationTicket>", "code": "123456" }; on success it returns the login token response.
POST https://api.mail.land/v1/auth/verify-email-ticket{
"ticket": "vt_abc123...",
"code": "123456"
}curl -X POST https://api.mail.land/v1/auth/verify-email-ticket \
-H "Content-Type: application/json" \
-d '{"ticket": "vt_abc123...", "code": "123456"}'Resend the verification email: POST /v1/auth/resend-verification-ticket with { "ticket": "..." }. It is rate-limited and returns a fresh ticket.
POST https://api.mail.land/v1/auth/resend-verification-ticket{
"ticket": "vt_abc123..."
}curl -X POST https://api.mail.land/v1/auth/resend-verification-ticket \
-H "Content-Type: application/json" \
-d '{"ticket": "vt_abc123..."}'Login
POST https://api.mail.land/v1/auth/login{
"email": "[email protected]",
"password": "SecureP@ssw0rd!"
}curl -X POST https://api.mail.land/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "SecureP@ssw0rd!"
}'Response
The login response varies based on the account state:
Successful login:
{
"userId": "v4j2x7n1m9k5p3",
"email": "[email protected]",
"fullName": "John Doe",
"avatarUrl": null,
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_a1b2c3d4e5...",
"expiresAt": "2026-09-25T13:00:00Z",
"requiresTwoFactor": false,
"twoFactorChallengeId": null,
"tenantCount": 1,
"activeTenantId": "k3m9x2p7qw4rtz",
"activeTenantName": "My Company",
"requiresEmailVerification": false,
"verificationTicket": null,
"maskedEmail": null
}There is no user object and no role in the response. An unverified account gets 200 with requiresEmailVerification: true and a verificationTicket, not an error.
Two-factor authentication required:
{
"accessToken": null,
"refreshToken": null,
"requiresTwoFactor": true,
"twoFactorChallengeId": "2fa_challenge_abc123"
}When requiresTwoFactor is true, complete the 2FA verification before receiving tokens.
Refresh Token
Access tokens expire after 60 minutes. Use the refresh token to obtain a new access token:
POST https://api.mail.land/v1/auth/refresh-token{
"refreshToken": "rt_a1b2c3d4e5..."
}curl -X POST https://api.mail.land/v1/auth/refresh-token \
-H "Content-Type: application/json" \
-d '{"refreshToken": "rt_a1b2c3d4e5..."}'The response is the same shape as the login response above (the full AuthResponse), not just these three fields:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_new_token...",
"expiresAt": "2026-09-25T14:00:00Z"
}Each refresh token can only be used once. The response includes a new refresh token for the next rotation.
Logout
Invalidate the current session:
POST https://api.mail.land/v1/auth/logoutSend { "refreshToken": "..." } in the request body (or the refresh-token cookie). The refresh token is revoked; the access token stays valid until it expires: a Bearer header alone revokes nothing.
curl -X POST https://api.mail.land/v1/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Revoke all active sessions:
POST https://api.mail.land/v1/auth/revoke-all-tokenscurl -X POST https://api.mail.land/v1/auth/revoke-all-tokens \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Password Management
Forgot Password
POST https://api.mail.land/v1/auth/forgot-password{
"email": "[email protected]"
}curl -X POST https://api.mail.land/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'Sends a password reset email with a time-limited token.
Reset Password
POST https://api.mail.land/v1/auth/reset-passwordconfirmPassword is required and must match newPassword; the same complexity rules apply as at registration.
{
"token": "reset-token-from-email",
"newPassword": "NewSecureP@ssw0rd!",
"confirmPassword": "NewSecureP@ssw0rd!"
}curl -X POST https://api.mail.land/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "reset-token-from-email",
"newPassword": "NewSecureP@ssw0rd!",
"confirmPassword": "NewSecureP@ssw0rd!"
}'Validate Reset Token
Check if a reset token is still valid before showing the reset form:
GET https://api.mail.land/v1/auth/validate-reset-token?token=reset-token-from-emailcurl "https://api.mail.land/v1/auth/validate-reset-token?token=reset-token-from-email"{
"valid": true,
"message": "Token is valid"
}Change Password
Change the password for the currently authenticated user:
POST https://api.mail.land/v1/auth/change-password{
"currentPassword": "OldP@ssw0rd!",
"newPassword": "NewSecureP@ssw0rd!"
}curl -X POST https://api.mail.land/v1/auth/change-password \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"currentPassword": "OldP@ssw0rd!",
"newPassword": "NewSecureP@ssw0rd!"
}'Profile Management
Get Current User
GET https://api.mail.land/v1/auth/mecurl https://api.mail.land/v1/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"id": "v4j2x7n1m9k5p3",
"email": "[email protected]",
"fullName": "John Doe",
"companyName": "Acme",
"avatarUrl": "https://cdn.mail.land/avatars/usr_a1b2c3d4e5.jpg",
"status": "Active",
"createdAt": "2026-01-15T08:00:00Z",
"subscription": {
"planName": "Pro",
"monthlyEmailLimit": 50000,
"rateLimitPerSecond": 10,
"startDate": "2026-01-15T08:00:00Z",
"endDate": "2026-10-15T08:00:00Z",
"status": "Active"
},
"stats": {
"credentialCount": 3,
"domainCount": 2
}
}There is no firstName, lastName, phone, tenantId, role or twoFactorEnabled field in this response.
Update Profile
PUT https://api.mail.land/v1/auth/profilefullName is required; omitting companyName clears it. The update matches the user's row by user id alone, so it works from any workspace the user is active in, not only the one the account was created in.
{
"fullName": "Jane Smith",
"companyName": "Acme"
}curl -X PUT https://api.mail.land/v1/auth/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"fullName": "Jane Smith",
"companyName": "Acme"
}'Upload Avatar
POST https://api.mail.land/v1/account/avatarSend a multipart/form-data request with the avatar image file. Supported formats: JPEG, PNG, GIF or WebP (the content is sniffed, not just the extension). Maximum size: 1 MB.
curl -X POST https://api.mail.land/v1/account/avatar \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-F "file=@/path/to/avatar.jpg"Social Authentication
MailLand supports social login via Google, GitHub, and Microsoft:
Redirect to provider
GET https://api.mail.land/auth/social/{provider}Where {provider} is google, github, or microsoft. This redirects the user to the provider's OAuth consent screen.
curl https://api.mail.land/auth/social/googleHandle callback
After the user authorizes, the provider redirects back to MailLand with an authorization code:
GET https://api.mail.land/auth/social/{provider}/callback?code=...curl "https://api.mail.land/auth/social/google/callback?code=authorization-code-from-provider"The callback is handled by MailLand, which redirects the browser to {frontend}/{locale}/auth/callback?code=<one-time code>&provider=.... That code is MailLand's own one-time code, not the provider's authorization code: pass it, unmodified, to the exchange step below.
Exchange for tokens
POST https://api.mail.land/v1/auth/social/exchange{
"code": "one-time-code-from-callback-redirect"
}curl -X POST https://api.mail.land/v1/auth/social/exchange \
-H "Content-Type: application/json" \
-d '{"code": "one-time-code-from-callback-redirect"}'Returns the same AuthResponse as regular login.
Delete Account
Deletion is per workspace, not per user account, and only a workspace Owner can start it. It schedules deletion after a grace period rather than deleting immediately:
POST https://api.mail.land/v1/account/lifecycle/schedule-deletion{
"gracePeriodDays": 30
}curl -X POST https://api.mail.land/v1/account/lifecycle/schedule-deletion \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"gracePeriodDays": 30}'gracePeriodDays accepts 7–90 (default 30); values outside that range are clamped, not refused. Cancel the pending deletion any time before the grace period ends:
POST https://api.mail.land/v1/account/lifecycle/cancel-deletionWorkspace Owners schedule deletion with POST /v1/account/lifecycle/schedule-deletion (gracePeriodDays 7–90, default 30, out-of-range values clamped) and can cancel it with POST /v1/account/lifecycle/cancel-deletion until then. There is no DELETE /v1/auth/delete-account endpoint, and this action is not per-user: it deletes the whole workspace once the grace period elapses.
Error Responses
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_FAILED | Registration request failed validation, with an errors[] array |
401 | UNAUTHORIZED | Wrong email or password |
403 | FORBIDDEN | Account is inactive |
409 | CONFLICT | Email already registered |
429 | TOO_MANY_LOGIN_ATTEMPTS | Too many failed login attempts; response includes Retry-After |
An unverified email does not return an error: login responds 200 with requiresEmailVerification: true and a fresh verificationTicket.
Next Steps
- Two-Factor Authentication: add 2FA to your account
- Team Management: invite team members
- Workspaces: manage multiple workspaces
- API Keys: create API keys for server-to-server access