Outbound fax for B2B SaaS - with HIPAA-grade compliance from day one of self-serve. The primitives modern teams expect: signed webhooks, idempotency keys, magic test numbers, and an error catalog that actually explains itself.
# send a fax. idempotent. signed. curl "https://api.mintfax.com/v1/faxes" \ -X POST \ -H "Authorization: Bearer mfx_live_..." \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "to": "+15551234567", "from_number": "fnum_8KQRm2", "document_url": "https://files.example.com/coversheet.pdf", "metadata": { "patient_id": "p_19a8c" }, "webhook_url": "https://app.example.com/webhooks/fax" }'
import { Mintfax } from "mintfax"; const mfx = new Mintfax(process.env.MINTFAX_KEY); const fax = await mfx.faxes.send({ to: "+15551234567", fromNumber: "fnum_8KQRm2", documentUrl: "https://files.example.com/coversheet.pdf", metadata: { patientId: "p_19a8c" }, idempotencyKey: crypto.randomUUID(), }); console.log(fax.id, fax.status); // fax_2K... queued
from mintfax import Mintfax mfx = Mintfax(os.environ["MINTFAX_KEY"]) fax = mfx.faxes.send( to="+15551234567", from_number="fnum_8KQRm2", document_url="https://files.example.com/coversheet.pdf", metadata={"patient_id": "p_19a8c"}, idempotency_key=uuid.uuid4().hex, ) print(fax.id, fax.status) # fax_2K... queued
require "mintfax" mfx = Mintfax::Client.new(ENV.fetch("MINTFAX_KEY")) fax = mfx.faxes.send( to: "+15551234567", from_number: "fnum_8KQRm2", document_url: "https://files.example.com/coversheet.pdf", metadata: { patient_id: "p_19a8c" }, idempotency_key: SecureRandom.uuid, ) puts fax.id, fax.status # fax_2K... queued
Modern SaaS teams already have opinions about how a third-party API should behave. We share them. Everything below ships on day one of self-serve - not behind an enterprise quote.
Every webhook carries a versioned signature header and a replay-protected timestamp. Rotate secrets without downtime; verify in three lines.
X-Mintfax-Signature: t=..., v1=...Send the same key twice, get the same fax back - never two. Keys are valid for 24h and stored alongside the canonical request hash.
Idempotency-Key: ik_b8c4...Retry policy is part of the response, not folklore. Inspect attempt count, next-attempt-at, and the carrier-side reason that triggered the retry.
retry_policy: { max: 5, backoff: "exp(2,30s)" }+1-555-MFX-BUSY is always busy. -NACK rejects on the first ring. -SLOW takes 90 seconds. Reproduce every failure path locally.
+1 555 MFX-BUSY - MFX-NACK - MFX-SLOWThe spec is the source of truth. Codegen your client, mock locally, diff schema changes in CI. Every endpoint has at least one example that round-trips.
openapi: 3.1.0 - spectral-cleanEvery error has a stable code, a one-line summary, a "what to try next," and a permalink. Nothing reads "internal_error: see logs."
err fax.recipient.unreachable - 12 docsNo enterprise plan. No procurement cycle. No "talk to sales to access encryption." Sign up, sign the BAA in-app, ship.
Most fax APIs were built for the channel - for resellers, hospital billing departments, MSPs. We're building for the engineer integrating fax into a product they didn't choose to build.
| Feature | mintfax | Legacy fax API | Cloud telco platform |
|---|---|---|---|
| HIPAA BAA on self-serve | Free, every paid tier | Enterprise plan only | Add-on, $299/mo |
| HMAC-signed webhooks | Day 1, versioned | Optional, IP allowlist | Day 1 |
| Idempotency keys | First-class, 24h | Best-effort dedupe | Not supported |
| Sandbox with failure simulation | Magic numbers - free | Trial credits only | Test mode only |
| OpenAPI 3.1 spec | Source of truth | PDF reference | 3.0 partial |
| Pricing model | Per-page, no minimums | Seats + minimums | Annual contract |
| Per-workspace audit log | Immutable JSONL | CSV on request | 90 days, console |
Drop your email. We'll send one note when the sandbox opens to the public, and one when GA ships. Nothing else.