Documentation API et CLI SingleFax
Clé API et crédits prépayés. CLI officielle npm ou exemples REST à copier en curl, Node, Python, Go ou PHP. Les applications de chat doivent utiliser MCP.
CLI
npx @singlefax/cli send FILE --to +E164, avec le fichier Word, Excel, PDF ou image d’origine, sans conversion préalable en PDF.
REST
POST /api/v1/faxes avec Bearer sf_live_… et un Idempotency-Key.
Démarrage rapide
- Créez une clé à portée limitée sur Clés API, avec
fax:send,fax:read,inbox:read,credits:read. - Achetez des crédits dans le compte ou via
npx @singlefax/cli credits buy. - Envoyez avec la CLI ci-dessous ou
POST /api/v1/faxes.
URL de base https://api.singlefax.com, mêmes chemins sur https://singlefax.com. Les clés sf_test_ n’envoient pas de fax réels. Ne placez pas de clé dans le navigateur.
CLI
Node 20 minimum. Configuration : SINGLEFAX_API_KEY ou ~/.config/singlefax/config.json. Fournissez le fichier original, Word, Excel, PowerPoint, PDF, texte ou image, sans conversion préalable en PDF. Ne convient pas à MCP OAuth ni 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 20Envoyer un fax
Une requête : document en base64 et destination. PDF, Word, Excel, PowerPoint, texte ou images. Idempotency-Key est obligatoire. Réutiliser la clé avec un contenu différent renvoie 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);Fichiers volumineux : téléverser, puis envoyer
La CLI crée un jeton d’import, envoie les octets par PUT, puis utilise uploadToken. Le PUT exige aussi le jeton Bearer. Préférez ce parcours à un gros fichier dans du JSON. Ne convertissez pas d’abord en 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);Référence REST
| Méthode | Chemin | Authentification | 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/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. |
Schémas complets : openapi.json. Changements incompatibles : politique de versionnement, préavis Sunset de 90 jours. HTTP 402 sur cette API de crédits signifie un solde insuffisant. Le 402 MPP concerne seulement POST /api/v1/machine/faxes.
Webhooks
Événements signés fax.queued, fax.delivered, fax.failed et credits.low_balance. Vérifiez SingleFax-Signature, t=,v1=, sur le corps brut. HMAC-SHA256 de timestamp.payload. Ajoutez un point d’accès dans webhooks du compte.
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"])Contraintes
- Gardez les clés API côté serveur. N’intégrez pas
sf_live_dans une page web. - N’envoyez pas de PHI. Les organismes concernés utilisent HIPAA Secure Mode sur le site avec un BAA électronique.
- Destinations E.164 valides. PDF, Word, Excel, PowerPoint, RTF, texte brut et images ; types MIME dans OpenAPI. Ne convertissez pas en PDF au préalable, nous le faisons avant l’envoi.
- MCP de chat et Stripe MPP : /agents, mpp.md et MPP ou Instant Checkout.
Questions fréquentes
Documentation lisible par les machines
Markdown simple et OpenAPI pour les robots et agents de programmation. Les mêmes informations que sur cette page.