REST · CLI · OpenAPI

SingleFax API docs and CLI

API key plus prepaid credits. Official CLI on npm, or copy-paste REST in curl, Node, Python, Go, or PHP. Chat apps should use MCP instead.

CLI

npx @singlefax/cli send FILE --to +E164 , original Word, Excel, PDF, or image; do not convert to PDF first.

REST

POST /api/v1/faxes with Bearer sf_live_… and an Idempotency-Key.

Chat MCP

Cursor, Claude, Codex, ChatGPT. OAuth, not an API key. Install MCP.

Quickstart

  1. Create a scoped key at API keys (fax:send, fax:read, inbox:read, credits:read).
  2. Prepay credits in the dashboard, or npx @singlefax/cli credits buy.
  3. Send with the CLI below, or POST /api/v1/faxes.

Base URL https://api.singlefax.com (same paths on https://singlefax.com). sf_test_ keys cannot live-send. Do not put keys in a browser.

CLI

Node 20+. Config: SINGLEFAX_API_KEY or ~/.config/singlefax/config.json. Pass the original file (Word, Excel, PowerPoint, PDF, text, or an image). Do not convert to PDF first. Not for MCP OAuth or Stripe MPP.

terminal
export SINGLEFAX_API_KEY=sf_live_…

npx @singlefax/cli send ./document.docx --to +14155552671
npx @singlefax/cli send ./document.pdf --to +14155552671 --wait --json
quote, status, credits
npx @singlefax/cli quote --to +14155552671 --pages 12
npx @singlefax/cli status <id>
npx @singlefax/cli list
npx @singlefax/cli numbers
npx @singlefax/cli numbers buy
npx @singlefax/cli credits
npx @singlefax/cli credits buy --dollars 20

Send a fax

One request: document as base64 plus destination. PDF, Word, Excel, PowerPoint, text, or images. Idempotency-Key is required. Reusing a key with a different body returns 409.

terminal
PDF_B64="$(base64 < document.pdf | tr -d '\n')"

curl -sS https://api.singlefax.com/api/v1/faxes \
  -H "Authorization: Bearer $SINGLEFAX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"to\":\"+14155552671\",\"base64\":\"$PDF_B64\"}"

Larger files, upload then send

What the CLI does: create an upload token, PUT the bytes, then send with uploadToken. PUT also needs the Bearer token. Prefer this over stuffing a large file into JSON. Do not convert to PDF first.

terminal
UPLOAD=$(curl -sS https://api.singlefax.com/api/v1/uploads \
  -H "Authorization: Bearer $SINGLEFAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contentType":"application/pdf"}')

UPLOAD_URL=$(printf '%s' "$UPLOAD" | python3 -c "import json,sys; print(json.load(sys.stdin)['uploadUrl'])")
TOKEN=$(printf '%s' "$UPLOAD" | python3 -c "import json,sys; print(json.load(sys.stdin)['uploadToken'])")

curl -sS -X PUT "$UPLOAD_URL" \
  -H "Authorization: Bearer $SINGLEFAX_API_KEY" \
  -H "Content-Type: application/pdf" \
  --data-binary @document.pdf

curl -sS https://api.singlefax.com/api/v1/faxes \
  -H "Authorization: Bearer $SINGLEFAX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"to\":\"+14155552671\",\"uploadToken\":\"$TOKEN\"}"

REST reference

MethodPathAuthNotes
POST/api/v1/faxesfax:sendIdempotency-Key required. JSON { to, base64 | uploadToken } or multipart file.
GET/api/v1/faxesfax:readList. Query limit, offset.
GET/api/v1/faxes/{id}fax:readStatus including latest delivery attempt.
POST/api/v1/faxes/quotefax:read{ to, pages? | base64? }, quote without charging.
POST/api/v1/uploadsfax:sendReturns uploadToken + uploadUrl. PUT the bytes, then send with uploadToken.
GET/api/v1/creditscredits:readPrepaid balance.
POST/api/v1/credits/checkoutcredits:checkout{ amountCents }. Stripe Checkout for a credit pack.
GET/api/v1/received-faxesinbox:readInbound inbox list. Query limit, cursor. No transcripts in list.
GET/api/v1/received-faxes/{id}inbox:readInbound detail with bounded transcript slice. HIPAA Secure content is omitted.
GET/api/v1/numbersfax:readActive fax numbers.
POST/api/v1/numberscredits:checkout{ sku?, country?, state? }, default inbound_30day. Returns checkoutUrl + reserved phoneNumber.
GET/api/v1/skuspublicCatalog. Same shape as MCP list_skus.
POST/api/v1/checkoutcredits:checkout{ sku }, credit_pack, inbound, subscriptions, lifetime.

Full schemas: openapi.json. Breaking changes: versioning policy (90-day Sunset). HTTP 402 on this credits API means insufficient balance. MPP 402 is only POST /api/v1/machine/faxes.

Webhooks

Signed fax.queued, fax.delivered, fax.failed, and credits.low_balance events. Verify SingleFax-Signature (t=,v1=) against the raw body. HMAC-SHA256 of timestamp.payload. Add an endpoint in dashboard webhooks.

verify.mjs
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifySingleFaxSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((kv) => kv.split('=')))
  const timestamp = Number(parts.t)
  if (!Number.isFinite(timestamp) || !parts.v1) return false
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) return false
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
  const a = Buffer.from(parts.v1)
  const b = Buffer.from(expected)
  return a.length === b.length && timingSafeEqual(a, b)
}

Constraints

  • Keep API keys on the server. Do not embed sf_live_ in a webpage.
  • Do not send PHI. Covered Entities use HIPAA Secure Mode on the website with an electronic BAA.
  • Valid E.164 destinations. PDF, Word, Excel, PowerPoint, RTF, plain text, and images, see OpenAPI for MIME types. Do not convert to PDF first; we convert before sending.
  • Chat MCP and Stripe MPP: /agents, mpp.md, and MPP vs Instant Checkout.

FAQ

Machine-readable docs

Plain markdown and OpenAPI for crawlers and coding agents. Same facts as this page.