LinkPop Private Email
Private email server for agency gigworkers, creators, and marketers who manage multiple social media accounts. Create unlimited disposable email addresses at @linkpop.site for platform signups, account management, and online services — without risking your primary inbox.
Why LinkPop Mail?
Managing multiple social media accounts means juggling dozens of email addresses. One platform ban can cascade across every account tied to your personal Gmail. LinkPop Mail solves this:
- Unlimited disposable mailboxes — Spin up
anything@linkpop.sitein seconds for Instagram, TikTok, Twitter, or any platform signup - Built-in password manager — Store login credentials scoped to each mailbox
- Built-in 2FA authenticator — Generate TOTP codes without a separate app, with QR code/webcam scanning
- Shareable standalone mailbox links — Hand off password-protected mailbox access to clients or team members via a standalone URL
- Email attachments — Private S3 storage with presigned URL access
- Auto-forward & webhooks — Real-time notifications for incoming emails (whitelisted users)
- API access — Full REST API for programmatic mailbox and email management
Pricing
| Tier | Mailboxes | Monthly Emails | Passwords | 2FA | Price |
|---|---|---|---|---|---|
| Free | 10 | 100 | 100 | 100 | $0/mo |
| Pro | 100 (unlimited with custom domain) | 1,000 (+$0.01/extra) | Unlimited | Unlimited | $9/mo |
URLs
| Resource | Staging | Production |
|---|---|---|
| Frontend | https://mail-staging.cloud.linkpop.site |
https://mail.cloud.linkpop.site |
| API Base | https://mail-staging.cloud.linkpop.site/api |
https://mail.cloud.linkpop.site/api |
| Resend Webhook | https://mail-staging.cloud.linkpop.site/api/webhooks/resend |
https://mail.cloud.linkpop.site/api/webhooks/resend |
Authentication
OTP Login (Web Users)
POST /auth/send-otpwith{ "email": "user@example.com" }— sends a 6-digit OTPPOST /auth/verify-otpwith{ "email": "user@example.com", "otp": "123456" }— returns JWT token + user profile
API Key (Programmatic Access)
After login, users have an API key in their settings. Pass it via header:
Authorization: Bearer <base64(user_id:secret)>
Rotate keys: POST /auth/rotate-api-key
Share Token (Standalone Mailbox)
Password-protected access via share token:
GET /share/:token/info— returns mailbox addressPOST /share/:token/authwith{ "password": "..." }— returns session token- Use session token for subsequent
/share/:token/*requests
API Endpoints
Auth (/auth)
| Method | Path | Description |
|---|---|---|
POST |
/auth/send-otp |
Send OTP to email. Body: { "email": "..." } |
POST |
/auth/verify-otp |
Verify OTP, login/register. Body: { "email": "...", "otp": "..." } |
POST |
/auth/login |
Email + password login. Body: { "email": "...", "password": "..." } |
POST |
/auth/register |
Register. Body: { "email": "...", "password": "..." } |
GET |
/auth/me |
Get current user profile |
POST |
/auth/change-password |
Change password. Body: { "current_password": "...", "new_password": "..." } |
POST |
/auth/rotate-api-key |
Rotate API key, returns new key |
Mailboxes (/mailboxes)
| Method | Path | Description |
|---|---|---|
GET |
/mailboxes |
List all user mailboxes |
POST |
/mailboxes |
Create mailbox. Body: { "address": "myname", "label": "optional label" } |
GET |
/mailboxes/:addr |
Get mailbox details |
PATCH |
/mailboxes/:addr |
Update mailbox. Body: { "label": "...", "webhook_url": "...", "auto_forward": "...", "share_password": "..." } |
DELETE |
/mailboxes/:addr |
Delete mailbox |
POST |
/mailboxes/:addr/share |
Create share link with password |
DELETE |
/mailboxes/:addr/share |
Remove share link |
Note: Address format is username@linkpop.site (production) or username.staging@linkpop.site (staging). A random 12-character share password is auto-generated on creation.
Emails (/emails)
| Method | Path | Description |
|---|---|---|
GET |
/emails/mailbox/:addr |
List emails for a mailbox |
GET |
/emails/all |
List all emails across all mailboxes |
GET |
/emails/:addr/:id |
Get single email (marks as read) |
POST |
/emails/reply |
Reply to email. Body: { "to": "...", "subject": "...", "html": "...", "in_reply_to": "..." } |
POST |
/emails/send |
Send new email (whitelisted only). Body: { "from_address": "...", "to": "...", "subject": "...", "html": "..." } |
POST |
/emails/forward |
Forward email (whitelisted only). Body: { "email_id": "...", "address": "...", "forward_to": "..." } |
GET |
/emails/attachments/:addr/:emailId/:attId |
Get presigned URL for attachment |
Passwords (/passwords)
| Method | Path | Description |
|---|---|---|
GET |
/passwords |
List passwords. Query: ?search=&mailbox= |
POST |
/passwords |
Create password entry. Body: { "mailbox_address": "...", "website": "...", "username": "...", "password": "...", "notes": "..." } |
PATCH |
/passwords/:id |
Update password entry |
DELETE |
/passwords/:id |
Delete password. Query: ?mailbox_address= |
TOTP / 2FA (/totp)
| Method | Path | Description |
|---|---|---|
GET |
/totp |
List TOTP entries. Query: ?mailbox=&search= |
POST |
/totp |
Create TOTP entry. Body: { "mailbox_address": "...", "label": "...", "secret": "...", "issuer": "..." } or { "mailbox_address": "...", "uri": "otpauth://..." } |
DELETE |
/totp/:id |
Delete TOTP entry. Query: ?mailbox_address= |
Shared Mailbox (/share/:token)
Password-protected standalone access. All endpoints require prior auth via POST /share/:token/auth.
| Method | Path | Description |
|---|---|---|
GET |
/share/:token/info |
Get mailbox address (for login screen) |
POST |
/share/:token/auth |
Authenticate. Body: { "password": "..." } |
GET |
/share/:token/emails |
List emails |
GET |
/share/:token/emails/:id |
Get single email |
GET |
/share/:token/attachments/:emailId/:attId |
Get attachment URL |
GET |
/share/:token/passwords |
List passwords |
POST |
/share/:token/passwords |
Create password |
PATCH |
/share/:token/passwords/:id |
Update password |
DELETE |
/share/:token/passwords/:id |
Delete password |
GET |
/share/:token/totp |
List TOTP entries |
POST |
/share/:token/totp |
Create TOTP |
DELETE |
/share/:token/totp/:id |
Delete TOTP |
Admin (/admin)
Requires X-Admin-Key header matching SUPERADMIN_KEY env var.
| Method | Path | Description |
|---|---|---|
POST |
/admin/login-as |
Login as any user. Body: { "user_id": "..." } |
POST |
/admin/set-tier |
Set user tier. Body: { "user_id": "...", "tier": "FREE_TIER|PAID_TIER|UNLIMITED_TIER" } |
GET |
/admin/stats/websites |
Password count per website. Query: ?website= |
GET |
/admin/whitelist |
List whitelist entries |
POST |
/admin/whitelist |
Add whitelist. Body: { "identifier": "user:UUID" } or { "identifier": "mailbox:addr@linkpop.site" } |
DELETE |
/admin/whitelist/:email |
Remove whitelist entry |
Webhooks (/webhooks)
| Method | Path | Description |
|---|---|---|
POST |
/webhooks/resend |
Resend inbound email webhook (called by Resend) |
Data Models
User
interface User {
user_id: string;
email: string;
tier: "FREE_TIER" | "PAID_TIER" | "UNLIMITED_TIER";
api_key_id: string;
api_key_secret: string; // hashed
monthly_email_count: number;
monthly_email_reset: string; // ISO date
created_at: string;
updated_at: string;
}
Mailbox
interface Mailbox {
user_id: string;
address: string; // e.g. "myname@linkpop.site"
label?: string;
share_token?: string;
share_password?: string; // hashed, auto-generated 12 chars
webhook_url?: string; // requires whitelist
auto_forward?: string; // requires whitelist
created_at: string;
updated_at: string;
}
interface Email {
address: string; // mailbox address
email_id: string;
from: string;
to: string;
subject: string;
text_body?: string;
html_body?: string;
attachments?: Attachment[];
read: boolean;
timestamp: string;
}
interface Attachment {
id: string;
filename: string;
content_type: string;
size: number;
s3_key: string;
}
Password Entry
interface PasswordEntry {
password_id: string;
user_id: string;
mailbox_address: string;
website: string;
username: string;
password: string; // encrypted
notes?: string;
created_at: string;
updated_at: string;
}
TOTP Entry
interface TOTPEntry {
totp_id: string;
user_id: string;
mailbox_address: string;
label: string;
secret: string;
issuer?: string;
algorithm?: string;
digits?: number;
period?: number;
created_at: string;
}
Rate Limits by Tier
| Tier | Mailboxes | Emails/Month | Passwords | TOTP |
|---|---|---|---|---|
FREE_TIER |
10 | 100 | 100 | 100 |
PAID_TIER |
100 | 1,000 | Unlimited | Unlimited |
UNLIMITED_TIER |
Unlimited | Unlimited | Unlimited | Unlimited |
Whitelist System
Certain features require admin whitelisting:
- Send emails —
POST /emails/send - Forward emails —
POST /emails/forward - Auto-forward — Setting
auto_forwardon a mailbox - Webhooks — Setting
webhook_urlon a mailbox
Whitelist can be granted at two levels:
- User level:
user:<user_id>— applies to all mailboxes - Mailbox level:
mailbox:<address>— applies to specific mailbox only
Architecture
- Frontend: React 18 + TypeScript + TailwindCSS + Zustand
- Backend: AWS Lambda (Node.js 20) + Express.js
- Database: DynamoDB (single table design, 3 GSIs)
- Storage: S3 (private attachments with presigned URLs, 365-day TTL)
- Email: Resend API (inbound webhook + outbound sending)
- Infrastructure: AWS CDK, CloudFront + S3 (frontend), API Gateway (backend)
- Domain:
linkpop.sitevia Route53 + ACM SSL
Stage Isolation
- Staging: Emails use
*.staging@linkpop.site, frontend atmail-staging.cloud.linkpop.site - Production: Emails use
*@linkpop.site, frontend atmail.cloud.linkpop.site - Resend webhook filters by stage suffix to prevent cross-stage contamination
Usage Examples
Create a Mailbox
const API = "https://mail-staging.cloud.linkpop.site/api";
const TOKEN = "your-jwt-token";
const res = await fetch(`${API}/mailboxes`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify({
address: "mycampaign",
label: "Q1 Marketing Campaign",
}),
});
const mailbox = await res.json();
// mailbox.address = "mycampaign.staging@linkpop.site"
// mailbox.share_token = "abc123..." (for standalone link)
// mailbox.share_password = "xK9m2..." (auto-generated)
List Emails
const emails = await fetch(`${API}/emails/mailbox/mycampaign.staging@linkpop.site`, {
headers: { Authorization: `Bearer ${TOKEN}` },
}).then((r) => r.json());
Store a Password
await fetch(`${API}/passwords`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify({
mailbox_address: "mycampaign.staging@linkpop.site",
website: "instagram.com",
username: "mycampaign_handle",
password: "s3cur3p4ss",
notes: "Client account for Q1 campaign",
}),
});
Add a TOTP Entry
await fetch(`${API}/totp`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify({
mailbox_address: "mycampaign.staging@linkpop.site",
uri: "otpauth://totp/Instagram:mycampaign?secret=JBSWY3DPEHPK3PXP&issuer=Instagram",
}),
});
Access Shared Mailbox
const SHARE_URL = "https://mail-staging.cloud.linkpop.site/api/share/abc123token";
// 1. Get mailbox info
const info = await fetch(`${SHARE_URL}/info`).then((r) => r.json());
// info.address = "mycampaign.staging@linkpop.site"
// 2. Authenticate
const auth = await fetch(`${SHARE_URL}/auth`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: "xK9m2..." }),
}).then((r) => r.json());
// 3. List emails
const emails = await fetch(`${SHARE_URL}/emails`, {
headers: { Authorization: `Bearer ${auth.token}` },
}).then((r) => r.json());
Error Handling
| Status | Meaning |
|---|---|
| 400 | Bad request / validation error |
| 401 | Unauthorized / invalid token |
| 403 | Forbidden / whitelist required |
| 404 | Resource not found |
| 429 | Rate limit exceeded (tier limit) |
| 500 | Internal server error |
Error response format:
{
"error": "Error message describing what went wrong"
}
Deployment
npm run install:all # Install all dependencies
npm run build:api # Build Lambda API
npm run build:web # Build React frontend
npm run deploy:staging # Deploy to staging via CDK
npm run deploy:production # Deploy to production via CDK
Deployment outputs are written to deployment.staging.json and deployment.production.json.