Documentazione API e CLI SingleFax
Chiave API e crediti prepagati. CLI ufficiale npm o esempi REST pronti per curl, Node, Python, Go e PHP. Le chat usano invece MCP.
CLI
npx @singlefax/cli send FILE --to +E164, passando Word, Excel, PDF o immagine originali, senza conversione preventiva.
REST
POST /api/v1/faxes con Bearer sf_live_… e Idempotency-Key.
Avvio rapido
- Crea una chiave con permessi limitati su Chiavi API (
fax:send,fax:read,inbox:read,credits:read). - Acquista crediti nel pannello o tramite
npx @singlefax/cli credits buy. - Invia con la CLI sotto o
POST /api/v1/faxes.
URL base https://api.singlefax.com (stessi percorsi su https://singlefax.com). Le chiavi sf_test_ non inviano realmente. Non mettere chiavi nel browser.
CLI
Node 20+. Configurazione: SINGLEFAX_API_KEY o ~/.config/singlefax/config.json. Passa il file originale (Word, Excel, PowerPoint, PDF, testo o immagine), senza convertirlo prima. Non per MCP OAuth o Stripe MPP.
export SINGLEFAX_API_KEY=sf_live_…
npx @singlefax/cli send ./document.docx --to +14155552671
npx @singlefax/cli send ./document.pdf --to +14155552671 --wait --jsonnpx @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 20Invia un fax
Una richiesta: documento base64 e destinazione. PDF, Word, Excel, PowerPoint, testo o immagini. Idempotency-Key obbligatorio. Riutilizzare una chiave con corpo diverso restituisce 409.
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\"}"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())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())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
$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);File grandi: carica, poi invia
La CLI crea un token di caricamento, esegue PUT dei byte e invia con uploadToken. Anche PUT richiede Bearer. Preferiscilo a grandi file nel JSON. Non convertire prima in PDF.
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\"}"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)import 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()
def request(url, *, data=None, method="GET", headers=None, content_type="application/json"):
req = urllib.request.Request(
url,
data=data,
headers={
"Authorization": f"Bearer {KEY}",
"Content-Type": content_type,
**(headers or {}),
},
method=method,
)
with urllib.request.urlopen(req) as res:
return res.read()
upload = json.loads(request(
f"{API}/api/v1/uploads",
method="POST",
data=json.dumps({"contentType": "application/pdf", "maxBytes": len(pdf)}).encode(),
))
request(upload["uploadUrl"], method="PUT", data=pdf, content_type="application/pdf")
fax = request(
f"{API}/api/v1/faxes",
method="POST",
data=json.dumps({"to": "+14155552671", "uploadToken": upload["uploadToken"]}).encode(),
headers={"Idempotency-Key": str(uuid.uuid4())},
)
print(fax.decode())package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
key := os.Getenv("SINGLEFAX_API_KEY")
pdf, err := os.ReadFile("document.pdf")
if err != nil {
panic(err)
}
upBody, _ := json.Marshal(map[string]any{"contentType": "application/pdf", "maxBytes": len(pdf)})
upReq, _ := http.NewRequest("POST", "https://api.singlefax.com/api/v1/uploads", bytes.NewReader(upBody))
upReq.Header.Set("Authorization", "Bearer "+key)
upReq.Header.Set("Content-Type", "application/json")
upRes, err := http.DefaultClient.Do(upReq)
if err != nil {
panic(err)
}
defer upRes.Body.Close()
var upload struct {
UploadToken string `json:"uploadToken"`
UploadUrl string `json:"uploadUrl"`
}
json.NewDecoder(upRes.Body).Decode(&upload)
put, _ := http.NewRequest("PUT", upload.UploadUrl, bytes.NewReader(pdf))
put.Header.Set("Authorization", "Bearer "+key)
put.Header.Set("Content-Type", "application/pdf")
if _, err := http.DefaultClient.Do(put); err != nil {
panic(err)
}
sendBody, _ := json.Marshal(map[string]string{"to": "+14155552671", "uploadToken": upload.UploadToken})
faxReq, _ := http.NewRequest("POST", "https://api.singlefax.com/api/v1/faxes", bytes.NewReader(sendBody))
faxReq.Header.Set("Authorization", "Bearer "+key)
faxReq.Header.Set("Content-Type", "application/json")
faxReq.Header.Set("Idempotency-Key", "send-document-1")
faxRes, err := http.DefaultClient.Do(faxReq)
if err != nil {
panic(err)
}
defer faxRes.Body.Close()
out, _ := io.ReadAll(faxRes.Body)
fmt.Println(string(out))
}<?php
$key = getenv('SINGLEFAX_API_KEY');
$pdf = file_get_contents('document.pdf');
$ch = curl_init('https://api.singlefax.com/api/v1/uploads');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $key",
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['contentType' => 'application/pdf', 'maxBytes' => strlen($pdf)]),
CURLOPT_RETURNTRANSFER => true,
]);
$upload = json_decode(curl_exec($ch), true);
$ch = curl_init($upload['uploadUrl']);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $key",
'Content-Type: application/pdf',
],
CURLOPT_POSTFIELDS => $pdf,
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
$ch = curl_init('https://api.singlefax.com/api/v1/faxes');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $key",
'Content-Type: application/json',
'Idempotency-Key: send-document-1',
],
CURLOPT_POSTFIELDS => json_encode(['to' => '+14155552671', 'uploadToken' => $upload['uploadToken']]),
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);Riferimento REST
| Metodo | Percorso | Autenticazione | Note |
|---|---|---|---|
| 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/received-faxes | inbox:read | Inbound inbox list. Query limit, cursor. No transcripts in list. |
| GET | /api/v1/received-faxes/{id} | inbox:read | Inbound detail with bounded transcript slice. HIPAA Secure content is omitted. |
| GET | /api/v1/numbers | fax:read | Active fax numbers. |
| 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. |
Schemi completi: openapi.json. Modifiche incompatibili: politica delle versioni (Sunset di 90 giorni). HTTP 402 su questa API crediti significa saldo insufficiente; MPP 402 è solo POST /api/v1/machine/faxes.
Webhook
Eventi firmati fax.queued, fax.delivered, fax.failed, credits.low_balance. Verifica SingleFax-Signature (t=,v1=) sul corpo originale. HMAC-SHA256 di timestamp.payload. Aggiungi un endpoint in webhook del pannello.
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)
}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"])Vincoli
- Conserva le chiavi sul server. Non incorporare
sf_live_in una pagina web. - Non inviare PHI. Gli enti soggetti usano HIPAA Secure sul sito con BAA elettronico.
- Destinazioni E.164 valide. PDF, Word, Excel, PowerPoint, RTF, testo e immagini: MIME in OpenAPI. Non convertire prima in PDF, lo facciamo prima dell’invio.
- MCP chat e Stripe MPP: /agents, mpp.md, MPP e Instant Checkout a confronto.
Domande frequenti
Documentazione leggibile dalle macchine
Markdown semplice e OpenAPI per crawler e agenti di programmazione. Stesse informazioni della pagina.