Skip to work
Melbin Kuriakose
Back to work

Sendpository

Transactional email infrastructure I built from API to mail server.

  • Product
  • UX
  • Engineering
  • Infrastructure
  • Solo
  1. API
  2. Queue
  3. Mail serverMine
  4. Inbox

Most alternatives stop at the queue and hand the third box to a supplier. That box is the one I chose to own.

16
schema migrations
12
automated test suites
4
competitors torn down
1
person built it
My role
Product, design, engineering, infrastructure - solo
What it is
Transactional email API with its own mail servers
Status
Live - API, docs and mail infrastructure deployed
Stack
Next.js, TypeScript, Postgres, Postal, Redis, Vercel
Sendpository landing page - the delivery pipeline and a live message log

The problem

Why sending email is still hard in 2026

Four problems, and only one of them is technical.

SPFIs this server allowed to send as this domain?DNS TXT lists our IPsPASSDKIMWas the message altered in transit?Signed with the domain’s private keyPASSDMARCWhat if a check fails?Policy + reports back to usPASSSPF, DKIM and a bounce MX are required - the bounce record is what lets SPF align for DMARCA signature that cannot be verified is a worse signal than no signature, so a failing DKIM record is treated as a bug
Three DNS checks decide whether mail reaches an inbox or a spam folder. Getting all three to pass on a new domain and a brand-new IP is most of the work.

Discovery

Finding out what was actually wrong

No client brief and no research budget. The question was whether this market had a real gap or just a crowded middle.

Signed up to all four and sent real mail through each

What I comparedAll fourSendpository
Transactional emailThe category everyone serves well.YesYes
Bills in your own currency (₹)They bill in USD, so there is conversion on top.NoYes
Indian GST tax invoiceWithout one, the GST paid cannot be claimed back.NoYes
Marketing in the same planMost split the two into separate subscriptions.NoYes
No surprise overage billOverage billing is the most common complaint in this category.NoYes
Two DNS records to startThree or four records, plus an MX entry on a subdomain.NoYes

The opportunity was never better delivery. It was simpler economics, packaging and setup.

From research to a decision

The line that runs from what people complained about to what the product refuses to do.

  1. 01

    Research

    Four competitors used as a customer, plus forums and issue trackers.

  2. 02

    What recurred

    Surprise overage bills. Marketing sold separately. Invoices with no claimable tax.

  3. 03

    The opening

    Not deliverability - economics, packaging and setup.

  4. 04

    The decision

    Price in rupees, one quota for both kinds of mail, a hard stop, two DNS records.

TL;DRThe opportunity was not better email delivery. It was simpler economics, packaging and setup.

How the scope narrowed

What came out of it was narrower than where I started. My first instinct was a general-purpose email API; the research said that competing on deliverability alone against providers with a decade of IP reputation was unwinnable. The gap was somewhere else - in billing, in packaging, and in how much setup a developer is asked to do before anything works.

Product decisions

Six decisions, each one a trade

Acting as the product manager on my own work meant deciding what to refuse.

Should marketing email be a second product?

One quota for both.

Cost
Less revenue per customer.
Gain
One account, one quota, one bill.

How much setup can we demand before someone gives up?

Reversed: three records, not two.

Cost
The onboarding advantage I had been marketing.
Gain
SPF that actually authenticates anything.

Where should the customer publish our records?

A dedicated subdomain, never the apex.

Cost
One more name for the customer to create.
Gain
Adding us cannot break their existing email.

Is verifying a domain once enough?

Re-check every domain, every day.

Cost
A job that can take paying customers offline.
Gain
A lost domain stops being signed for.

What happens when a customer exceeds their plan?

Sending stops at the quota.

Cost
All overage revenue.
Gain
No bill anyone did not expect.

Who is this actually for?

Indian businesses, priced in rupees.

Cost
A much smaller addressable market.
Gain
Tax that can actually be claimed.

Build the hard part, or rent it?

Run the mail servers myself.

Cost
Months of extra work before launch.
Gain
The keys and the reputation are ours.

What is an admin screen actually for?

Show values, with an Edit button.

Cost
One extra click to change anything.
Gain
Scannable screens, no stray keystrokes.

Where each record goes, and why

What blocks a send, what is merely recommended, and which name each one sits on.

Required to send

  • SPFsp.Which servers may send. Ends in a hard fail, not the usual soft one, because nothing of the customer’s sends from this name.
  • DKIMapex2048-bit key, signed as the apex domain so it aligns even under strict DMARC.
  • Return-Pathsp.An MX, because a CNAME cannot share a name with the SPF record. This is what moves the envelope sender inside the customer’s domain, so SPF aligns.

