Billing
Manage your subscription, view invoices, and monitor usage
MailLand uses Stripe for subscription management and billing. Each workspace has its own subscription with usage-based or plan-based pricing. Any member can read billing state; only the workspace Owner can change it (mutating calls return 403 INSUFFICIENT_PERMISSIONS, and API keys are always refused on those endpoints).
Payment Statuses
paymentStatus is returned as a snake_case string, not a numeric enum:
| Status | Description |
|---|---|
current | Subscription active, payment up to date |
grace_period | Payment failed, grace period active |
overdue | Payment overdue, features restricted |
blocked | Account blocked due to non-payment |
Get Subscription Details
GET https://api.mail.land/v1/account/subscriptioncurl https://api.mail.land/v1/account/subscription \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"planName": "Pro",
"monthlyEmailLimit": 100000,
"rateLimitPerSecond": 10,
"pricePerMonth": 49.00,
"startDate": "2026-01-15T08:00:00Z",
"endDate": "2026-03-01T00:00:00Z",
"status": "Active",
"paymentStatus": "current",
"marketingPausedDueToPayment": false,
"sendingBlockedDueToPayment": false,
"transactionalDailyLimit": 5000,
"daysUntilBlocked": null,
"cancelAtPeriodEnd": false,
"planFamilyId": "fam_pro",
"planTierPublicId": "tier_pro_monthly",
"usage": {
"emailsSentThisMonth": 45000,
"monthlyLimit": 100000,
"percentageUsed": 45.0,
"transactionalCount": 30000,
"marketingCount": 12000,
"apiCount": 40000,
"smtpCount": 5000,
"campaignCount": 3000,
"automationCount": 2000,
"inboundForwardCount": 500
}
}There is no id, planId, interval, currentPeriodStart/currentPeriodEnd, or nested numeric paymentStatus/validation/domain/team/automation usage fields in this response, use the field names above. paymentStatus is a snake_case string: "current", "grace_period", "overdue" or "blocked". status is PascalCase ("Active", "Suspended", "Cancelled"): the only exception is the no-subscription fallback, which returns lowercase "active".
Usage Fields
| Field | Description |
|---|---|
emailsSentThisMonth | Emails sent in current billing period |
monthlyLimit | Maximum emails per period |
percentageUsed | emailsSentThisMonth as a percentage of monthlyLimit |
transactionalCount | Transactional emails sent |
marketingCount | Marketing emails sent |
apiCount | Emails sent via the API |
smtpCount | Emails sent via SMTP relay |
campaignCount | Emails sent by campaigns |
automationCount | Emails sent by automations |
inboundForwardCount | Emails sent by inbound-forwarding rules |
There are no validation, domain, team-member or automation usage counters in this response, only email-send counters.
When Email Usage Is Counted
Recipients count against your usage and quota (emailsSentThisMonth / monthlyLimit) at the moment acceptance is durably recorded. Recipients that are rejected or suppressed before acceptance never count. A bounce, downstream suppression, delivery failure, or other non-delivery that happens after acceptance does not automatically restore or reclaim usage or quota: the count is not retroactively adjusted. This section describes usage accounting only; it does not state or imply per-email Stripe pricing or any refund right, which are governed separately by your subscription terms.
Quota resets each Stripe billing period for plans billed through Stripe, or on the 1st of each UTC calendar month for plans not billed through Stripe (Free, and admin-granted plans): it is not always "per period" in the Stripe sense.
Plans & Retention
Plans are Free, Pro and Max. Message logs are kept 30 days on Free, 90 days on Pro, and 180 days on Max.
Policy Summary
Get the workspace verification status and sending policy:
GET https://api.mail.land/v1/account/subscription/policy-summarycurl https://api.mail.land/v1/account/subscription/policy-summary \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."The response is a nested object, not flat (SubscriptionController.cs:137-200):
{
"subscription": {
"planFamilyId": "fam_pro",
"planFamilyName": "Pro",
"planTierId": "tier_pro_monthly",
"monthlyQuota": 100000,
"pricePerMonth": 49.00
},
"usage": {
"usedThisMonth": 45000,
"remainingThisMonth": 55000,
"percentUsed": 45.0
},
"effectiveRates": {
"transactionalRatePerMin": 600,
"marketingRatePerMin": 300,
"isOverridden": false
},
"domainWarmup": [
{
"domainId": "h4k7m1n9w2p5x8",
"domainName": "example.com",
"currentStage": 2,
"totalStages": 5,
"todaySentCount": 350,
"dailyLimit": 500,
"progressPercent": 70.0,
"completed": false,
"bypassed": false
}
],
"alerts": [
{
"type": "warning",
"code": "QUOTA_WARNING",
"message": "You have used 80% of your monthly quota. Consider upgrading your plan.",
"actionUrl": "/billing"
}
],
"dailySendingLimit": {
"limit": 5000,
"usedToday": 1200,
"remainingToday": 3800,
"resetsAt": "2026-02-18T00:00:00Z"
},
"verification": {
"level": 2,
"levelName": "SenderVerified",
"limits": {},
"progress": {},
"nextSteps": []
}
}| Field | Description |
|---|---|
subscription | Plan family, tier and monthly quota/price |
usage | Emails used, remaining and the percentage used this month |
effectiveRates | Effective per-minute sending rates and whether they are overridden |
domainWarmup | Per-domain warmup progress, one entry per domain under warmup |
alerts | Active alerts for this workspace (quota, daily limit, and similar) |
dailySendingLimit | Daily send limit and remaining allowance, when applicable |
verification | Current verification level, its limits, progress and next steps |
Create Checkout Session
Start a Stripe Checkout session to subscribe or upgrade:
POST https://api.mail.land/v1/account/billing/checkout{
"planTierId": "tier_pro_monthly",
"locale": "en",
"businessAttested": true,
"businessAttestationKey": "business-use-v3-button"
}| Field | Type | Required | Description |
|---|---|---|---|
planTierId | string | Yes | Plan tier's public id |
locale | string | No | Locale for the hosted checkout page |
businessAttested | boolean | Yes | true confirms you are buying MailLand for business or professional use (including as a freelancer, sole trader or for your own website). The dashboard carries it on the pay button: its label says "Pay for business use" and a one-line note under it states what the click confirms |
businessAttestationKey | string | Yes | The wording version the confirmation refers to; currently business-use-v3-button (business-use-v1, the earlier wording, is still accepted) |
A request without businessAttested: true and an accepted businessAttestationKey is refused with 400 BUSINESS_ATTESTATION_REQUIRED before anything is charged. MailLand stores who confirmed, when, which wording (the key you sent) and in which locale. In Stripe Checkout you can enter a tax ID; it is optional. Prices exclude VAT and MailLand does not charge VAT; a verified EU VAT ID outside the UK adds the reverse-charge note to your invoices.
There is no successUrl or cancelUrl field, success/cancel URLs are not caller-supplied.
curl -X POST https://api.mail.land/v1/account/billing/checkout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{ "planTierId": "tier_pro_monthly", "locale": "en", "businessAttested": true, "businessAttestationKey": "business-use-v3-button" }'{
"upgraded": false,
"sessionId": "cs_live_abc123...",
"url": "https://checkout.stripe.com/c/pay/cs_live_abc123..."
}If the workspace already has an active subscription, the plan is changed in place instead of opening a new checkout: the response is { "upgraded": true } with no sessionId/url, and there is no redirect. Only redirect the user to url when it is present.
Billing Portal
Open the Stripe Customer Portal for self-service management (update payment method, cancel, view invoices):
POST https://api.mail.land/v1/account/billing/portal{
"locale": "en"
}locale is optional.
curl -X POST https://api.mail.land/v1/account/billing/portal \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{ "locale": "en" }'{
"url": "https://billing.stripe.com/p/session/abc123..."
}Redirect the user to the url to access the Stripe portal.
List Invoices
GET https://api.mail.land/v1/account/billing/invoices| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 20 | Number of invoices to return |
cursor | string | - | Opaque cursor from a previous response's nextCursor |
curl https://api.mail.land/v1/account/billing/invoices \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"items": [
{
"publicId": "inv_abc123",
"invoiceNumber": "INV-2026-0015",
"stripeInvoiceId": "in_1AbCdEfGhIjKlMn",
"status": "Paid",
"amountDue": 4900,
"amountPaid": 4900,
"currency": "eur",
"periodStart": "2026-01-01T00:00:00Z",
"periodEnd": "2026-02-01T00:00:00Z",
"paidAt": "2026-01-01T00:05:00Z",
"hostedInvoiceUrl": "https://pay.stripe.com/invoice/inv_abc123",
"invoicePdfUrl": "https://pay.stripe.com/invoice/inv_abc123/pdf",
"createdAt": "2026-01-01T00:00:00Z"
},
{
"publicId": "inv_def456",
"invoiceNumber": "INV-2026-0014",
"stripeInvoiceId": "in_2BcDeFgHiJkLmNo",
"status": "Paid",
"amountDue": 4900,
"amountPaid": 4900,
"currency": "eur",
"periodStart": "2025-12-01T00:00:00Z",
"periodEnd": "2026-01-01T00:00:00Z",
"paidAt": "2025-12-01T00:05:00Z",
"hostedInvoiceUrl": "https://pay.stripe.com/invoice/inv_def456",
"invoicePdfUrl": "https://pay.stripe.com/invoice/inv_def456/pdf",
"createdAt": "2025-12-01T00:00:00Z"
}
],
"nextCursor": null,
"hasMore": false
}This is a cursor-paged response, not { "invoices": [...] }: pass nextCursor back as the cursor parameter to get the next page, and stop when hasMore is false. The default page size is 20, not 10; the field names are invoiceNumber, amountDue, amountPaid, hostedInvoiceUrl and invoicePdfUrl (not number, amount, invoiceUrl, invoicePdf).
Invoice amounts are in the smallest unit of the invoice's currency (cents for EUR). Divide by 100 for display. Plan prices are in euros (EUR) and exclude VAT.
Error Responses
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_FAILED | Invalid request body |
400 | INVALID_REQUEST | Plan tier exists but is not synced to Stripe |
403 | INSUFFICIENT_PERMISSIONS | Only the workspace Owner can perform this mutation (billing reads are open to any member) |
404 | RESOURCE_NOT_FOUND | Unknown or inactive plan tier |
502 | STRIPE_ERROR | Stripe request failed |
Other Billing Endpoints
Also available, not detailed above: POST /v1/account/billing/cancel, POST /v1/account/billing/reactivate, and the public GET /public/plans/catalog.
Next Steps
- Workspaces: manage workspace settings
- Team Management: manage team members
- Rate Limits: understand API rate limits