Digital Transformation

Fax API Integration: A Developer's Guide

Build fax capabilities into your applications with modern REST APIs. Code examples, best practices, and architecture patterns for enterprise fax integration.

Farjad Fani
Farjad Fani
Enterprise Fax Consultant
October 30, 2024
12 min read
API integration REST API developer guide webhook automation
Fax API Integration: A Developer's Guide

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.

Farjad Fani

About the Author

Farjad Fani is an enterprise fax consultant with 25+ years of experience. He built onlinefaxes.com and sold over 100,000 customers to eFax. Today, he helps healthcare, finance, and government organizations modernize their fax infrastructure while maintaining compliance.

Get in touch

Ready to Modernize Your Fax Infrastructure?

Let's discuss your specific challenges and find the right solution for your organization.

Get in Touch
Let's Talk
Before You Go

Let's Connect

Have questions about your fax infrastructure? I respond personally within 24 hours.