Recommended, never blocking

  • ~DMARCapexReporting policy. Generated and recommended, never blocking.

Everything but the DKIM key sits on an sp. subdomain the customer uses for nothing else, so the SPF record they already publish on their apex for Google Workspace is never touched. An earlier version put SPF on the apex and required only two records - it read as less setup, but the record was never the one evaluated, and SPF could not align for DMARC at all.

What I deliberately did not build

A solo build has one scarce resource, and it is not ideas.

Built

  • Send API with attachments and idempotency
  • Domain verification against live DNS
  • Delivery logs, events and timing
  • Signed webhooks
  • Scheduled sending
  • Suppression list
  • GST billing and invoices
  • Admin console
  • Documentation and Node SDK

Deliberately not built

  • Drag-and-drop template builder
  • Contact lists and segmentation
  • A/B testing of campaigns
  • Inbound email parsing
  • Dedicated IP pools
  • A mobile app

Every one is a real feature of a competing product. None of them gets a developer closer to their first successful send.

The test applied each time

Does a developer need this before their first successful send? A template builder and contact segmentation are genuinely useful and genuinely expensive, and neither gets anyone closer to a working integration. The alternatives page on the product says so plainly, including where competitors are the better choice - a comparison that only flatters itself is dismissed by exactly the reader it was written for.

TL;DREvery major decision traded short-term revenue for trust and simplicity.

The product

Three jobs: send, diagnose, control

Everything a developer needs to send their first email, and everything a business needs to be billed for it.

Request

POST /v1/emails
Authorization: Bearer sk_live_...
Idempotency-Key: 8f14e45f

{
  "from": "billing@yourdomain.com",
  "to": "customer@example.com",
  "subject": "Your invoice",
  "html": "<p>Thanks!</p>"
}

What arrives

Your invoice
  • spf=pass
  • dkim=pass
  • dmarc=pass

Read out of the received headers on a real delivery, not assumed.

Send

One POST, and a message that authenticates on arrival.

Send API

One POST with from, to, subject and body. Attachments, CC and BCC, custom headers, reply-to, and an idempotency key so a retried request never sends twice.

Scheduled sending

Pass a timestamp and the message is held until then, cancellable right up to the moment it is handed to the mail servers.

Node SDK

A typed client published to npm as `sendpository` - zero dependencies, dual ESM and CommonJS builds, and webhook signature verification included.

Diagnose

The first place anyone looks when an email did not arrive.

Delivery logs

Every message with its status, events and timing. Searchable, filterable, exportable - the first place anyone looks when an email did not arrive.

Signed webhooks

Delivery, bounce and complaint events with an HMAC signature and the timestamp inside the signed payload, so a captured request cannot be replayed.

Suppression list

Hard bounces and spam complaints suppress the address automatically. Sending to a dead address twice is how a sender reputation dies.

Control

Domains, money and the console that runs the whole thing.

Domain verification

Generates an RSA keypair per domain, shows the exact DNS records, and checks them against live DNS. Three records, on a subdomain that cannot collide with the customer’s existing mail.

GST billing

Gapless invoice numbering, CGST/SGST/IGST by place of supply, credit notes on refund, Razorpay checkout and autopay, coupons applied before tax.

Admin console

Clients, plans, coupons, invoices, refunds, impersonation with an audit trail, and an infrastructure page that reports what is misconfigured.

Documentation

A full docs site with quickstart, guides and API reference - plus llms.txt, so an AI assistant can answer questions about the product accurately.

One API call passes five gates before a message leaves the building

And the sixth box is the one almost every competitor rents.

  1. POST /emails

    One HTTPS request in.

  2. Authentication

    API key hashed, scope checked.

  3. Domain verification

    Is this domain proven yours?

  4. Quota check

    Reserved inside the UPDATE, so two requests cannot both pass.

  5. Suppression check

    Bounced and complained addresses never sent to twice.

  6. Queue

    Accepted, durable, scheduled if asked.

  7. Mail server

    Postal, on my own IP. This is the part I chose to own.

  8. Inbox

    SPF, DKIM and DMARC passing on arrival.

Inside the box everyone else rents

  1. VPS
  2. Postal
  3. DKIM
  4. DNS
  5. SMTP
  6. Inbox

Developer experience

For a developer product, the client library is the interface

The API is what the product does. The SDK is what using it feels like - so it is designed, not generated.

0
runtime dependencies
3.6 kB
gzipped
ESM + CJS
with types for both
MIT
Node 18+

Three lines to a delivered email.

import { Sendpository } from "sendpository";

const sendpository = new Sendpository(process.env.SENDPOSITORY_API_KEY);

