MailLandDocs

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:

StatusDescription
currentSubscription active, payment up to date
grace_periodPayment failed, grace period active
overduePayment overdue, features restricted
blockedAccount blocked due to non-payment

Get Subscription Details

GET https://api.mail.land/v1/account/subscription
Get subscription details
curl https://api.mail.land/v1/account/subscription \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Subscription response
{
  "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

FieldDescription
emailsSentThisMonthEmails sent in current billing period
monthlyLimitMaximum emails per period
percentageUsedemailsSentThisMonth as a percentage of monthlyLimit
transactionalCountTransactional emails sent
marketingCountMarketing emails sent
apiCountEmails sent via the API
smtpCountEmails sent via SMTP relay
campaignCountEmails sent by campaigns
automationCountEmails sent by automations
inboundForwardCountEmails 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-summary
Get policy summary
curl 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):

Policy summary response
{
  "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": []
  }
}
FieldDescription
subscriptionPlan family, tier and monthly quota/price
usageEmails used, remaining and the percentage used this month
effectiveRatesEffective per-minute sending rates and whether they are overridden
domainWarmupPer-domain warmup progress, one entry per domain under warmup
alertsActive alerts for this workspace (quota, daily limit, and similar)
dailySendingLimitDaily send limit and remaining allowance, when applicable
verificationCurrent 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
Checkout request
{
  "planTierId": "tier_pro_monthly",
  "locale": "en",
  "businessAttested": true,
  "businessAttestationKey": "business-use-v3-button"
}
FieldTypeRequiredDescription
planTierIdstringYesPlan tier's public id
localestringNoLocale for the hosted checkout page
businessAttestedbooleanYestrue 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
businessAttestationKeystringYesThe 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.

Create checkout session
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" }'
Checkout response
{
  "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
Portal request
{
  "locale": "en"
}

locale is optional.

Open billing portal
curl -X POST https://api.mail.land/v1/account/billing/portal \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{ "locale": "en" }'
Portal response
{
  "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
ParameterTypeDefaultDescription
limitint20Number of invoices to return
cursorstring-Opaque cursor from a previous response's nextCursor
List invoices
curl https://api.mail.land/v1/account/billing/invoices \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Invoices response
{
  "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

StatusCodeDescription
400VALIDATION_FAILEDInvalid request body
400INVALID_REQUESTPlan tier exists but is not synced to Stripe
403INSUFFICIENT_PERMISSIONSOnly the workspace Owner can perform this mutation (billing reads are open to any member)
404RESOURCE_NOT_FOUNDUnknown or inactive plan tier
502STRIPE_ERRORStripe 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

On this page