Set up email
Hardcount sends email through your email provider’s SMTP server. SMTP is the standard way programs hand email to a provider. This page connects a provider, checks that email arrives, and sets up bounce handling.
Hardcount sends:
- invoices, receipts and credit notes,
- dunning emails (reminders after a card payment fails),
- payment-failed emails and links to add a payment method,
- password resets and team invites.
Hardcount sends about 20 to 30 emails a minute, and you cannot change that. Emails above that rate wait in a queue and go out in order. Hardcount sends only through SMTP, so it cannot show opens or clicks.
Connect a provider
Section titled “Connect a provider”-
Set the SMTP settings on the Hardcount server. Your provider gives you these values; Settings for each provider has them for common ones.
Terminal window SMTP_HOST=smtp.your-provider.comSMTP_PORT=587SMTP_USERNAME=your-username-or-apikeySMTP_PASSWORD=your-password-or-apikey-secretSMTP_FROM=billing@yourdomain.comSMTP_TLS=starttls # starttls (default) | implicit | noneSMTP_TLSis how the connection is encrypted:starttlsfor port 587,implicitfor port 465.nonesends in plain text: use it only for a local test inbox, never in production, where Hardcount refuses to send with it. -
Set the web addresses that the links in emails point to. With the one-VM install, all of them use your dashboard’s address.
Terminal window HOSTED_INVOICE_BASE_URL=https://billing.example.comPAYMENT_UPDATE_URL=https://billing.example.com/update-paymentDASHBOARD_BASE_URL=https://billing.example.comCUSTOMER_PORTAL_URL=https://billing.example.comWhat each one is for: What breaks when a setting is missing.
-
Restart Hardcount. With Docker Compose, run
docker compose up -d.You see: a start-up log line
SMTP configuredwith your host. A missing setting logs a warning instead; see What breaks when a setting is missing. -
In the dashboard, open Settings → Email.
You see: the badge reads Ready, and each setting under “Email links” reads Set.
-
Click Send test email.
You see: “Sent to” and your address. The email arrives within a few minutes. If it does not, check spam and the server log.
Keep email out of spam
Section titled “Keep email out of spam”Before you go live, set up your sending domain at your provider, so emails are trusted. Hardcount cannot do this for you, because the domain’s DNS is yours.
- SPF: a DNS record that lists the servers allowed to send for your domain.
- DKIM: a signature on each email. Most providers turn it on when you add their DNS records.
- DMARC: a policy for email that fails the two checks above. Start with
p=none, which only reports, then move top=quarantineonce SPF and DKIM pass.
Each provider has step-by-step DNS instructions. You do this once per sending domain.
Use a SMTP_FROM address on that domain. Every email Hardcount sends carries a Date and a Message-ID header, and the Message-ID ends with the SMTP_FROM domain.
What breaks when a setting is missing
Section titled “What breaks when a setting is missing”Hardcount starts without any of these. It logs one warning at start-up for each one that is missing, starting with the setting’s name (SMTP NOT CONFIGURED for SMTP_HOST, … NOT SET for the others).
| Setting | What Hardcount uses it for | Without it |
|---|---|---|
SMTP_HOST |
Your provider’s SMTP server | No email is sent. Queued emails are retried for about 72 hours, then marked failed. Actions that send email right away (send an invoice or a credit note, send a setup link, invite a teammate) refuse with 409 email_not_configured. |
HOSTED_INVOICE_BASE_URL |
The “View & pay invoice” and “View receipt” buttons. Hardcount adds /invoice/<token>. |
Invoice, receipt, dunning and payment-failed emails have no link in their button. |
PAYMENT_UPDATE_URL |
The link in the email that asks a customer to add a payment method. Hardcount adds ?token=…. |
That email is not sent. Hardcount sends it when an invoice has no payment method on file, and when you click Resend setup link. Payment-failed and dunning emails link to the invoice page instead, so they do not need it. |
DASHBOARD_BASE_URL |
Password-reset and team-invite links | Password-reset emails are not sent, and team invites are refused. Hardcount never builds these links from the address in a request. |
CUSTOMER_PORTAL_URL |
Not an email link: where Stripe sends a customer after they save a card. Hardcount adds /payment-method-added. |
Stripe sends the customer to http://localhost:5173, which exists only in local development. The card is still saved. |
Settings for each provider
Section titled “Settings for each provider”Each example assumes you have already set up the sending domain at the provider.
SendGrid
Section titled “SendGrid”SMTP_HOST=smtp.sendgrid.netSMTP_PORT=587SMTP_USERNAME=apikeySMTP_PASSWORD=SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSMTP_FROM=billing@yourdomain.comSMTP_TLS=starttlsSMTP_USERNAME is the word apikey; the password is your API key. SendGrid SMTP docs
Postmark
Section titled “Postmark”SMTP_HOST=smtp.postmarkapp.comSMTP_PORT=587SMTP_USERNAME=YOUR-SERVER-API-TOKENSMTP_PASSWORD=YOUR-SERVER-API-TOKENSMTP_FROM=billing@yourdomain.comSMTP_TLS=starttlsThe user name and the password are both the same Server API Token. Postmark is for transactional email only, so send billing email through it, not marketing. SMTP_FROM must be a verified Sender Signature or on a verified domain: Postmark accepts email from an unverified address and then drops it, so Hardcount shows it as sent while nothing arrives. Postmark can also report bounces back to Hardcount; see Bounces and spam complaints. Postmark SMTP docs
AWS SES, port 587
Section titled “AWS SES, port 587”SMTP_HOST=email-smtp.us-east-1.amazonaws.com # use your regionSMTP_PORT=587SMTP_USERNAME=YOUR-SMTP-USERNAME-FROM-IAMSMTP_PASSWORD=YOUR-SMTP-PASSWORD-FROM-IAMSMTP_FROM=billing@yourdomain.comSMTP_TLS=starttlsThe user name and password are not your AWS credentials. Create SMTP credentials in the SES console (IAM → Create SMTP credentials). Verify your sending domain in SES first. A new SES account starts in sandbox mode, where it sends only to verified addresses. SES SMTP docs
AWS SES, port 465
Section titled “AWS SES, port 465”SMTP_HOST=email-smtp.us-east-1.amazonaws.comSMTP_PORT=465SMTP_USERNAME=YOUR-SMTP-USERNAME-FROM-IAMSMTP_PASSWORD=YOUR-SMTP-PASSWORD-FROM-IAMSMTP_FROM=billing@yourdomain.comSMTP_TLS=implicitUse this when your firewall blocks port 587. Only the connection differs.
Mailgun
Section titled “Mailgun”SMTP_HOST=smtp.mailgun.orgSMTP_PORT=587SMTP_USERNAME=postmaster@yourdomain.mailgun.orgSMTP_PASSWORD=YOUR-MAILGUN-SMTP-PASSWORDSMTP_FROM=billing@yourdomain.comSMTP_TLS=starttlsSMTP_USERNAME is the SMTP user from the Mailgun dashboard (domain → SMTP credentials), not your account email. Mailgun SMTP docs
Resend
Section titled “Resend”SMTP_HOST=smtp.resend.comSMTP_PORT=587SMTP_USERNAME=resendSMTP_PASSWORD=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSMTP_FROM=billing@yourdomain.comSMTP_TLS=starttlsSMTP_USERNAME is the word resend. Resend SMTP docs
Mailtrap (test inbox, not for production)
Section titled “Mailtrap (test inbox, not for production)”Mailtrap catches every email in its own inbox, so nothing reaches a real person. Use it on a staging server.
SMTP_HOST=sandbox.smtp.mailtrap.ioSMTP_PORT=2525SMTP_USERNAME=YOUR-MAILTRAP-USERNAMESMTP_PASSWORD=YOUR-MAILTRAP-PASSWORDSMTP_FROM=billing@example.comSMTP_TLS=starttlsMailpit (local development)
Section titled “Mailpit (local development)”Mailpit is a local email catcher with a web inbox. The repository’s root docker-compose.yml runs it next to Postgres and Redis.
-
Start it.
Terminal window docker compose up -d mailpit -
Point Hardcount at it. Set the link settings too, or the links in the emails are empty.
Terminal window SMTP_HOST=localhostSMTP_PORT=1025SMTP_USERNAME=SMTP_PASSWORD=SMTP_FROM=billing@local.testSMTP_TLS=noneHOSTED_INVOICE_BASE_URL=http://localhost:5173CUSTOMER_PORTAL_URL=http://localhost:5173PAYMENT_UPDATE_URL=http://localhost:5173/update-paymentDASHBOARD_BASE_URL=http://localhost:5173 -
Send an email from Hardcount.
You see: it in the inbox at http://localhost:8025. Nothing leaves your machine.
Common mistakes
Section titled “Common mistakes”| Mistake | What you see | Fix |
|---|---|---|
| Sending domain not set up | Emails land in spam | Set up SPF and DKIM at your provider |
| Wrong user name | 535 authentication failed |
The user name differs per provider (see above) |
SMTP_FROM not verified at the provider |
550 not authorized |
Verify the sending domain, or use a verified address |
| Provider still in sandbox mode | 554 recipient not verified |
Move the account out of sandbox mode (SES), or verify the recipient |
| Firewall blocks port 587 | Connection timeout | Use port 465 and SMTP_TLS=implicit |
| Provider limits your sending rate | 421 throttled |
Raise the limit in your provider’s dashboard |
Check a real invoice email
Section titled “Check a real invoice email”Send one invoice email and follow it.
-
Set three shell variables:
API: your Hardcount address, such ashttps://billing.example.com.KEY: a secret API key (hc_secret_…) in the same mode, test or live, as the invoice. See API keys.INV_ID: the ID of a finalized invoice.
-
Send it.
Terminal window curl -X POST -H "Authorization: Bearer $KEY" \-H "Content-Type: application/json" \-d '{"email":"you@yourdomain.com"}' \"$API/v1/invoices/$INV_ID/send"You see: a 2xx answer, and soon after, the email in your inbox.
-
Open the customer’s page in the dashboard and find Sent emails.
You see: the email with status
dispatched, which means your provider accepted it. With the Postmark webhook below, it then changes todelivered.
If the email stays pending, Hardcount is retrying it. The server log shows the provider’s error; fix the cause it names. Hardcount retries for about 72 hours, then marks the email failed.
If the email is failed at once with email: recipient is not a single valid email address, the customer’s email or a CC entry is not one bare address, such as Bob <bob@example.com>. New addresses are checked when they are entered; an address stored earlier may still have this form. Hardcount does not retry this email. Change the address to the bare form (bob@example.com) and send again.
Bounces and spam complaints
Section titled “Bounces and spam complaints”A bounce is an email that could not be delivered. Hardcount stops emailing an address that bounced or that marked Hardcount’s email as spam.
Both ways below need HARDCOUNT_EMAIL_BIDX_KEY (64 hex characters, openssl rand -hex 32). Hardcount uses it to find the customer by email address, because stored emails are encrypted. Without it, Hardcount cannot mark a customer as bounced, and it logs a warning at start-up.
When the SMTP server rejects the address
Section titled “When the SMTP server rejects the address”Hardcount marks the customer’s email_status as bounced, and sends no more email to that address. A rejection is the server’s answer to the recipient address: a 550, 551, 552 or 553 reply, or any 5xx reply whose status starts with 5.1 or 5.2. This catches failures that happen while Hardcount is still connected.
These do not mark a customer bounced, and Hardcount retries them:
- an authentication error (535),
- a policy rejection (a 5.7.x status such as
550 5.7.1 relaying denied), - a 4xx reply,
- a network error such as a timeout,
- a refusal of the sender or of the message, whatever its code, such as SendGrid’s
550 The from address does not match a verified Sender Identity.
When Postmark reports a bounce later
Section titled “When Postmark reports a bounce later”Some bounces arrive later: the provider accepts the email, and the recipient’s mailbox rejects it afterwards. Hardcount hears about these only from Postmark, through a webhook (a message Postmark sends to Hardcount) at POST /v1/webhooks/postmark.
-
Pick a user name and a password for the webhook, and set them on the Hardcount server:
Terminal window POSTMARK_WEBHOOK_USER=hardcount-postmarkPOSTMARK_WEBHOOK_PASS=$(openssl rand -hex 24)On the Compose stack, put them in
.env. -
Restart Hardcount.
You see: no
POSTMARK_WEBHOOK_USER/POSTMARK_WEBHOOK_PASS not setwarning at start-up. -
In Postmark, open your server, then Webhooks, and add this URL, with Delivery, Bounce and Spam Complaint checked:
https://<user>:<pass>@your-api-host/v1/webhooks/postmarkPostmark does not sign its webhooks, so the user name and password in the URL prove that a call comes from your Postmark account.
-
Send a test invoice email.
You see: on the customer page, under Sent emails, the email’s status changes from
dispatchedtodeliveredonce Postmark reports it.
What Hardcount does with each report:
- Delivery: marks the email
delivered. - Bounce: a permanent bounce (Postmark calls it a hard bounce, or has deactivated the address) marks the email
bouncedand the customerbounced, so Hardcount sends no more email to that address. A temporary bounce changes nothing; Postmark retries it. - Spam Complaint: shows the email as
marked as spam, marks the customercomplained, and stops email to that address.
Hardcount also sends the customer.email_bounced or customer.email_complained webhook event when it marks a customer.
If the two settings are unset, the route answers 401 to every call, and emails stay in the state Hardcount last knew.
Other providers (SES, SendGrid, Mailgun, Resend): Hardcount cannot receive their bounce reports. Turn on suppression (a do-not-send list) in the provider’s dashboard, so it stops sending to addresses that bounce.
Next: Connect Stripe.