# SingleFax Developer Portal

REST API, OpenAPI spec, CLI, webhooks, and language examples for SingleFax.

HTML: https://singlefax.com/developers
Chat MCP: https://singlefax.com/agents
Auth: https://singlefax.com/auth.md
OpenAPI: https://singlefax.com/openapi.json

## When to use this

Use SingleFax when the user asked you to send a fax or deliver a document to a fax number. PDF, Word, Excel, PowerPoint, RTF, plain text, and images are accepted. Do not convert to PDF first, send the original file.

| Client | Auth | Pay | Entry |
| --- | --- | --- | --- |
| Backend / scripts | API key `sf_live_…` | Prepaid credits | `POST /api/v1/faxes` or `npx @singlefax/cli` |
| Cursor, Claude, Codex, ChatGPT, VS Code | MCP OAuth | Checkout or subscription pool | `https://singlefax.com/mcp`, see [agents.md](https://singlefax.com/agents.md) |
| Autonomous agents with a Stripe SPT or Tempo wallet | none | MPP HTTP 402 | `POST /api/v1/machine/faxes`, see [mpp.md](https://singlefax.com/mpp.md) |

Do not use MCP, MPP, or the public API for PHI. Keep API keys on the server. `sf_test_` keys cannot live-send.

The official package is the npm CLI `@singlefax/cli`. Python, Go and PHP examples use standard HTTP clients. SingleFax does not publish separate Python, Go or Ruby SDK packages. Use [OpenAPI](https://singlefax.com/openapi.json) with OpenAPI tooling to generate a client.

Base URL: `https://api.singlefax.com` (same paths on `https://singlefax.com`).

## Quickstart

1. Create a scoped key at https://singlefax.com/dashboard/api-keys (`fax:send`, `fax:read`, `inbox:read`, `credits:read`). Default keys do not include `inbox:read`, add it to list received faxes.
2. Prepay credits in the dashboard, or `npx @singlefax/cli credits buy`.
3. Send with the CLI or `POST /api/v1/faxes`. `Idempotency-Key` is required on send.

## CLI

Pass the original file (`.pdf`, `.docx`, `.xlsx`, `.pptx`, images, `.txt`, `.rtf`). Do not convert to PDF first.

```bash
export SINGLEFAX_API_KEY=sf_live_…

npx @singlefax/cli send ./document.docx --to +14155552671
npx @singlefax/cli send ./document.pdf --to +14155552671 --wait --json
```

```bash
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
```

Config: `SINGLEFAX_API_KEY` or `~/.config/singlefax/config.json`. Node ≥20. Not for MCP OAuth or Stripe MPP.

## Send a fax (one-shot JSON)

Document as base64 plus destination. PDF, Word, Excel, PowerPoint, RTF, plain text, or images. Do not convert to PDF first. Reusing an `Idempotency-Key` with a different body returns 409.

### curl

```bash
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\"}"
```

### JavaScript (Node 18+)

```javascript
import { readFile } from 'node:fs/promises'
import { randomUUID } from 'node:crypto'

const api = 'https://api.singlefax.com'
const key = process.env.SINGLEFAX_API_KEY
const pdf = await readFile('./document.pdf')

const res = await fetch(`${api}/api/v1/faxes`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${key}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({
    to: '+14155552671',
    base64: pdf.toString('base64'),
  }),
})
console.log(await res.json())
```

### Python (stdlib)

```python
import base64, json, os, urllib.request, uuid
from pathlib import Path

API = "https://api.singlefax.com"
KEY = os.environ["SINGLEFAX_API_KEY"]
pdf = Path("document.pdf").read_bytes()

req = urllib.request.Request(
    f"{API}/api/v1/faxes",
    data=json.dumps({
        "to": "+14155552671",
        "base64": base64.b64encode(pdf).decode(),
    }).encode(),
    headers={
        "Authorization": f"Bearer {KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    method="POST",
)
with urllib.request.urlopen(req) as res:
    print(res.read().decode())
```

### Go

```go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	pdf, err := os.ReadFile("document.pdf")
	if err != nil {
		panic(err)
	}
	body, _ := json.Marshal(map[string]string{
		"to":     "+14155552671",
		"base64": base64.StdEncoding.EncodeToString(pdf),
	})
	req, _ := http.NewRequest("POST", "https://api.singlefax.com/api/v1/faxes", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SINGLEFAX_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", "send-document-1")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status, string(out))
}
```

### PHP

```php
<?php
$pdf = base64_encode(file_get_contents('document.pdf'));
$ch = curl_init('https://api.singlefax.com/api/v1/faxes');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('SINGLEFAX_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: send-document-1',
  ],
  CURLOPT_POSTFIELDS => json_encode(['to' => '+14155552671', 'base64' => $pdf]),
  CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);
```

## Larger files, upload then send

What the CLI does: `POST /api/v1/uploads` → `PUT` bytes to `uploadUrl` → `POST /api/v1/faxes` with `uploadToken`. Pass the original Word, Excel, PDF, or image, do not convert to PDF first. The PUT URL is a capability. HMAC in the path is enough (Bearer is optional).

### curl

```bash
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\"}"
```

### JavaScript (Node 18+)

```javascript
import { readFile } from 'node:fs/promises'
import { randomUUID } from 'node:crypto'

const api = 'https://api.singlefax.com'
const key = process.env.SINGLEFAX_API_KEY
const auth = { Authorization: `Bearer ${key}` }
const pdf = await readFile('./document.pdf')

const upload = await fetch(`${api}/api/v1/uploads`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({ contentType: 'application/pdf', maxBytes: pdf.byteLength }),
}).then((r) => r.json())

await fetch(upload.uploadUrl, {
  method: 'PUT',
  headers: { ...auth, 'Content-Type': 'application/pdf' },
  body: pdf,
})

const fax = await fetch(`${api}/api/v1/faxes`, {
  method: 'POST',
  headers: {
    ...auth,
    'Content-Type': 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({ to: '+14155552671', uploadToken: upload.uploadToken }),
}).then((r) => r.json())

console.log(fax)
```

Python, Go, and PHP upload examples: https://singlefax.com/developers

## REST reference

| Method | Path | Auth | Notes |
| --- | --- | --- | --- |
| POST | `/api/v1/faxes` | `fax:send` | Idempotency-Key required. JSON `{ to, base64 \| uploadToken }` or multipart file. |
| GET | `/api/v1/faxes` | `fax:read` | List. Query `limit`, `offset`. |
| GET | `/api/v1/faxes/{id}` | `fax:read` | Status including latest delivery attempt. |
| POST | `/api/v1/faxes/quote` | `fax:read` | `{ to, pages? \| base64? }`, quote without charging. |
| POST | `/api/v1/uploads` | `fax:send` | Returns `uploadToken` + `uploadUrl`. PUT the bytes, then send with `uploadToken`. |
| GET | `/api/v1/credits` | `credits:read` | Prepaid balance. |
| POST | `/api/v1/credits/checkout` | `credits:checkout` | `{ amountCents }`. Stripe Checkout for a credit pack. |
| GET | `/api/v1/numbers` | `fax:read` | Active fax numbers. |
| GET | `/api/v1/received-faxes` | `inbox:read` | Paginated inbound inbox. Held/HIPAA/EIN/legacy = envelope only. |
| GET | `/api/v1/received-faxes/{id}` | `inbox:read` | Detail + bounded transcript slice for non-HIPAA released rows. Paid inbound runs searchable extract automatically. |
| POST | `/api/v1/numbers` | `credits:checkout` | `{ sku?, country?, state? }`, default `inbound_30day`. Returns `checkoutUrl` + reserved `phoneNumber`. |
| GET | `/api/v1/skus` | public | Catalog. Same shape as MCP `list_skus`. |
| POST | `/api/v1/checkout` | `credits:checkout` | `{ sku }`, `credit_pack`, inbound, subscriptions, lifetime. |

HTTP 402 on `/api/v1/faxes` means insufficient credits, that is not an MPP challenge. MPP 402 is only `POST /api/v1/machine/faxes`.

Schemas: https://singlefax.com/openapi.json
Versioning (90-day Sunset): https://singlefax.com/versioning.md

## Webhooks

Signed `fax.queued`, `fax.delivered`, `fax.failed`, and `credits.low_balance` events. Header `SingleFax-Signature` (`t=,v1=`). HMAC-SHA256 of `timestamp.payload` over the raw body. Configure endpoints at https://singlefax.com/dashboard/webhooks

### JavaScript

```javascript
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)
}
```

### Python

```python
import hashlib, hmac, time

def verify_singlefax_signature(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp = int(parts["t"])
    if abs(int(time.time()) - timestamp) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

## n8n

Community node `@singlefax/n8n-nodes-singlefax` ([github.com/SingleFax/n8n-nodes-singlefax](https://github.com/SingleFax/n8n-nodes-singlefax)). API key credential. Send Fax uploads the original file, then `POST /api/v1/faxes` with `Idempotency-Key`. Get Fax Status, Quote Fax, Get Credits, and List Received Faxes call the same REST paths as the CLI. On Received Fax polls `GET /api/v1/received-faxes`. On Fax Delivered verifies `SingleFax-Signature` for `fax.delivered` and `fax.failed`. Not Secure Mode. Do not send protected health information.

Install `@singlefax/n8n-nodes-singlefax` from npm on self-hosted n8n. The n8n Cloud listing is submitted and waiting on n8n approval. Develop and release the node only in that public repo.

## Named developer resources

| Resource | URL |
| --- | --- |
| HTML developer docs | https://singlefax.com/developers |
| SingleFax OpenAPI spec | https://singlefax.com/openapi.json |
| SingleFax auth docs | https://singlefax.com/auth.md |
| SingleFax MCP server | https://singlefax.com/mcp |
| SingleFax agent guide | https://singlefax.com/agents.md |
| SingleFax pricing | https://singlefax.com/pricing.md |
| SingleFax CLI (npm) | https://www.npmjs.com/package/@singlefax/cli |
| SingleFax MPP guide | https://singlefax.com/mpp.md |
| Stripe MPP vs Instant Checkout | https://singlefax.com/blog/stripe-machine-payments-protocol |
| API versioning | https://singlefax.com/versioning.md |