const { id } = await sendpository.emails.send({
  from: "Acme <hello@mail.acme.com>",
  to: ["customer@example.com"],
  subject: "Your code is 481920",
  html: "<p>It expires in 10 minutes.</p>",
});

The key is read from the environment when you do not pass one, because the most common mistake in this category is a key that ends up in a browser bundle.

Two kinds of key

The scope split exists so that the credential in your application is not the credential that can create more of them.

sending

What your application should use

  • Send email
  • Read delivery status

full_access

Kept out of the app

  • Mint more API keys
  • Manage webhooks
  • Read the request logs

A sending key that leaks can send email. It cannot mint more keys - which is the difference between an incident and a catastrophe.

TL;DRZero dependencies is a security decision as much as a size one. Every package a client library pulls in is a package that can reach the key it holds.

Full reference in the SDK guide.

The process

How it was actually built

In the order the risks needed answering, not the order that demos well. Pick a step.

01

Proved the mail actually sent

The first thing built was not a dashboard - it was a real message, from a real domain, arriving in a real inbox with SPF, DKIM and DMARC all passing. Everything else is worthless if that never works, so it came first rather than last.

Architecture

How it is put together

Pick any piece to see what it does. The mail server is the one that makes this a platform rather than a wrapper.

Postal (my VPS)

A real MTA with its own IP and reverse DNS, TLS submission on 587, inbound bounce handling on 25, and a provisioning service so domains register themselves.

The piece almost every competitor rents.

The hard part: letting strangers send as themselves

Prove a customer owns a domain, then sign as them - without ever holding a key that could leak, and without letting one customer send as another.

  1. 01

    Customer adds a domain

  2. 02

    Platform generates an RSA keypair

  3. 03

    Customer publishes SPF and DKIM

  4. 04

    Platform checks live DNS

  5. 05

    Ownership confirmed

  6. 06

    Messages signed with the private key

  7. 07

    Send allowed

The key cannot leak. The private half is encrypted at rest with AES-256-GCM under a key that lives only in the environment, so a database dump yields nothing signable. It is decrypted in memory at send time and never rendered to a browser.

Customer A cannot send as Customer B. Tenant isolation was tested on every endpoint; every cross-account attempt returns 404 rather than a permission error that would confirm the record exists.

Decisions worth defending

The interesting parts were rarely the features. They were the choices underneath.

TL;DRI did not just build the API. I built the infrastructure underneath it.

Security

I attacked it before calling it finished

A platform holding other people’s signing keys has to be tested as an attacker would, not as its author would.

Attacked against the live system, not a staging copy

  • SQL injection
  • Header injection
  • SSRF via webhook URLs
  • Open relay
  • Auth bypass
  • Privilege escalation
  • Cross-tenant access

Tenant isolation held on every endpoint. Parameterised SQL throughout. No open relay. Every admin and cron path refused unauthenticated access.

Three real findings

TL;DRThree real findings, all fixed and covered by tests. The rest held.

Billing

Money that has to reconcile

Indian tax law is specific about invoices. A wrong number is a filing error, not a formatting bug - so try it.

Taxable value
₹2,000.00
CGST @ 9%
₹180.00
SGST @ 9%
₹180.00
Total
₹2,360.00

The total is identical either way - the split is what changes, and getting it wrong is a filing error rather than a display bug.

What else the billing had to get right

Beyond the split: gapless invoice numbering that holds under concurrent requests, credit notes that reverse tax proportionally on refund, coupons applied to the taxable value before tax rather than to the total, and pro-rated upgrades that charge only for the days remaining in the period. All of it arithmetic in whole paise, verified by tests before a rupee moved.

Outcome

Where it stands

Live, authenticating and documented - with the infrastructure work most similar products pay a vendor to avoid.

Mail that authenticates

SPF, DKIM and DMARC all passing from a domain and IP built from nothing - verified in received headers, not assumed.

Tested where it matters

12 suites covering tax arithmetic, quota races, tenant isolation, SSRF, encryption, MFA and retention - run against a real database, not mocks.

Audited against production

Cross-tenant access, injection, open relay and auth bypass all tested live. Two findings fixed; the rest held.

Documented for humans and machines

A full docs site, an SDK published to npm, and structured data plus llms.txt so AI assistants answer questions about it correctly.

A one-person product team

No team, no handover. Every decision was mine to make and mine to live with.

One builder

Pick any one of them to see what it actually meant here.

What this project demonstrates

Most full-stack work stops at the application boundary. This one goes past it - into DNS, TLS, mail transfer agents, IP reputation and tax law - because a transactional email product that stops at the API is a wrapper, not a platform. Taking it end to end meant being the product manager deciding what not to build, the designer drawing the screens, the engineer writing the tax arithmetic, and the operator on the server at two in the morning when the reverse DNS was wrong.