Documentación API y CLI de SingleFax
Clave API y crédito prepago. CLI oficial de npm o ejemplos REST en curl, Node, Python, Go y PHP. Las aplicaciones de chat deben usar MCP.
CLI
npx @singlefax/cli send FILE --to +E164: envía Word, Excel, PDF o imagen original, sin convertir antes a PDF.
REST
POST /api/v1/faxes con Bearer sf_live_… y Idempotency-Key.
Inicio rápido
- Crea una clave con permisos limitados en Claves API:
fax:send,fax:read,inbox:read,credits:read. - Compra crédito en el panel o mediante
npx @singlefax/cli credits buy. - Envía con la CLI de abajo o con
POST /api/v1/faxes.
URL base https://api.singlefax.com, mismas rutas en https://singlefax.com. Las claves sf_test_ no envían faxes reales. No las pongas en el navegador.
CLI
Node 20 o superior. Configuración: SINGLEFAX_API_KEY o ~/.config/singlefax/config.json. Envía el Word, Excel, PowerPoint, PDF, texto o imagen original; no conviertas antes. No sirve para 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 20Enviar un fax
Una solicitud: documento en base64 y destino. PDF, Word, Excel, PowerPoint, texto o imágenes. Idempotency-Key es obligatorio. Reutilizar una clave con otro cuerpo devuelve 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);Archivos grandes: subir y después enviar
La CLI crea un token de subida, carga los datos con PUT y envía con uploadToken. PUT también requiere Bearer. Es preferible a incluir un archivo grande en JSON. No conviertas antes a 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);Referencia REST
| Método | Ruta | Autenticación | Notas |
|---|---|---|---|
| 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. |
Esquemas completos: openapi.json. Cambios incompatibles: política de versiones, Sunset de 90 días. HTTP 402 en esta API significa saldo insuficiente. MPP 402 solo se usa en POST /api/v1/machine/faxes.
Webhooks
Eventos firmados fax.queued, fax.delivered, fax.failed y credits.low_balance. Verifica SingleFax-Signature, t=,v1=, con el cuerpo original. HMAC-SHA256 de timestamp.payload. Añade el endpoint en webhooks del panel.
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"])Limitaciones
- Guarda las claves API en el servidor. No incluyas
sf_live_en una web. - No envíes PHI. Las entidades sujetas a HIPAA usan HIPAA Secure en la web con BAA electrónico.
- Destinos E.164 válidos. PDF, Word, Excel, PowerPoint, RTF, texto e imágenes; consulta MIME en OpenAPI. No conviertas antes a PDF: lo hacemos antes de enviar.
- MCP de chat y Stripe MPP: /agents, mpp.md y MPP o pago inmediato.
Preguntas frecuentes
Documentación legible por máquinas
Markdown sencillo y OpenAPI para rastreadores y agentes de programación. Los mismos datos que esta página.