Modern cloud fax platforms expose REST APIs that let developers build fax capabilities directly into applications. This guide covers the patterns, practices, and considerations for enterprise fax API integration.
API Architecture Overview
Typical Cloud Fax API Structure
Most enterprise fax APIs follow RESTful conventions:
Base URL: https://api.faxprovider.com/v1
Endpoints:
POST /faxes - Send a fax
GET /faxes/{id} - Get fax status
GET /faxes - List faxes
DELETE /faxes/{id} - Cancel pending fax
POST /faxes/{id}/resend - Resend failed fax
GET /numbers - List fax numbers
POST /numbers - Provision new number
POST /webhooks - Register webhook
GET /webhooks - List webhooks
DELETE /webhooks/{id} - Remove webhook
Authentication Patterns
API Key Authentication
GET /faxes HTTP/1.1
Host: api.faxprovider.com
Authorization: Bearer your-api-key-here
OAuth 2.0 (Enterprise)
POST /oauth/token HTTP/1.1
Host: api.faxprovider.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=your-client-id&
client_secret=your-client-secret
Sending Faxes
Basic Send Request
const sendFax = async (recipientNumber, documentBase64) => {
const response = await fetch('https://api.faxprovider.com/v1/faxes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: recipientNumber,
documents: [{
content: documentBase64,
contentType: 'application/pdf'
}],
coverPage: {
enabled: true,
subject: 'Medical Records Request',
message: 'Please find attached documentation.'
}
})
});
return response.json();
};
Handling Multiple Documents
const sendMultiDocFax = async (recipient, documents) => {
const formattedDocs = documents.map((doc, index) => ({
content: doc.base64,
contentType: doc.mimeType,
order: index + 1
}));
const response = await fetch('https://api.faxprovider.com/v1/faxes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: recipient,
documents: formattedDocs,
options: {
resolution: 'fine', // 'standard' or 'fine'
retries: 3,
retryDelay: 300 // seconds between retries
}
})
});
return response.json();
};
Response Handling
// Successful send response
{
"id": "fax_abc123def456",
"status": "queued",
"to": "+14155551234",
"from": "+14155550000",
"pageCount": 3,
"createdAt": "2024-10-30T10:30:00Z",
"estimatedDelivery": "2024-10-30T10:35:00Z"
}
// Error response
{
"error": {
"code": "invalid_recipient",
"message": "The recipient number is not a valid fax number",
"details": {
"number": "+14155551234",
"reason": "number_type_mismatch"
}
}
}
Receiving Faxes (Webhooks)
Webhook Registration
const registerWebhook = async (callbackUrl, events) => {
const response = await fetch('https://api.faxprovider.com/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: callbackUrl,
events: events, // ['fax.received', 'fax.completed', 'fax.failed']
secret: 'your-webhook-secret'
})
});
return response.json();
};
Webhook Payload Structure
// Inbound fax received
{
"event": "fax.received",
"timestamp": "2024-10-30T10:45:00Z",
"data": {
"id": "fax_xyz789",
"from": "+14155559999",
"to": "+14155550000",
"pageCount": 5,
"status": "received",
"documents": [{
"id": "doc_abc123",
"url": "https://api.faxprovider.com/v1/documents/doc_abc123",
"pageCount": 5,
"expiresAt": "2024-10-31T10:45:00Z"
}]
},
"signature": "sha256=..."
}
Webhook Handler Implementation
// Express.js webhook handler
const crypto = require('crypto');
app.post('/webhooks/fax', express.json(), (req, res) => {
// Verify webhook signature
const signature = req.headers['x-webhook-signature'];
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== `sha256=${expectedSignature}`) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = req.body;
switch (event) {
case 'fax.received':
handleInboundFax(data);
break;
case 'fax.completed':
handleFaxSuccess(data);
break;
case 'fax.failed':
handleFaxFailure(data);
break;
}
res.status(200).json({ received: true });
});
Enterprise Integration Patterns
Pattern 1: EHR Integration
// Epic integration example
class EpicFaxIntegration {
constructor(faxApi, epicApi) {
this.faxApi = faxApi;
this.epicApi = epicApi;
}
async sendPatientDocument(patientId, documentId, recipientFax) {
// Retrieve document from Epic
const document = await this.epicApi.getDocument(documentId);
// Send via fax
const faxResult = await this.faxApi.sendFax({
to: recipientFax,
documents: [{
content: document.base64,
contentType: 'application/pdf'
}],
metadata: {
patientId: patientId,
documentId: documentId,
source: 'epic'
}
});
// Log to Epic audit trail
await this.epicApi.logActivity({
patientId: patientId,
action: 'fax_sent',
details: {
faxId: faxResult.id,
recipient: recipientFax,
documentId: documentId
}
});
return faxResult;
}
async handleInboundFax(webhookData) {
// Download fax document
const document = await this.faxApi.getDocument(webhookData.documents[0].id);
// OCR to extract patient info
const extractedData = await this.ocrService.extract(document);
// Match to patient in Epic
const patient = await this.epicApi.findPatient(extractedData);
// Create InBasket message
await this.epicApi.createInBasketItem({
patientId: patient.id,
type: 'fax_received',
document: document,
from: webhookData.from
});
}
}
Pattern 2: Claims System Integration
// Insurance claims integration
class ClaimsFaxAutomation {
async processInboundClaim(faxData) {
// Download and OCR the fax
const document = await this.faxApi.getDocument(faxData.documentId);
const ocrResult = await this.ocrService.process(document);
// Extract claim data
const claimData = this.extractClaimInfo(ocrResult);
// Find or create claim
let claim = await this.claimsDb.findByNumber(claimData.claimNumber);
if (!claim) {
claim = await this.claimsDb.create({
claimNumber: claimData.claimNumber,
status: 'new',
source: 'fax'
});
}
// Attach document
await this.claimsDb.attachDocument(claim.id, {
type: claimData.documentType,
content: document,
receivedAt: faxData.timestamp,
from: faxData.from
});
// Route to appropriate queue
await this.routingEngine.route(claim);
}
async requestMedicalRecords(claimId, providerId) {
const claim = await this.claimsDb.get(claimId);
const provider = await this.providerDb.get(providerId);
// Generate request letter
const requestLetter = await this.documentGenerator.createMedicalRecordsRequest({
claim: claim,
provider: provider
});
// Send fax
const faxResult = await this.faxApi.sendFax({
to: provider.faxNumber,
documents: [requestLetter],
tracking: {
claimId: claimId,
requestType: 'medical_records'
}
});
// Update claim
await this.claimsDb.addActivity(claimId, {
type: 'records_requested',
providerId: providerId,
faxId: faxResult.id
});
}
}
Pattern 3: Queue-Based Processing
// High-volume processing with message queue
const Queue = require('bull');
const faxSendQueue = new Queue('fax-send', REDIS_URL);
const faxReceiveQueue = new Queue('fax-receive', REDIS_URL);
// Producer: Queue outbound faxes
app.post('/api/send-fax', async (req, res) => {
const job = await faxSendQueue.add({
recipient: req.body.recipient,
document: req.body.document,
priority: req.body.priority || 'normal',
metadata: req.body.metadata
}, {
priority: req.body.priority === 'urgent' ? 1 : 10,
attempts: 3,
backoff: {
type: 'exponential',
delay: 60000
}
});
res.json({ jobId: job.id, status: 'queued' });
});
// Consumer: Process outbound queue
faxSendQueue.process(10, async (job) => {
const { recipient, document, metadata } = job.data;
const result = await faxApi.sendFax({
to: recipient,
documents: [document],
metadata: metadata
});
return result;
});
// Handle webhook by adding to receive queue
app.post('/webhooks/fax', async (req, res) => {
await faxReceiveQueue.add(req.body);
res.status(200).send('OK');
});
// Process inbound faxes
faxReceiveQueue.process(5, async (job) => {
const faxData = job.data;
// Download document
// OCR processing
// Route to appropriate system
// Update tracking
});
Error Handling Best Practices
Retry Logic
const sendFaxWithRetry = async (faxData, maxRetries = 3) => {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await faxApi.sendFax(faxData);
return result;
} catch (error) {
lastError = error;
// Don't retry on certain errors
if (error.code === 'invalid_recipient' ||
error.code === 'invalid_document') {
throw error;
}
// Exponential backoff
if (attempt < maxRetries) {
await sleep(Math.pow(2, attempt) * 1000);
}
}
}
throw lastError;
};
Error Classification
const classifyFaxError = (error) => {
const retryableErrors = [
'busy',
'no_answer',
'network_error',
'rate_limited'
];
const permanentErrors = [
'invalid_recipient',
'invalid_document',
'number_blocked',
'account_suspended'
];
if (retryableErrors.includes(error.code)) {
return { retry: true, delay: 300 };
}
if (permanentErrors.includes(error.code)) {
return { retry: false, action: 'alert' };
}
return { retry: true, delay: 60, escalate: true };
};
Security Considerations
API Key Management
// Never hardcode API keys
// Use environment variables or secret management
const API_KEY = process.env.FAX_API_KEY;
// Rotate keys regularly
// Monitor for unauthorized usage
// Use separate keys for different environments
Data Protection
// Encrypt sensitive data before storing
const encryptFaxMetadata = (metadata) => {
return crypto.encrypt(JSON.stringify(metadata), ENCRYPTION_KEY);
};
// Implement data retention policies
const cleanupOldFaxes = async () => {
const cutoffDate = new Date();
cutoffDate.setFullYear(cutoffDate.getFullYear() - 7); // 7 year retention
await faxDb.deleteOlderThan(cutoffDate);
};
// Log access for audit purposes
const logFaxAccess = async (userId, faxId, action) => {
await auditLog.write({
timestamp: new Date(),
userId: userId,
resource: `fax:${faxId}`,
action: action
});
};
Testing Strategies
Unit Testing
describe('FaxService', () => {
it('should send fax successfully', async () => {
const mockApi = {
sendFax: jest.fn().mockResolvedValue({
id: 'fax_123',
status: 'queued'
})
};
const service = new FaxService(mockApi);
const result = await service.send('+14155551234', testDocument);
expect(result.status).toBe('queued');
expect(mockApi.sendFax).toHaveBeenCalledWith(
expect.objectContaining({
to: '+14155551234'
})
);
});
});
Integration Testing
describe('Fax Integration', () => {
it('should complete round-trip fax', async () => {
// Send fax to test number
const sendResult = await faxApi.sendFax({
to: TEST_FAX_NUMBER,
documents: [testDocument]
});
// Wait for completion
const finalStatus = await waitForStatus(sendResult.id, 'completed', 120000);
expect(finalStatus.status).toBe('completed');
expect(finalStatus.pageCount).toBe(1);
});
});
The Bottom Line
Fax APIs transform fax from a manual process to a programmable capability. Whether you’re building EHR integration, claims automation, or custom workflows, modern APIs provide the building blocks for enterprise-grade solutions.
The key is treating fax integration with the same rigor as any other critical system integration—proper error handling, security practices, and testing strategies.
Building a fax integration? Let’s discuss your architecture and requirements.