PineMail
PricingBusinessDevelopers

PineMail is in beta. Card payment is not open yet, and we raise an account's limits on request. See the limits

JavaScript and TypeScript SDK

The package pinemail wraps the sending API in methods with types, and checks webhook signatures. It has no dependencies and runs on Node 18+, Bun, Deno and edge runtimes. Field names are the API's own (snake_case), so the rest of these docs apply as they are. Other languages use the API directly, with the OpenAPI contract.

Install

bash
npm install https://pinemail.app/sdk/pinemail-0.4.0.tgz

Send

ts
import { PineMail } from "pinemail"; const pinemail = new PineMail(process.env.PINEMAIL_API_KEY); const { id, status } = await pinemail.emails.send( { from: "Acme <hello@acme.example>", to: "ada@example.com", template: { slug: "welcome", variables: { name: "Ada" } }, scheduled_at: "2026-11-02T09:00:00+01:00", }, { idempotencyKey: "welcome-42" }, ); await pinemail.emails.cancel(id);

A request that fails throws PineMailError with status, the API's problem (its detail and field errors) and, on 429, retryAfter in seconds.

What it covers

emailssend, batch, get, list, reschedule, cancel
templateslist, get, create, update, remove, versions, restore
domainslist, get, create, dns, verify, remove
webhookslist, get, create, update, remove, test, retryFailed, retryDelivery
suppressionslist, get, add, remove, softBounces, softBounce
limitsget

pinemail.request(method, path, body) reaches any other endpoint with the same key, such as mailboxes or the directory.

Check a webhook

ts
import { verifyWebhook } from "pinemail"; export async function POST(request: Request) { const raw = await request.text(); const ok = await verifyWebhook(raw, request.headers.get("x-mail-signature"), process.env.PINEMAIL_WEBHOOK_SECRET!); if (!ok) return new Response("bad signature", { status: 400 }); const event = JSON.parse(raw); // event.type is email.delivered, email.bounced, ... return new Response(null, { status: 204 }); }

Pass the body exactly as it arrived, before parsing it. A signature older than five minutes is refused; the fourth argument changes that.